<?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,
    ) {}

    /**
     * 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
            );

            $splits = $this->accounts->splitRevenue(
                $refundPayment->academy_id,
                $refundPayment->invoice,
                $refundAmount
            );

            foreach ($splits as $split) {
                Transaction::create([
                    'academy_id'        => $refundPayment->academy_id,
                    'branch_id'         => $refundPayment->branch_id,
                    'debit_account_id'  => $split['account']->id,
                    'credit_account_id' => $asset->id,
                    'payment_id'        => $refundPayment->id,
                    'invoice_id'        => $refundPayment->invoice_id,
                    'amount'            => $split['amount'],
                    'currency'          => $refundPayment->currency ?? 'EGP',
                    'type'              => TransactionType::Refund,
                    'description'       => "استرداد: {$payment->reference}",
                    'transaction_date'  => now()->toDateString(),
                    '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;
    }
}
