<?php

declare(strict_types=1);

namespace App\Business\Settlement;

use App\Business\Settlement\Enums\PaymentOrderType;
use App\Business\Settlement\Enums\SettlementDocumentStatus;
use App\Business\Settlement\Enums\SettlementFundsUsageStatus;
use App\Models\DberpPaymentOrder;
use App\Services\DberpAuditService;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class PaymentService
{
    public function __construct(
        private readonly SettlementRuleService $ruleService,
        private readonly SettlementOverLimitGuard $overLimitGuard,
        private readonly AdvanceAccountService $advanceAccountService,
        private readonly DberpAuditService $auditService,
    ) {}

    public function submit(DberpPaymentOrder $order): void
    {
        $blockedAuditPayload = null;

        try {
            DB::transaction(function () use ($order, &$blockedAuditPayload): void {
                $locked = DberpPaymentOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();
                $locked->loadMissing('purchaseOrder');

                if ($locked->status !== SettlementDocumentStatus::Draft->value) {
                    throw ValidationException::withMessages([
                        'status' => __('dberp.errors.settlement_doc_not_draft'),
                    ]);
                }

                $this->validateDraftDocument($locked, $blockedAuditPayload);

                $before = ['status' => $locked->status, 'submitted_at' => $locked->submitted_at];

                $this->auditService->logSemanticSave(
                    'dberp.payment_order.submitted',
                    $locked,
                    function () use ($locked): void {
                        $locked->forceFill([
                            'status' => SettlementDocumentStatus::Submitted->value,
                            'submitted_at' => now(),
                        ])->save();
                    },
                    $before,
                    [
                        'status' => $locked->status,
                        'submitted_at' => $locked->submitted_at,
                    ],
                );
            });
        } catch (ValidationException $exception) {
            if (is_array($blockedAuditPayload)) {
                $this->auditService->log('dberp.payment_order.closed_document_blocked', $order, null, $blockedAuditPayload);
            }

            throw $exception;
        }
    }

    public function confirm(DberpPaymentOrder $order): void
    {
        $blockedAuditPayload = null;

        try {
            DB::transaction(function () use ($order, &$blockedAuditPayload): void {
                $locked = DberpPaymentOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();
                $locked->loadMissing('purchaseOrder');

                if (! in_array($locked->status, [SettlementDocumentStatus::Draft->value, SettlementDocumentStatus::Submitted->value], true)) {
                    throw ValidationException::withMessages([
                        'status' => __('dberp.errors.settlement_doc_not_submitted'),
                    ]);
                }

                $this->validateDraftDocument($locked, $blockedAuditPayload);

                $before = [
                    'status' => $locked->status,
                    'effective_at' => $locked->effective_at,
                    'available_amount' => (string) $locked->available_amount,
                    'write_off_status' => (string) $locked->write_off_status,
                ];

                $this->auditService->logSemanticSave(
                    'dberp.payment_order.confirmed',
                    $locked,
                    function () use ($locked): void {
                        $locked->forceFill([
                            'status' => SettlementDocumentStatus::Effective->value,
                            'effective_at' => now(),
                            'available_amount' => (string) $locked->amount,
                            'written_off_amount' => '0.0000',
                            'occupied_amount' => '0.0000',
                            'write_off_status' => SettlementFundsUsageStatus::Unwritten->value,
                        ])->save();
                    },
                    $before,
                    [
                        'status' => $locked->status,
                        'effective_at' => $locked->effective_at,
                        'available_amount' => (string) $locked->available_amount,
                        'write_off_status' => (string) $locked->write_off_status,
                    ],
                );

                if ($locked->payment_type === PaymentOrderType::Advance->value) {
                    $this->advanceAccountService->confirmPayment($locked);
                }
            });
        } catch (ValidationException $exception) {
            if (is_array($blockedAuditPayload)) {
                $this->auditService->log('dberp.payment_order.closed_document_blocked', $order, null, $blockedAuditPayload);
            }

            throw $exception;
        }
    }

    public function apply(DberpPaymentOrder $order): void
    {
        $this->confirm($order);
    }

    public function void(DberpPaymentOrder $order): void
    {
        if (! $this->ruleService->allowsSettlementVoid()) {
            throw ValidationException::withMessages([
                'status' => __('dberp.errors.settlement_void_policy_blocked'),
            ]);
        }

        DB::transaction(function () use ($order): void {
            $locked = DberpPaymentOrder::query()->whereKey($order->getKey())->lockForUpdate()->firstOrFail();

            if ($locked->status !== SettlementDocumentStatus::Effective->value) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.settlement_doc_not_effective'),
                ]);
            }

            if ((float) $locked->written_off_amount > 0.00005) {
                throw ValidationException::withMessages([
                    'status' => __('dberp.errors.settlement_document_has_write_offs'),
                ]);
            }

            $before = ['status' => $locked->status, 'voided_at' => $locked->voided_at];

            $this->auditService->logSemanticSave(
                'dberp.payment_order.voided',
                $locked,
                function () use ($locked): void {
                    $locked->forceFill([
                        'status' => SettlementDocumentStatus::Voided->value,
                        'voided_at' => now(),
                        'available_amount' => '0.0000',
                        'occupied_amount' => '0.0000',
                    ])->save();
                },
                $before,
                [
                    'status' => $locked->status,
                    'voided_at' => $locked->voided_at,
                ],
            );

            if ($locked->payment_type === PaymentOrderType::Advance->value) {
                $this->advanceAccountService->voidPayment($locked);
            }
        });
    }

    private function validateDraftDocument(DberpPaymentOrder $order, ?array &$blockedAuditPayload = null): void
    {
        if ((float) $order->amount <= 0) {
            throw ValidationException::withMessages([
                'amount' => __('dberp.errors.payment_amount_invalid'),
            ]);
        }

        if ($order->purchaseOrder !== null) {
            if ((int) $order->purchaseOrder->supplier_id !== (int) $order->supplier_id) {
                throw ValidationException::withMessages([
                    'supplier_id' => __('dberp.errors.purchase_supplier_mismatch'),
                ]);
            }

            if ($order->purchaseOrder->status === 'draft' || $order->purchaseOrder->status === 'voided') {
                throw ValidationException::withMessages([
                    'purchase_order_id' => __('dberp.errors.payment_purchase_order_unsettled'),
                ]);
            }

            if ($order->purchaseOrder->isSettlementBlockedByClosedPolicy() && ! $this->ruleService->allowSettlementOnClosedDocument()) {
                $blockedAuditPayload = [
                    'purchase_order_id' => $order->purchase_order_id,
                    'status' => $order->purchaseOrder->status,
                ];

                throw ValidationException::withMessages([
                    'purchase_order_id' => __('dberp.errors.settlement_closed_document_blocked'),
                ]);
            }
        }

        $this->overLimitGuard->assertPaymentWithinPolicy($order);
    }
}
