<?php

namespace Modules\Inventory\Services;

use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\Accounts\Models\AccountVoucher;
use Modules\Accounts\Traits\HasFiscalContext;
use Modules\Approval\Services\ApprovalSubmissionService;
use Modules\Approval\Support\ApprovalRelations;
use Modules\Billing\Enums\BillPayStatusEnum;
use Modules\Core\Enums\PaymentStatusEnum;
use Modules\Core\Exceptions\ApiValidationException;
use Modules\Core\Exceptions\BusinessException;
use Modules\Core\Traits\HasActionContext;
use Modules\Inventory\DTO\PurchasePaymentAccountFilter;
use Modules\Inventory\Enums\PurchaseStatusEnum;
use Modules\Inventory\Models\PurchaseInvoice;
use Modules\Inventory\Models\PurchasePayment;
use Modules\Inventory\Repositories\PurchasePaymentRepository;
use Modules\Inventory\Repositories\PurchaseRepository;

class PurchasePaymentService
{
    use HasFiscalContext,
        HasActionContext;

    public function __construct(
        private PurchasePaymentRepository $paymentRepo,
        private PurchaseRepository $purchaseRepo,
        private PurchasePaymentPostingService $postingService,
        protected ApprovalSubmissionService $approvalSubmissionService,
    ) {}

    public function getAccountsForPayment(PurchasePaymentAccountFilter $filters): array
    {
        return $this->paymentRepo->searchPaymentAccounts($filters)
            ->map(fn($account) => [
                'id' => $account->id,
                'text' => "[{$account->account_code}] {$account->account_name}"
            ])
            ->toArray();
    }

    public function list(int $perPage = 20)
    {
        return $this->paymentRepo->paginate($perPage);
    }

    public function getByInvoice(PurchaseInvoice $purchase)
    {
        return $this->paymentRepo->getByInvoice($purchase);
    }

    public function paymentSummary(PurchaseInvoice $invoice): PurchaseInvoice
    {
        $invoice = $this->purchaseRepo->findById(
            $invoice->id,
            columns: [
                'id', 'supplier_id', 'invoice_no', 'grand_total', 'paid_amount', 'due_amount', 'status', 'is_posted', 'payment_status'
            ],
            relations: [
                'supplier:id,name'
            ]
        );

        $this->validateInvoiceForPayment($invoice);

        $reservedAmount = $this->paymentRepo->getReservedAmountByInvoice(
            $invoice->id
        );

        $availableAmount = $this->calculateAvailableAmount($invoice);

        return $invoice->setAttribute(
            'payment_summary',
            [
                'invoice_id' => $invoice->id,
                'grand_total' => (float) $invoice->grand_total,
                'paid_amount' => (float) $invoice->paid_amount,
                'due_amount' => (float) $invoice->due_amount,
                'reserved_amount' => (float) $reservedAmount,
                'available_amount' => (float) $availableAmount,
                'allow_payment' => $availableAmount > 0,
            ]
        );
    }

    public function store(PurchaseInvoice $invoice, array $data)
    {
        return DB::transaction(function () use ($invoice, $data) {
            $this->validatePayment(
                $invoice,
                $data['amount']
            );

            $data = $this->attachFiscalContext(
                $data,
                'payment_date'
            );

            return $this->paymentRepo->create([
                ...$data,
                'purchase_invoice_id' => $invoice->id,
                'status' => PaymentStatusEnum::DRAFT->value,
                'is_posted' => false
            ]);
        });
    }

    private function validatePayment(PurchaseInvoice $invoice, float $amount, ?int $excludePaymentId = null): void
    {
        $this->validateInvoiceForPayment($invoice);

        if (bccomp((string) $amount, '0.00', 2) <= 0) {
            ApiValidationException::errors([
                'amount' => ['Payment amount must be greater than zero.']
            ]);
        }

        $availableAmount = $this->calculateAvailableAmount($invoice, $excludePaymentId);

        if (bccomp($availableAmount, '0.00', 2) <= 0) {
            BusinessException::message('No payable amount is currently available for this invoice.');
        }

        if (bccomp((string) $amount, $availableAmount, 2) === 1) {
            ApiValidationException::errors([
                'amount' => ["Payment amount cannot exceed the available payable amount ({$availableAmount})."]
            ]);
        }
    }

    private function validateInvoiceForPayment(PurchaseInvoice $invoice): void
    {
        if ($invoice->status !== PurchaseStatusEnum::POSTED->value || !$invoice->is_posted) {
            BusinessException::message('Only posted purchase invoices can receive payments.');
        }

        if ($invoice->payment_status === BillPayStatusEnum::PAID->value) {
            BusinessException::message('This invoice has already been fully paid.');
        }

        if (bccomp((string) $invoice->due_amount, '0.00', 2) <= 0) {
            BusinessException::message('No outstanding balance remains for this purchase invoice.');
        }
    }

    private function calculateAvailableAmount(PurchaseInvoice $invoice, ?int $excludePaymentId = null): string
    {
        $reservedAmount = $this->paymentRepo->getReservedAmountByInvoice(
            $invoice->id,
            excludePaymentId: $excludePaymentId
        );

        $availableAmount = bcsub((string) $invoice->due_amount, (string) $reservedAmount, 2);

        return bccomp($availableAmount, '0.00', 2) === -1 ? '0.00' : $availableAmount;
    }

    public function show(PurchasePayment $payment): PurchasePayment
    {
        return $payment->load(array_merge([
            'purchaseInvoice:id,invoice_no,grand_total,paid_amount,due_amount,payment_status',
            'account:id,account_code,account_name',
            'voucher:id,voucher_no',
        ], ApprovalRelations::approvalRequest()));
    }

    public function update(PurchasePayment $payment, array $data): PurchasePayment
    {
        return DB::transaction(function () use ($payment, $data) {
                $payment = $this->paymentRepo->findForUpdate($payment->id);

                $this->ensureDraft($payment);

                $invoice = $this->lockInvoiceForUpdate($payment);

                $data['amount'] = (float) $data['amount'];

                $this->validatePayment(
                    invoice: $invoice,
                    amount: $data['amount'],
                    excludePaymentId: $payment->id
                );

                $data = $this->attachFiscalContext(
                    $data,
                    'payment_date'
                );

                return $this->paymentRepo->update(
                    payment: $payment,
                    data: $data
                );
            }
        );
    }

    private function lockInvoiceForUpdate(PurchasePayment $payment): PurchaseInvoice
    {
        return $payment->purchaseInvoice()
            ->lockForUpdate()
            ->with('supplier:id,name,account_id')
            ->firstOrFail([
                'id', 'supplier_id', 'invoice_no', 'grand_total', 'paid_amount', 'due_amount', 'payment_status', 'is_posted', 'status'
            ]);
    }

    private function ensureDraft(PurchasePayment $payment, string $action = 'modified'): void
    {
        if ($payment->status !== PaymentStatusEnum::DRAFT->value || $payment->is_posted) {
            BusinessException::message("Only draft payments can be {$action}.");
        }
    }

    public function submit(PurchasePayment $payment): PurchasePayment
    {
        return DB::transaction(function () use ($payment) {
            $payment = $this->paymentRepo->findForUpdate(
                $payment->id,
                relations: [
                    'purchaseInvoice:id,supplier_id,invoice_no,grand_total,paid_amount,due_amount,payment_status,is_posted,status'
                ]
            );

            $this->ensureDraft($payment, 'submitted');

            $this->validatePayment(
                invoice: $payment->purchaseInvoice,
                amount: (float) $payment->amount,
                excludePaymentId: $payment->id
            );

            $this->approvalSubmissionService->submit($payment);

            return $this->paymentRepo->update(
                payment: $payment,
                data: [
                    'status' => PurchaseStatusEnum::SUBMITTED->value,
                    'submitted_at' => now(),
                ]
            );
        });
    }

    public function post(PurchasePayment $payment): PurchasePayment
    {
        return DB::transaction(function () use ($payment) {
            $payment = $this->paymentRepo->findForUpdate($payment->id);
            $invoice = $this->lockInvoiceForUpdate($payment);

            $this->validatePosting(
                invoice: $invoice,
                payment: $payment
            );

            $voucher = $this->postingService->create(
                invoice: $invoice,
                payment: $payment
            );

            $paymentPayload = [
                'voucher_id' => $voucher?->id,
                'status'     => PaymentStatusEnum::POSTED->value,
                'is_posted'  => true,
                ...$this->attach('posted_by', 'posted_at'),
            ];

            $payment = $this->paymentRepo->update(
                payment: $payment,
                data: $paymentPayload
            );

            $this->recalculateInvoiceBalance($invoice);

            return $payment->fresh();
        });
    }

    private function recalculateInvoiceBalance(PurchaseInvoice $invoice): void
    {
        $paidAmount = $invoice->payments()
            ->where('status', PaymentStatusEnum::POSTED->value)
            ->where('is_posted', true)
            ->sum('amount');

        $dueAmount = max(0, (float) bcsub((string) $invoice->grand_total, (string) $paidAmount, 2));

        $status = match (true) {
            bccomp((string) $paidAmount, '0.00', 2 ) <= 0 => BillPayStatusEnum::UNPAID->value,
            bccomp((string) $dueAmount, '0.00', 2 ) <= 0 => BillPayStatusEnum::PAID->value,
            default => BillPayStatusEnum::PARTIALLY_PAID->value,
        };

        $this->purchaseRepo->updateInvoice(
            invoice: $invoice,
            data : [
                'paid_amount' => $paidAmount,
                'due_amount' => $dueAmount,
                'payment_status' => $status,
            ]
        );
    }

    private function validatePosting(PurchaseInvoice $invoice, PurchasePayment $payment): void
    {
        if (!$payment->isEligibleForPosting()) {
            BusinessException::message('Payment status is invalid or already processed.');
        }

        if (bccomp((string) $payment->amount, '0.00', 2) <= 0) {
            BusinessException::message('Invalid payment amount.');
        }

        $this->validateInvoiceForPayment($invoice);

        $supplierAccountId = $invoice->supplier?->account_id;

        if (!$supplierAccountId) {
            BusinessException::message('Supplier payable account not configured.');
        }

        $availableAmount = $this->calculateAvailableAmount($invoice, excludePaymentId: $payment->id);

        if (bccomp((string) $payment->amount, $availableAmount, 2) === 1) {
            BusinessException::message("Payment exceeds available balance ({$availableAmount}).");
        }
    }

    public function reverse(PurchasePayment $payment, array $data): PurchasePayment
    {
        return DB::transaction(function () use ($payment, $data) {
            $payment = $this->paymentRepo->findForUpdate(
                $payment->id,
                relations: [
                    'voucher',
                    'voucher.lines',
                    'account:id,account_name,status'
                ]
            );

            $invoice = $this->lockInvoiceForUpdate($payment);

            $this->ensureReversible(
                voucher: $payment?->voucher,
                payment: $payment
            );

            $this->validateReversalDate(
                $payment,
                $data['reversal_date']
            );

            $data = $this->attachFiscalContext(
                $data,
                'reversal_date'
            );

            $reversalVoucher = $this->postingService->createReversal(
                voucher: $payment?->voucher,
                reversalData: $data
            );

            $reversePayload = [
                'reversal_voucher_id' => $reversalVoucher?->id,
                'reversal_date'   => $data['reversal_date'],
                'reversal_reason' => $data['reversal_reason'] ?? null,
                'is_posted' => false,
                'status'    => PaymentStatusEnum::REVERSED->value,
                ...$this->attach('reversed_by', 'reversed_at'),
            ];

            $payment = $this->paymentRepo->update(
                payment: $payment,
                data: $reversePayload
            );

            $this->recalculateInvoiceBalance($invoice);

            return $payment->fresh();
        });
    }

    private function ensureReversible(AccountVoucher $voucher, PurchasePayment $payment): void
    {
        if ($payment->status !== PaymentStatusEnum::POSTED->value) {
            BusinessException::message('Only posted payments can be reversed.');
        }

        if ($payment->status === PaymentStatusEnum::REVERSED->value) {
            BusinessException::message('Payment already reversed.');
        }

        if (!$payment->voucher_id) {
            BusinessException::message('Payment voucher not found.');
        }

        if (!$voucher) {
            BusinessException::message('Associated voucher does not exist.');
        }

        if ($voucher->is_reversal) {
            BusinessException::message('Reversal voucher cannot be reversed again.');
        }

        if (!$voucher->is_posted) {
            BusinessException::message(
                'Only posted vouchers can be reversed.'
            );
        }
    }

    private function validateReversalDate(PurchasePayment $payment, string $reversalDate): void
    {
        if (Carbon::parse($reversalDate)->lt(Carbon::parse($payment->payment_date))) {
            ApiValidationException::errors([
                'reversal_date' => ['Reversal date cannot be earlier than payment date.']
            ]);
        }
    }

    public function delete(PurchasePayment $payment): void
    {
        $this->ensureDraft($payment, 'deleted');

        DB::transaction(
            fn() => $this->paymentRepo->delete(
                $payment
            )
        );
    }
}
