<?php

namespace App\Services\Accounting;

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

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

    public function post(ContraSettlement $contra): ContraSettlement
    {
        return DB::transaction(function () use ($contra): ContraSettlement {
            $contra = ContraSettlement::query()->lockForUpdate()->findOrFail($contra->id);

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

            $invoice = CustomerInvoice::query()
                ->lockForUpdate()
                ->findOrFail($contra->customer_invoice_id);
            $bill = VendorBill::query()
                ->lockForUpdate()
                ->findOrFail($contra->vendor_bill_id);

            if ((int) $invoice->company_id !== (int) $contra->company_id
                || (int) $bill->company_id !== (int) $contra->company_id
                || (int) $invoice->party_id !== (int) $contra->party_id
                || (int) $bill->party_id !== (int) $contra->party_id) {
                throw new DomainException(
                    'Contra documents must belong to the same company and dual-role party.',
                );
            }

            if (! in_array($invoice->status, [DocumentStatus::POSTED, DocumentStatus::PARTIALLY_PAID], true)
                || ! in_array($bill->status, [DocumentStatus::POSTED, DocumentStatus::PARTIALLY_PAID], true)) {
                throw new DomainException('Only posted open documents may be settled by contra.');
            }

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

            if (Decimal::compare($contra->amount, '0') <= 0
                || Decimal::compare($contra->amount, $invoiceBalance) > 0
                || Decimal::compare($contra->amount, $billBalance) > 0) {
                throw new DomainException(
                    'Contra amount exceeds an available receivable or payable balance.',
                );
            }

            $this->journals->post($contra, [
                'company_id' => $contra->company_id,
                'branch_id' => $contra->branch_id,
                'posting_date' => $contra->settlement_date,
                'document_date' => $contra->settlement_date,
                'reference_number' => $contra->contra_number,
                'source_module' => 'contra_settlements',
                'description' => 'Contra settlement '.$contra->contra_number,
            ], [
                [
                    'account_id' => $contra->payable_account_id,
                    'party_id' => $contra->party_id,
                    'party_ledger' => PartyLedger::VENDOR->value,
                    'debit' => $contra->amount,
                    'description' => 'Reduce accounts payable',
                ],
                [
                    'account_id' => $contra->receivable_account_id,
                    'party_id' => $contra->party_id,
                    'party_ledger' => PartyLedger::CUSTOMER->value,
                    'credit' => $contra->amount,
                    'description' => 'Reduce accounts receivable',
                ],
            ]);

            $invoice->forceFill([
                'allocated_total' => Decimal::add(
                    $invoice->allocated_total,
                    $contra->amount,
                ),
            ])->save();
            $bill->forceFill([
                'allocated_total' => Decimal::add(
                    $bill->allocated_total,
                    $contra->amount,
                ),
            ])->save();

            $this->settlementStatuses->refreshCustomerInvoice($invoice->refresh());
            $this->settlementStatuses->refreshVendorBill($bill->refresh());

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

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

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