<?php

namespace App\Services\Accounting;

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

final class PaymentPostingService
{
    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 vendor_bill_id => amount */
    public function post(Payment $payment, array $allocations = []): Payment
    {
        return DB::transaction(function () use ($payment, $allocations): Payment {
            $payment = Payment::query()->lockForUpdate()->findOrFail($payment->id);

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

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

            $allocated = Decimal::scale('0');
            $lines = [];

            if ($payment->payment_type !== 'vendor_payment') {
                if ($allocations !== [] || $payment->allocations()->exists()) {
                    throw new DomainException('Only vendor payments may be allocated to vendor bills.');
                }
                $lines[] = [
                    'account_id' => $payment->payable_account_id,
                    'party_id' => $payment->party_id,
                    'party_ledger' => $payment->payment_type === 'customer_refund' ? PartyLedger::CUSTOMER->value : null,
                    'debit' => $payment->amount,
                    'description' => $payment->payment_type === 'customer_refund' ? 'Customer refund' : 'Other payment',
                ];
                $lines[] = ['account_id' => $payment->cash_bank_account_id, 'credit' => $payment->amount, 'description' => 'Cash/bank payment'];
                $this->journals->post($payment, [
                    'company_id' => $payment->company_id,
                    'branch_id' => $payment->branch_id,
                    'posting_date' => $payment->payment_date,
                    'document_date' => $payment->payment_date,
                    'reference_number' => $payment->payment_number,
                    'source_module' => 'payments',
                    'description' => 'Payment '.$payment->payment_number,
                    'currency_code' => $payment->currency_code,
                    'exchange_rate' => $payment->exchange_rate,
                ], $lines);
                $payment->forceFill(['allocated_total' => '0', 'status' => DocumentStatus::POSTED, 'posted_at' => now()])->save();
                $this->audit->log('posted', 'payments', $payment, [], ['status' => 'posted', 'allocated_total' => '0']);
                return $payment->refresh();
            }

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

            foreach ($allocations as $billId => $amount) {
                $amount = Decimal::scale((string) $amount);
                $bill = VendorBill::query()->lockForUpdate()->findOrFail($billId);

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

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

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

                $payment->allocations()->updateOrCreate(
                    ['vendor_bill_id' => $bill->id],
                    ['amount' => $amount],
                );
                $bill->forceFill([
                    'paid_total' => Decimal::add($bill->paid_total, $amount),
                ])->save();
                $this->settlementStatuses->refreshVendorBill($bill->refresh());

                $allocated = Decimal::add($allocated, $amount);
                $lines[] = [
                    'account_id' => $payment->payable_account_id,
                    'party_id' => $payment->party_id,
                    'party_ledger' => PartyLedger::VENDOR->value,
                    'debit' => $amount,
                    'description' => 'Vendor bill allocation '.$bill->bill_number,
                ];
            }

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

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

            $lines[] = [
                'account_id' => $payment->cash_bank_account_id,
                'credit' => $payment->amount,
                'description' => 'Cash/bank payment',
            ];

            $this->journals->post($payment, [
                'company_id' => $payment->company_id,
                'branch_id' => $payment->branch_id,
                'posting_date' => $payment->payment_date,
                'document_date' => $payment->payment_date,
                'reference_number' => $payment->payment_number,
                'source_module' => 'payments',
                'description' => 'Payment '.$payment->payment_number,
                'currency_code' => $payment->currency_code,
                'exchange_rate' => $payment->exchange_rate,
            ], $lines);

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

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

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