<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * A completed (or in-progress) sale — header row + items + payments.
 *
 * Status lifecycle (per `docs/features/sales-checkout.md` §2):
 *   draft → completed → voided
 *                 ↓
 *                 → partially_refunded → refunded
 *   draft ↔ held
 *
 * Slice 1 only writes `completed` rows directly (cashier flow). Held / void /
 * refund land in later slices — the constants are here so transition actions
 * have a single source of truth.
 *
 * Totals are server-computed inside {@see App\Actions\Sales\CompleteSale} from
 * per-line resolver output. Anything that mutates the row's financial state
 * (paid_total, balance_due, status, voided_at) goes through an Action with a
 * `lockForUpdate()` + `forceFill()`, never form-driven mass assignment.
 */
class Sale extends Model
{
    use SoftDeletes;

    public const STATUS_DRAFT              = 'draft';
    public const STATUS_HELD               = 'held';
    public const STATUS_PLACED             = 'placed';   // kiosk order awaiting counter payment
    public const STATUS_COMPLETED          = 'completed';
    public const STATUS_VOIDED             = 'voided';
    public const STATUS_PARTIALLY_REFUNDED = 'partially_refunded';
    public const STATUS_REFUNDED           = 'refunded';

    /**
     * Mass-assignable columns. Deliberately omits:
     *   - `number`                                — server-generated by GenerateSaleNumber
     *   - `status`, `voided_at`, `void_reason`    — owned by transition actions
     *   - `subtotal`, `discount_total`, `tax_total`, `additional_charges_total`,
     *     `rounding_adjustment`, `grand_total`    — computed by CompleteSale
     *   - `paid_total`, `change_returned`,
     *     `balance_due`                           — derived from payments
     *   - `held_*`, `synced_*`, `irn_*`           — owned by their respective flows
     *
     * Actions that need to write these columns use `forceFill()`.
     */
    protected $fillable = [
        'store_id', 'terminal_id', 'shift_id',
        'cashier_id', 'customer_id',
        'local_uuid',
        'sale_date', 'sale_datetime',
        'currency_code', 'exchange_rate_to_base',
        'notes',
        'origin', 'kiosk_note', 'pickup_code', 'kiosk_payment_claim_method_id', 'kiosk_collected_at',
        'is_b2b_invoice', 'requires_einvoice',
        'created_by', 'updated_by',
    ];

    protected function casts(): array
    {
        return [
            'sale_date'                 => 'date',
            'sale_datetime'             => 'datetime',
            'exchange_rate_to_base'     => 'decimal:10',
            'subtotal'                  => 'decimal:4',
            'discount_total'            => 'decimal:4',
            'tax_total'                 => 'decimal:4',
            'additional_charges_total'  => 'decimal:4',
            'rounding_adjustment'       => 'decimal:4',
            'grand_total'                => 'decimal:4',
            'paid_total'                => 'decimal:4',
            'change_returned'           => 'decimal:4',
            'balance_due'               => 'decimal:4',
            'held_at'                   => 'datetime',
            'held_discount'             => 'array',
            'placed_at'                 => 'datetime',
            'kiosk_collected_at'        => 'datetime',
            'voided_at'                 => 'datetime',
            'synced_from_offline'       => 'boolean',
            'client_completed_at'       => 'datetime',
            'synced_at'                 => 'datetime',
            'is_b2b_invoice'            => 'boolean',
            'requires_einvoice'         => 'boolean',
            'irn_acknowledged_at'       => 'datetime',
        ];
    }

    /* ── Status helpers ─────────────────────────────────────────── */

    public function isCompleted(): bool { return $this->status === self::STATUS_COMPLETED; }
    public function isHeld(): bool      { return $this->status === self::STATUS_HELD; }
    public function isPlaced(): bool    { return $this->status === self::STATUS_PLACED; }
    public function isVoided(): bool    { return $this->status === self::STATUS_VOIDED; }

    /* ── Scopes ─────────────────────────────────────────────────── */

    /** @param Builder<Sale> $q */
    public function scopeCompleted(Builder $q): void
    {
        $q->where('status', self::STATUS_COMPLETED);
    }

    /** @param Builder<Sale> $q */
    public function scopeForStore(Builder $q, int $storeId): void
    {
        $q->where('store_id', $storeId);
    }

    /** Kiosk orders awaiting counter payment — the staff orders queue. */
    /** @param Builder<Sale> $q */
    public function scopePlaced(Builder $q): void
    {
        $q->where('status', self::STATUS_PLACED);
    }

    /**
     * Kiosk sales already PAID at the machine whose goods are still behind the
     * counter. The customer holds a pickup code and nothing else; staff work
     * this list, hand the bag over, and stamp `kiosk_collected_at`.
     *
     * @param Builder<Sale> $q
     */
    public function scopeAwaitingCollection(Builder $q): void
    {
        $q->where('status', self::STATUS_COMPLETED)
            ->where('origin', 'kiosk')
            ->whereNotNull('pickup_code')
            ->whereNull('kiosk_collected_at');
    }

    /** Active = not voided. Used by reports + the index list. */
    /** @param Builder<Sale> $q */
    public function scopeActive(Builder $q): void
    {
        $q->whereNull('voided_at');
    }

    /* ── Relationships ──────────────────────────────────────────── */

    /** @return BelongsTo<Store, $this> */
    public function store(): BelongsTo { return $this->belongsTo(Store::class); }

    /** @return BelongsTo<Customer, $this> */
    public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }

    /** @return BelongsTo<User, $this> */
    public function cashier(): BelongsTo { return $this->belongsTo(User::class, 'cashier_id'); }

    /**
     * The method a kiosk shopper CLAIMS they paid with by scanning the
     * station's static UPI QR. Not a payment — see the column's migration.
     *
     * @return BelongsTo<PaymentMethod, $this>
     */
    public function kioskPaymentClaimMethod(): BelongsTo
    {
        return $this->belongsTo(PaymentMethod::class, 'kiosk_payment_claim_method_id');
    }

    /**
     * The transaction note the kiosk embedded in its `upi://pay?…` QR, which
     * the customer's UPI app carries into the merchant's statement narration.
     *
     * Staff use it to find the transfer: without it they are matching a bare
     * amount against every other transfer of the same value in the last ten
     * minutes. Kept in lockstep with the `tn` param built in kiosk-app.js
     * (`_orderUuid().slice(0, 12)`), so changing one means changing the other.
     */
    public function upiClaimReference(): ?string
    {
        if (! $this->kiosk_payment_claim_method_id || ! $this->local_uuid) {
            return null;
        }

        return substr((string) $this->local_uuid, 0, 12);
    }

    /** @return BelongsTo<Shift, $this> */
    public function shift(): BelongsTo { return $this->belongsTo(Shift::class); }

    /** @return BelongsTo<User, $this> */
    public function voider(): BelongsTo { return $this->belongsTo(User::class, 'voided_by'); }

    /** Manager who approved an over-threshold discount (Discounts Slice 2). */
    /** @return BelongsTo<User, $this> */
    public function discountApprover(): BelongsTo { return $this->belongsTo(User::class, 'discount_approved_by'); }

    /** Order-level discount audit row (Discounts Slice 3). */
    /** @return \Illuminate\Database\Eloquent\Relations\HasOne<SaleDiscount, $this> */
    public function saleDiscount(): \Illuminate\Database\Eloquent\Relations\HasOne
    {
        return $this->hasOne(SaleDiscount::class);
    }

    /** @return HasMany<SaleItem, $this> */
    public function items(): HasMany
    {
        return $this->hasMany(SaleItem::class)->orderBy('sort_order')->orderBy('id');
    }

    /** @return HasMany<SaleReturn, $this> */
    public function returns(): HasMany
    {
        return $this->hasMany(SaleReturn::class)->orderByDesc('return_date')->orderByDesc('id');
    }

    /** @return HasMany<SalePayment, $this> */
    public function payments(): HasMany
    {
        return $this->hasMany(SalePayment::class)->orderBy('paid_at')->orderBy('id');
    }

    /** @return MorphMany<ReceiptPublicLink, $this> */
    public function receiptPublicLinks(): MorphMany
    {
        return $this->morphMany(ReceiptPublicLink::class, 'reference');
    }

    /**
     * The live public link to this sale's receipt, minting one on first
     * ask. Stable across reprints/re-sends so a shared URL keeps working.
     */
    public function ensurePublicReceiptLink(): ReceiptPublicLink
    {
        return ReceiptPublicLink::for($this);
    }

    /** The no-login public receipt URL (used by the CFD QR, WhatsApp, email). */
    public function publicReceiptUrl(): string
    {
        return route('receipt.public', ['token' => $this->ensurePublicReceiptLink()->token]);
    }

    /** Payment-method providers that are true online gateways. */
    public const GATEWAY_PROVIDERS = ['stripe', 'razorpay', 'paystack', 'flutterwave', 'mercado_pago'];

    /**
     * The payment methods a refund on this sale may go back through.
     *
     * Rules:
     *   - Non-gateway tenders (cash, bank transfer, manual card, store
     *     credit, …) are ALWAYS offered — the cashier can hand money back
     *     however suits, regardless of how the sale was paid.
     *   - Gateway tenders are restricted to the gateway(s) actually used on
     *     this sale — a gateway refund reverses the original charge, so you
     *     can't refund a Razorpay charge via Stripe. Gateways NOT used on the
     *     sale are hidden.
     *
     * Queries PaymentMethod directly (not the eager-loaded relation) so the
     * `provider` column is always available for the gateway test.
     *
     * @return \Illuminate\Support\Collection<int, \App\Models\PaymentMethod>
     */
    public function refundMethods(): \Illuminate\Support\Collection
    {
        // Gateway methods actually used to pay THIS sale.
        $usedMethodIds = $this->payments->pluck('payment_method_id')->filter()->unique()->all();

        $usedGateways = empty($usedMethodIds)
            ? collect()
            : PaymentMethod::query()
                ->whereIn('id', $usedMethodIds)
                ->whereIn('provider', self::GATEWAY_PROVIDERS)
                ->get(['id', 'code', 'name', 'type', 'provider']);

        // Every active non-gateway tender.
        $nonGateways = PaymentMethod::query()
            ->where('is_active', true)
            ->where(fn ($q) => $q->whereNull('provider')->orWhereNotIn('provider', self::GATEWAY_PROVIDERS))
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get(['id', 'code', 'name', 'type', 'provider']);

        return $nonGateways->concat($usedGateways)->unique('id')->values();
    }
}
