<?php

namespace App\Services\Accounting;

use App\Enums\DocumentStatus;
use App\Enums\PartyLedger;
use App\Models\CustomerInvoice;
use App\Models\Receipt;
use App\Services\Audit\AuditLogger;
use App\Support\Decimal;
use DomainException;
use Illuminate\Support\Facades\DB;

final class ReceiptPostingService
{
    public function __construct(
        private readonly AuthorizationGuard $authorization,
        private readonly DocumentApprovalGuard $guard,
        private readonly JournalPostingService $journals,
        private readonly SettlementStatusService $settlementStatuses,
        private readonly AuditLogger $audit,
    ) {}

    /** @param array<int, string> $allocations invoice_id => amount */
    public function post(Receipt $receipt, array $allocations = []): Receipt
    {
        return DB::transaction(function () use ($receipt, $allocations): Receipt {
            $receipt = Receipt::query()->lockForUpdate()->findOrFail($receipt->id);

            $this->authorization->ensureCan(
                'receipts.post',
                (int) $receipt->company_id,
                (int) $receipt->branch_id,
            );
            $this->guard->ensureApproved($receipt);

            if (Decimal::compare($receipt->amount, '0') <= 0) {
                throw new DomainException('Receipt amount must be greater than zero.');
            }

            $allocated = Decimal::scale('0');
            $lines = [[
                'account_id' => $receipt->cash_bank_account_id,
                'debit' => $receipt->amount,
                'description' => 'Cash/bank receipt',
            ]];

            if ($receipt->receipt_type !== 'customer_receipt') {
                if ($allocations !== [] || $receipt->allocations()->exists()) {
                    throw new DomainException('Only customer receipts may be allocated to customer invoices.');
                }
                $lines[] = [
                    'account_id' => $receipt->receivable_account_id,
                    'party_id' => $receipt->party_id,
                    'party_ledger' => $receipt->receipt_type === 'vendor_refund' ? PartyLedger::VENDOR->value : null,
                    'credit' => $receipt->amount,
                    'description' => $receipt->receipt_type === 'vendor_refund' ? 'Vendor refund / recovery' : 'Other receipt',
                ];
                $this->journals->post($receipt, [
                    'company_id' => $receipt->company_id,
                    'branch_id' => $receipt->branch_id,
                    'posting_date' => $receipt->receipt_date,
                    'document_date' => $receipt->receipt_date,
                    'reference_number' => $receipt->receipt_number,
                    'source_module' => 'receipts',
                    'description' => 'Receipt '.$receipt->receipt_number,
                    'currency_code' => $receipt->currency_code,
                    'exchange_rate' => $receipt->exchange_rate,
                ], $lines);
                $receipt->forceFill(['allocated_total' => '0', 'status' => DocumentStatus::POSTED, 'posted_at' => now()])->save();
                $this->audit->log('posted', 'receipts', $receipt, [], ['status' => 'posted', 'allocated_total' => '0']);
                return $receipt->refresh();
            }

            if ($allocations === []) {
                $allocations = $receipt->allocations()->pluck('amount', 'customer_invoice_id')->all();
            }

            foreach ($allocations as $invoiceId => $amount) {
                $amount = Decimal::scale((string) $amount);
                $invoice = CustomerInvoice::query()->lockForUpdate()->findOrFail($invoiceId);

                if ((int) $invoice->company_id !== (int) $receipt->company_id
                    || (int) $invoice->party_id !== (int) $receipt->party_id
                    || ! in_array($invoice->status, [DocumentStatus::POSTED, DocumentStatus::PARTIALLY_PAID], true)) {
                    throw new DomainException('Invalid invoice allocation.');
                }

                $available = Decimal::subtract(
                    $invoice->grand_total,
                    Decimal::add($invoice->paid_total, $invoice->allocated_total),
                );

                if (Decimal::compare($amount, '0') <= 0
                    || Decimal::compare($amount, $available) > 0) {
                    throw new DomainException('Receipt allocation exceeds invoice balance.');
                }

                $receipt->allocations()->updateOrCreate(
                    ['customer_invoice_id' => $invoice->id],
                    ['amount' => $amount],
                );
                $invoice->forceFill([
                    'paid_total' => Decimal::add($invoice->paid_total, $amount),
                ])->save();
                $this->settlementStatuses->refreshCustomerInvoice($invoice->refresh());

                $allocated = Decimal::add($allocated, $amount);
                $lines[] = [
                    'account_id' => $receipt->receivable_account_id,
                    'party_id' => $receipt->party_id,
                    'party_ledger' => PartyLedger::CUSTOMER->value,
                    'credit' => $amount,
                    'description' => 'Invoice allocation '.$invoice->invoice_number,
                ];
            }

            if (Decimal::compare($allocated, $receipt->amount) > 0) {
                throw new DomainException('Total allocation exceeds receipt amount.');
            }

            $unallocated = Decimal::subtract($receipt->amount, $allocated);
            if (Decimal::compare($unallocated, '0') > 0) {
                if (! $receipt->customer_advance_account_id) {
                    throw new DomainException(
                        'Customer advance account is required for an unallocated receipt.',
                    );
                }
                $lines[] = [
                    'account_id' => $receipt->customer_advance_account_id,
                    'party_id' => $receipt->party_id,
                    'party_ledger' => PartyLedger::CUSTOMER->value,
                    'credit' => $unallocated,
                    'description' => 'Customer advance/unallocated receipt',
                ];
            }

            $this->journals->post($receipt, [
                'company_id' => $receipt->company_id,
                'branch_id' => $receipt->branch_id,
                'posting_date' => $receipt->receipt_date,
                'document_date' => $receipt->receipt_date,
                'reference_number' => $receipt->receipt_number,
                'source_module' => 'receipts',
                'description' => 'Receipt '.$receipt->receipt_number,
                'currency_code' => $receipt->currency_code,
                'exchange_rate' => $receipt->exchange_rate,
            ], $lines);

            $receipt->forceFill([
                'allocated_total' => $allocated,
                'status' => DocumentStatus::POSTED,
                'posted_at' => now(),
            ])->save();

            $this->audit->log(
                'posted',
                'receipts',
                $receipt,
                [],
                ['status' => 'posted', 'allocated_total' => $allocated],
            );

            return $receipt->refresh();
        }, 3);
    }
}
