<?php

namespace App\Models\Lookups;

use App\Models\Concerns\BelongsToTenant;
use App\Services\PaymentService;
use Illuminate\Database\Eloquent\Model;

class PaymentMethod extends Model
{
    use BelongsToTenant;

    public const CASH = 'cash';

    public const CARD = 'card';

    public const BANK_TRANSFER = 'bank_transfer';

    public const CHEQUE = 'cheque';

    public const CREDIT = 'credit';

    public const OTHER = 'other';

    /**
     * Built-in codes with hardcoded checkout behavior — these never carry a
     * `fields` config; only tenant-created custom methods do.
     */
    public const BUILT_IN = [
        self::CASH, self::CARD, self::BANK_TRANSFER, self::CHEQUE, self::CREDIT, self::OTHER,
    ];

    /**
     * Internal/system codes that never appear as a chooseable tender at
     * checkout (POS buttons, invoice payment selects) and are never exposed
     * in the master-control "Payment Methods" tenant admin UI — CREDIT is
     * driven entirely by the "apply credit note" flow and OTHER backs
     * legacy/internal bookkeeping ({@see PaymentService::recordLegacyOpeningSettlement()}).
     * Both stay permanently active; only {@see BUILT_IN} minus this set, plus
     * tenant-custom methods, are ever toggled.
     */
    public const SYSTEM = [self::CREDIT, self::OTHER];

    protected $fillable = [
        'code', 'name', 'requires_reference', 'requires_cheque_fields',
        'auto_approve', 'sort_order', 'is_active', 'fields',
    ];

    protected function casts(): array
    {
        return [
            'requires_reference' => 'boolean',
            'requires_cheque_fields' => 'boolean',
            'auto_approve' => 'boolean',
            'is_active' => 'boolean',
            'sort_order' => 'integer',
            'fields' => 'array',
        ];
    }

    /**
     * Active methods, memoised for the request.
     *
     * This is a handful of rows on a lookup table, but the form layer asks for
     * it repeatedly while building a schema — the payments repeater's method
     * select, its custom-field components, the add-payment modal — and every
     * one of those runs on every render of a form that has payments on it.
     * Keeping one copy per request turns that into a single query.
     *
     * @var \Illuminate\Database\Eloquent\Collection<int, self>|null
     */
    protected static $activeCache = null;

    protected static function booted(): void
    {
        // Any write invalidates the memo, so a toggle followed by a read inside
        // one process (Master Control saving a method, then re-rendering; the
        // test suite) can never see the stale list.
        static::saved(fn () => static::flushActiveCache());
        static::deleted(fn () => static::flushActiveCache());
    }

    public static function flushActiveCache(): void
    {
        static::$activeCache = null;
    }

    /**
     * @return \Illuminate\Database\Eloquent\Collection<int, self>
     */
    public static function active()
    {
        return static::$activeCache ??= static::query()
            ->where('is_active', true)
            ->orderBy('sort_order')
            ->get();
    }

    public function isCustom(): bool
    {
        return ! in_array($this->code, self::BUILT_IN, true);
    }

    /**
     * Checks a submission's custom_fields against this method's `fields`
     * config. Null when nothing is wrong; a human-readable message otherwise.
     * No-op for built-in codes — their fields stay hardcoded in the checkout
     * flow and are validated by the request rules directly, not this config.
     *
     * @param  array<string, mixed>  $submitted
     */
    public static function validateCustomFields(string $code, array $submitted): ?string
    {
        if (in_array($code, self::BUILT_IN, true)) {
            return null;
        }

        $method = static::where('code', $code)->where('is_active', true)->first();

        if (! $method || ! $method->fields) {
            return null;
        }

        foreach ($method->fields as $field) {
            if (empty($field['required'])) {
                continue;
            }

            $value = $submitted[$field['key']] ?? null;
            if ($value === null || trim((string) $value) === '') {
                return "\"{$field['label']}\" is required for {$method->name}.";
            }
        }

        return null;
    }

    public static function findByCode(string $code): ?self
    {
        return static::query()->where('code', $code)->first();
    }

    /**
     * Like {@see findByCode()} but only returns a method a customer/cashier is
     * currently allowed to pay with — used at every user-facing payment entry
     * point (POS, invoice settlement, standalone transactions) so a toggled-off
     * method can never be paid with even if the client submits its code
     * directly. Internal/system flows that must always succeed regardless of
     * the tenant's toggle (e.g. legacy opening settlements booked as OTHER)
     * use {@see findByCode()} instead.
     */
    public static function findActiveByCode(string $code): ?self
    {
        return static::query()->where('code', $code)->where('is_active', true)->first();
    }

    public static function idOf(string $code): ?int
    {
        return static::findByCode($code)?->id;
    }

    /**
     * Active, tenant-selectable methods for checkout UIs (POS buttons, the
     * invoice "add payment" select): the built-ins minus the permanently-on
     * {@see SYSTEM} codes, plus any active custom methods — ordered the same
     * way everywhere it's rendered.
     *
     * @return array<string, string>
     */
    public static function checkoutOptions(): array
    {
        return static::active()
            ->reject(fn (self $method): bool => in_array($method->code, self::SYSTEM, true))
            ->pluck('name', 'code')
            ->all();
    }

    /**
     * Checks a submission against this method's `requires_reference` /
     * `requires_cheque_fields` flags. Null when nothing is wrong; a
     * human-readable message otherwise. Mirrors {@see validateCustomFields()}'s
     * shape so callers can run both checks the same way.
     *
     * @param  array<string, mixed>  $submitted
     */
    public static function validateRequiredFields(string $code, array $submitted): ?string
    {
        $method = static::findActiveByCode($code);

        if (! $method) {
            return null;
        }

        if ($method->requires_reference && trim((string) ($submitted['reference'] ?? '')) === '') {
            return "A reference is required for {$method->name}.";
        }

        if ($method->requires_cheque_fields) {
            $chequeFields = [
                'cheque_number' => 'Cheque number',
                'cheque_bank' => 'Bank',
                'cheque_date' => 'Cheque date',
            ];

            foreach ($chequeFields as $key => $label) {
                if (trim((string) ($submitted[$key] ?? '')) === '') {
                    return "{$label} is required for {$method->name}.";
                }
            }
        }

        return null;
    }
}
