<?php

namespace App\Domain\Financial\Services;

use App\Domain\Financial\Enums\PaymentMethod;
use App\Domain\Financial\Enums\PaymentStatus;
use App\Domain\Financial\Enums\TransactionType;
use App\Domain\Financial\Models\CashSession;
use App\Domain\Financial\Models\Payment;
use App\Domain\Financial\Models\Transaction;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Training\Models\Enrollment;
use App\Models\User;

class RefundService
{
    public function __construct(
        private readonly InvoiceService $invoiceService,
        private readonly CashSessionService $cashSessionService,
        private readonly LedgerAccountResolver $accounts,
        private readonly RevenueRouter $router,
    ) {}

    /**
     * Preview the impact of refunding the given payment UUIDs.
     * Returns plain arrays (safe for Livewire state storage).
     */
    public function preview(array $paymentUuids): array
    {
        $results = [];

        foreach ($paymentUuids as $uuid) {
            $payment = Payment::where('uuid', $uuid)->with('invoice')->first();
            if (! $payment) {
                continue;
            }

            $enrollmentName = null;
            if ($payment->invoice_id) {
                $enrollment = Enrollment::where('invoice_id', $payment->invoice_id)
                    ->whereIn('status', ['active', 'pending'])
                    ->with('program')
                    ->first();
                if ($enrollment) {
                    $enrollmentName = $enrollment->program?->name_ar
                        ?? $enrollment->program?->name
                        ?? 'برنامج';
                }
            }

            $results[] = [
                'uuid'             => $payment->uuid,
                'reference'        => $payment->reference,
                'amount'           => $payment->amount,
                'method_label'     => $payment->method->label(),
                'enrollment_name'  => $enrollmentName,
            ];
        }

        return $results;
    }

    /**
     * Execute the financial side of a refund for each payment UUID.
     * MUST be called inside a DB::transaction() by the caller.
     * Returns [['refund' => Payment, 'enrollment' => Enrollment|null, 'original' => Payment], ...]
     * so the caller can cancel enrollments in the same transaction.
     */
    /**
     * @param  array<string,int>  $amounts  Optional partial amount in piasters
     *                                      keyed by payment uuid. A uuid absent
     *                                      from the map is refunded in full.
     */
    public function processRefunds(array $paymentUuids, string $reason, User $actor, array $amounts = []): array
    {
        if (count($paymentUuids) > 2) {
            throw new DomainException('يمكن استرداد دفعتين كحد أقصى في عملية واحدة');
        }

        if (empty($paymentUuids)) {
            throw new DomainException('لم يتم تحديد أي دفعات للاسترداد');
        }

        $results = [];

        foreach ($paymentUuids as $uuid) {
            $payment = Payment::where('uuid', $uuid)
                ->lockForUpdate()
                ->first();

            if (! $payment) {
                throw new DomainException("الدفعة غير موجودة");
            }

            if ($payment->status !== PaymentStatus::Confirmed) {
                throw new DomainException("الدفعة {$payment->reference} غير مؤكدة ولا يمكن استردادها");
            }

            if ($payment->direction !== 'inbound') {
                throw new DomainException("لا يمكن استرداد دفعة صادرة");
            }

            // A partial refund is the only way to correct an over-approved
            // amount: the ledger is immutable, so the correction has to be a
            // new reversing entry for the difference, not an edit.
            $refundAmount = (int) ($amounts[$uuid] ?? $payment->amount);
            $alreadyRefunded = (int) Payment::where('academy_id', $payment->academy_id)
                ->where('direction', 'outbound')
                ->where('status', PaymentStatus::Confirmed)
                ->where('metadata->refund_of_payment_id', $payment->id)
                ->sum('amount');

            if ($refundAmount <= 0) {
                throw new DomainException('مبلغ الاسترداد يجب أن يكون أكبر من صفر');
            }

            if ($refundAmount + $alreadyRefunded > $payment->amount) {
                throw new DomainException(
                    'مبلغ الاسترداد أكبر من المتبقي من الدفعة (' . format_money($payment->amount - $alreadyRefunded) . ')'
                );
            }

            // Find linked enrollment BEFORE mutations (for caller to cancel)
            $enrollment = null;
            if ($payment->invoice_id) {
                $enrollment = Enrollment::where('invoice_id', $payment->invoice_id)
                    ->whereIn('status', ['active', 'pending'])
                    ->with(['program', 'group'])
                    ->first();
            }

            // Create outbound refund payment record
            $refundPayment = Payment::create([
                'academy_id'   => $payment->academy_id,
                'branch_id'    => $payment->branch_id,
                'invoice_id'   => $payment->invoice_id,
                'reference'    => 'REF-' . now()->format('YmdHis') . '-' . $payment->id,
                'direction'    => 'outbound',
                'method'       => $payment->method,
                'status'       => PaymentStatus::Confirmed,
                'payer_type'   => $payment->payer_type,
                'payer_id'     => $payment->payer_id,
                'amount'       => $refundAmount,
                'currency'     => $payment->currency ?? 'EGP',
                'payment_date' => now()->toDateString(),
                'confirmed_at' => now(),
                'notes'        => "استرداد للدفعة {$payment->reference}" . ($reason ? " — {$reason}" : ''),
                'created_by'   => $actor->id,
                'metadata'     => ['refund_of_payment_id' => $payment->id],
            ]);

            // A payment is only "refunded" once nothing of it is left.
            if ($refundAmount + $alreadyRefunded >= $payment->amount) {
                $payment->update(['status' => PaymentStatus::Refunded]);
            }

            // Reverse the ledger entries the original payment produced: debit
            // back the revenue accounts it credited, credit the asset account
            // the money is leaving. The old code hardcoded ids 2 and 1 with a
            // comment claiming Accounts Receivable — which is account 3.
            $asset = $this->accounts->assetAccountForMethod(
                $refundPayment->academy_id,
                $refundPayment->method?->value ?? (string) $refundPayment->method
            );

            // Unwind the accounts the money actually went into — including the
            // tax liability, since refunding VAT to revenue would leave the
            // academy owing tax on money it gave back.
            //
            // Read off the original payment's own ledger rows rather than by
            // re-running today's routing rules: a rule the accountant edited
            // last week would otherwise reverse a collection into accounts it
            // never touched, and no report would ever reconcile again.
            foreach ($this->reversalsFor($payment, $refundPayment, $refundAmount) as $reversal) {
                Transaction::create([
                    'academy_id'        => $refundPayment->academy_id,
                    'branch_id'         => $refundPayment->branch_id,
                    'debit_account_id'  => $reversal['account_id'],
                    'credit_account_id' => $asset->id,
                    'payment_id'        => $refundPayment->id,
                    'invoice_id'        => $refundPayment->invoice_id,
                    'amount'            => $reversal['amount'],
                    'currency'          => $refundPayment->currency ?? 'EGP',
                    'type'              => TransactionType::Refund,
                    'description'       => "استرداد: {$payment->reference}",
                    'transaction_date'  => now()->toDateString(),
                    'metadata'          => $reversal['metadata'],
                    'created_by'        => $actor->id,
                ]);
            }

            // Deduct from the session that TOOK the money where it is still
            // open, not from whichever drawer the person issuing the refund
            // happens to have open — that debited the wrong cashier.
            if ($payment->method === PaymentMethod::Cash) {
                $cashSession = null;

                if ($payment->cash_session_id) {
                    $cashSession = CashSession::where('id', $payment->cash_session_id)
                        ->where('status', 'open')
                        ->first();
                }

                // The fallback drawer must at least be in the branch whose
                // money is going back out. Without the branch condition this
                // debits whatever till the refunding user has open — which,
                // for someone who works across branches, can be a completely
                // different branch's cash count.
                $cashSession ??= $this->cashSessionService->getOpenSession(
                    $actor,
                    $payment->branch_id ? (int) $payment->branch_id : null
                );

                if ($cashSession) {
                    $this->cashSessionService->recordCashOut($cashSession, $refundAmount);
                }
            }

            // Reverse the invoice balance
            if ($payment->invoice_id) {
                $this->invoiceService->updatePaidAmount(
                    $payment->invoice()->withoutGlobalScopes()->lockForUpdate()->first(),
                    -$refundAmount,
                    $actor
                );
            }

            $results[] = [
                'refund'      => $refundPayment,
                'enrollment'  => $enrollment,
                'original'    => $payment,
            ];
        }

        return $results;
    }

    /**
     * The accounts a refund debits, and how much of each.
     *
     * A refund is the mirror of a specific collection, so it is read off that
     * collection's own ledger rows: whatever was credited then is debited now,
     * in the same proportions. A partial refund takes a proportional slice of
     * each, floored, with the remainder on the last row so the reversal totals
     * exactly the money going back out.
     *
     * The fallback matters. Payments taken before the account resolver was
     * fixed credited cash and bank and never a revenue account, and there are
     * hundreds of them; those have nothing to mirror, so the current rules are
     * the best available answer and the row says so.
     *
     * @return list<array{account_id: int, amount: int, metadata: array}>
     */
    private function reversalsFor(Payment $payment, Payment $refundPayment, int $refundAmount): array
    {
        $original = Transaction::withoutGlobalScopes()
            ->where('payment_id', $payment->id)
            ->where('type', TransactionType::PaymentReceived)
            ->orderBy('id')
            ->get(['id', 'credit_account_id', 'amount', 'metadata']);

        $base = (int) $original->sum('amount');

        if ($base <= 0) {
            $allocation = $this->router->allocatePayment(
                (int) $refundPayment->academy_id,
                $refundPayment->invoice,
                $refundAmount
            );

            return array_map(fn ($posting) => [
                'account_id' => $posting->account->id,
                'amount' => $posting->amount,
                'metadata' => $posting->metadata() + ['reversal_basis' => 'current_rules'],
            ], $allocation->postings);
        }

        $out = [];
        $allocated = 0;
        $last = $original->count() - 1;

        foreach ($original as $i => $row) {
            $share = $i === $last
                ? $refundAmount - $allocated
                : intdiv($refundAmount * (int) $row->amount, $base);

            $allocated += $share;

            if ($share <= 0) {
                continue;
            }

            $out[] = [
                'account_id' => (int) $row->credit_account_id,
                'amount' => $share,
                'metadata' => ((array) $row->metadata) + [
                    'reversal_basis' => 'original_entry',
                    'reverses_transaction_id' => (int) $row->id,
                ],
            ];
        }

        return $out;
    }
}
