<?php

namespace Database\Seeders\Demo;

use App\Models\Lookups\PaymentMethod;
use App\Models\Lookups\Status;
use App\Models\Sales\Invoice;
use App\Models\Sales\Transaction;
use App\Services\PaymentService;
use Database\Seeders\Demo\Concerns\GeneratesDemoData;
use Illuminate\Database\Seeder;
use RuntimeException;

/**
 * Standalone transactions layered on top of SalesSeeder's unpaid/partial
 * invoices: multi-invoice allocations (some overpaying into a credit note),
 * plus a handful of cheques in each lifecycle state.
 */
class PaymentSeeder extends Seeder
{
    use GeneratesDemoData;

    private const ALLOCATION_COUNT = 55;

    private const STANDALONE_CHEQUE_COUNT = 15;

    public function run(): void
    {
        $payments = app(PaymentService::class);
        $cashId = PaymentMethod::idOf(PaymentMethod::CASH);
        $unpaidId = Status::idOf(Status::CTX_INVOICE_PAYMENT, Status::UNPAID);
        $partialId = Status::idOf(Status::CTX_INVOICE_PAYMENT, Status::PARTIAL);

        // Resolve SalesSeeder's pending cheques first — until a cheque is
        // approved its claim doesn't count toward paid_amount, so an invoice
        // paid only by a still-pending cheque looks unpaid. Settling those
        // before allocating fresh payments avoids stacking a second payment
        // on top of a cheque that then clears.
        $this->settlePendingCheques();
        $this->allocatedPayments($payments, $cashId, $unpaidId, $partialId);
        $this->standaloneCheques($payments);
        $this->settlePendingCheques();
    }

    private function allocatedPayments(PaymentService $payments, ?int $cashId, ?int $unpaidId, ?int $partialId): void
    {
        $customerIds = Invoice::whereIn('payment_status_id', array_filter([$unpaidId, $partialId]))
            ->whereNotNull('customer_id')
            ->distinct()
            ->pluck('customer_id');

        $processed = 0;
        foreach ($customerIds->shuffle() as $customerId) {
            if ($processed >= self::ALLOCATION_COUNT) {
                break;
            }

            // Skip invoices with a cheque still awaiting clearance — a
            // cashier wouldn't normally re-collect on one already covered by
            // a submitted (if unconfirmed) cheque.
            $pendingId = Status::idOf(Status::CTX_TRANSACTION, Status::PENDING);
            $invoices = Invoice::where('customer_id', $customerId)
                ->whereIn('payment_status_id', array_filter([$unpaidId, $partialId]))
                ->whereDoesntHave('claims.transaction', fn ($q) => $q->where('status_id', $pendingId))
                ->get()
                ->filter(fn (Invoice $i) => (float) $i->balance > 0.01);

            if ($invoices->isEmpty()) {
                continue;
            }

            $selected = $invoices->random(min(random_int(1, 3), $invoices->count()));
            $totalBalance = (float) $selected->sum('balance');

            // ~25% of the time, overpay slightly to exercise the overpayment
            // credit-note path.
            $amount = fake()->boolean(25)
                ? round($totalBalance + fake()->randomFloat(2, 100, 2000), 2)
                : round($totalBalance * fake()->randomFloat(2, 0.5, 1), 2);

            $allocations = [];
            $remaining = $amount;
            $items = $selected->values();
            foreach ($items as $i => $invoice) {
                $isLast = $i === $items->count() - 1;
                $cap = min((float) $invoice->balance, $remaining);
                $take = $isLast ? $cap : round($cap * fake()->randomFloat(2, 0.5, 1), 2);
                $take = min($take, $remaining);
                if ($take <= 0.01) {
                    continue;
                }
                $allocations[] = ['invoice_id' => $invoice->id, 'amount' => $take];
                $remaining -= $take;
            }

            if (empty($allocations)) {
                continue;
            }

            $branch = $selected->first()->branch_id;

            try {
                $transaction = $payments->createAllocatedTransaction(
                    data: [
                        'customer_id' => $customerId,
                        'branch_id' => $branch,
                        'payment_method_id' => $cashId,
                        'amount' => $amount,
                    ],
                    allocations: $allocations,
                );
                $this->backdate($transaction, $this->recentDate(5));
                $processed++;
            } catch (RuntimeException) {
                // Balance moved between selection and allocation (e.g. this
                // customer already got settled by another loop pass) — skip.
                continue;
            }
        }
    }

    private function standaloneCheques(PaymentService $payments): void
    {
        $rows = Invoice::whereNotNull('customer_id')->inRandomOrder()->limit(self::STANDALONE_CHEQUE_COUNT)->get(['branch_id', 'customer_id']);

        foreach ($rows as $row) {
            $transaction = $payments->createStandalone([
                'customer_id' => $row->customer_id,
                'branch_id' => $row->branch_id,
                'payment_method' => PaymentMethod::CHEQUE,
                'amount' => fake()->randomFloat(2, 1000, 20000),
                'cheque_number' => fake()->numerify('CHQ-######'),
                'cheque_bank' => fake()->randomElement(['Commercial Bank', 'Sampath Bank', 'HNB', "People's Bank", 'BOC']),
                'cheque_branch_code' => fake()->numerify('####'),
                'cheque_date' => now()->addDays(random_int(-5, 20))->toDateString(),
            ]);
            $this->backdate($transaction, $this->recentDate(4));
        }
    }

    /** Clear out a chunk of pending cheques: mostly approved, a few rejected. */
    private function settlePendingCheques(): void
    {
        $payments = app(PaymentService::class);
        $pendingId = Status::idOf(Status::CTX_TRANSACTION, Status::PENDING);

        $pending = Transaction::where('status_id', $pendingId)
            ->where('payment_method_id', PaymentMethod::idOf(PaymentMethod::CHEQUE))
            ->get();

        foreach ($pending as $index => $transaction) {
            try {
                if ($index % 6 === 0) {
                    $payments->rejectTransaction($transaction, 'Cheque returned by bank — insufficient funds.');
                } elseif ($index % 3 !== 0) {
                    $payments->approveTransaction($transaction);
                }
                // Remaining ~1/6 stay pending, awaiting clearance.
            } catch (RuntimeException) {
                continue;
            }
        }
    }
}
