<?php

namespace App\Services\Accounting;

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

final class InvoicePostingService
{
    public function __construct(
        private readonly AuthorizationGuard $authorization,
        private readonly DocumentApprovalGuard $guard,
        private readonly JournalPostingService $journals,
        private readonly LedgerBalanceService $balances,
        private readonly AuditLogger $audit,
    ) {}

    public function post(CustomerInvoice $invoice): CustomerInvoice
    {
        return DB::transaction(function () use ($invoice): CustomerInvoice {
            $invoice = CustomerInvoice::query()
                ->with(['items', 'party', 'transportJob.pods'])
                ->lockForUpdate()
                ->findOrFail($invoice->id);

            $this->authorization->ensureCan(
                'customer_invoices.post',
                (int) $invoice->company_id,
                (int) $invoice->branch_id,
            );
            $this->guard->ensureApproved($invoice);
            $this->validateTotals($invoice);
            $this->validateOperationalControls($invoice);

            $lines = [[
                'account_id' => $invoice->receivable_account_id,
                'party_id' => $invoice->party_id,
                'party_ledger' => PartyLedger::CUSTOMER->value,
                'debit' => $invoice->grand_total,
                'description' => 'Accounts receivable',
            ]];

            if ($invoice->items->isNotEmpty()) {
                foreach ($invoice->items->groupBy('income_account_id') as $accountId => $items) {
                    $lines[] = [
                        'account_id' => (int) $accountId,
                        'credit' => $this->sumAmounts($items, 'taxable_amount'),
                        'description' => 'Transport/service income',
                    ];
                }

                foreach ($invoice->items->where('vat_amount', '>', 0)->groupBy('output_vat_account_id') as $accountId => $items) {
                    if (! $accountId) {
                        throw new DomainException('An output VAT account is required for taxable invoice lines.');
                    }
                    $lines[] = [
                        'account_id' => (int) $accountId,
                        'credit' => $this->sumAmounts($items, 'vat_amount'),
                        'description' => 'Output VAT',
                    ];
                }
            } else {
                $lines[] = [
                    'account_id' => $invoice->income_account_id,
                    'credit' => $invoice->taxable_total,
                    'description' => 'Transport/service income',
                ];

                if (Decimal::compare($invoice->vat_total, '0') > 0) {
                    if (! $invoice->output_vat_account_id) {
                        throw new DomainException('An output VAT account is required for a taxable invoice.');
                    }
                    $lines[] = [
                        'account_id' => $invoice->output_vat_account_id,
                        'credit' => $invoice->vat_total,
                        'description' => 'Output VAT',
                    ];
                }
            }

            $this->journals->post($invoice, [
                'company_id' => $invoice->company_id,
                'branch_id' => $invoice->branch_id,
                'posting_date' => $invoice->issue_date,
                'document_date' => $invoice->issue_date,
                'reference_number' => $invoice->invoice_number,
                'source_module' => 'customer_invoices',
                'description' => 'Customer invoice '.$invoice->invoice_number,
                'currency_code' => $invoice->currency_code,
                'exchange_rate' => $invoice->exchange_rate,
            ], $lines);

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

            $this->audit->log(
                'posted',
                'customer_invoices',
                $invoice,
                [],
                ['status' => 'posted'],
            );

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

    private function sumAmounts(iterable $items, string $field): string
    {
        $total = Decimal::scale('0');

        foreach ($items as $item) {
            $total = Decimal::add($total, (string) $item->{$field});
        }

        return $total;
    }

    private function validateOperationalControls(CustomerInvoice $invoice): void
    {
        if ($invoice->transportJob && config('operations.require_pod_for_invoice') && $invoice->transportJob->pods->isEmpty()) {
            $user = auth()->user();
            if (! $user || ! method_exists($user, 'hasPermission') || ! $user->hasPermission('transport_jobs.invoice_without_pod')) {
                throw new DomainException('Proof of delivery is required before posting this job invoice.');
            }
        }

        $party = $invoice->party;
        if ($party && Decimal::compare((string) $party->credit_limit, '0') > 0) {
            $projected = Decimal::add($this->balances->customerReceivable((int) $party->id), (string) $invoice->grand_total);
            if (Decimal::compare($projected, (string) $party->credit_limit) > 0) {
                $user = auth()->user();
                if (! $user || ! method_exists($user, 'hasPermission') || ! $user->hasPermission('customer_invoices.override_credit_limit')) {
                    throw new DomainException('Posting this invoice would exceed the customer credit limit.');
                }
            }
        }
    }

    private function validateTotals(CustomerInvoice $invoice): void
    {
        if (! Decimal::equals(
            Decimal::add($invoice->taxable_total, $invoice->vat_total),
            $invoice->grand_total,
        )) {
            throw new DomainException('Invoice totals do not reconcile.');
        }

        if ($invoice->items->isEmpty()) {
            return;
        }

        $taxable = $this->sumAmounts($invoice->items, 'taxable_amount');
        $vat = $this->sumAmounts($invoice->items, 'vat_amount');
        $gross = $this->sumAmounts($invoice->items, 'line_total');

        if (! Decimal::equals($taxable, $invoice->taxable_total)
            || ! Decimal::equals($vat, $invoice->vat_total)
            || ! Decimal::equals($gross, $invoice->grand_total)) {
            throw new DomainException('Invoice item totals do not match the document totals.');
        }
    }
}
