<?php

namespace App\Domain\Billing;

use App\Enums\InstallmentStatus;
use App\Enums\InvoiceStatus;
use App\Enums\PaymentMethod;
use App\Models\Installment;
use App\Models\Invoice;
use App\Models\Payment;
use Illuminate\Support\Facades\DB;

/**
 * One entry point for the money ledger. Payments are append-only —
 * no edits/deletes — so the cash history is always trustworthy.
 *
 * Allocation: a payment aimed at an installment fills it first, then spills
 * forward across the invoice's remaining installments (oldest due first);
 * an invoice-level payment spills the same way. Invoice paid_amount, status,
 * and the patient's outstanding balance are kept in sync.
 */
class PaymentService
{
    public function __construct(
        protected InstallmentService $installments,
        protected InvoiceService $invoices,
    ) {
    }

    public function record(
        Invoice $invoice,
        int $amount,
        PaymentMethod $method,
        ?Installment $installment = null,
        ?string $reference = null,
        ?string $note = null,
        ?int $receivedBy = null,
    ): Payment {
        abort_unless($invoice->status->isOpen(), 422, __('Payments can only be recorded on issued invoices.'));
        abort_unless($amount > 0, 422, __('Amount must be positive.'));
        abort_if($amount > $invoice->remaining(), 422,
            __('Amount exceeds the remaining balance (:rem IQD).', ['rem' => number_format($invoice->remaining())]));
        if ($installment) {
            abort_unless($installment->invoice_id === $invoice->id, 404);
        }

        return DB::transaction(function () use ($invoice, $amount, $method, $installment, $reference, $note, $receivedBy) {
            $payment = Payment::create([
                'patient_id' => $invoice->patient_id,
                'invoice_id' => $invoice->id,
                'installment_id' => $installment?->id,
                'amount' => $amount,
                'method' => $method,
                'reference' => $reference,
                'note' => $note,
                'received_by' => $receivedBy,
                'paid_at' => now(),
            ]);

            // Spread across installments: target first, then oldest unpaid.
            $left = $amount;
            if ($installment) {
                $left -= $this->installments->applyToInstallment($installment, $left);
            }
            if ($left > 0 && $invoice->hasInstallments()) {
                $queue = $invoice->installments()
                    ->where('status', '!=', InstallmentStatus::Paid)
                    ->orderBy('due_date')->orderBy('seq')->get();
                foreach ($queue as $next) {
                    if ($left <= 0) {
                        break;
                    }
                    $left -= $this->installments->applyToInstallment($next, $left);
                }
            }

            // Sync invoice + patient totals.
            $paid = $invoice->paid_amount + $amount;
            $invoice->update([
                'paid_amount' => $paid,
                'status' => $paid >= $invoice->total
                    ? InvoiceStatus::Paid
                    : InvoiceStatus::PartiallyPaid,
            ]);

            $invoice->patient->increment('total_paid', $amount);
            $this->invoices->recalcPatientBalance($invoice->patient);

            return $payment;
        });
    }
}
