<?php

declare(strict_types=1);

namespace App\Business\Settlement;

use App\Business\Settlement\Enums\PaymentOrderType;
use App\Business\Settlement\Enums\ReceiptOrderType;
use App\Business\Settlement\Enums\SettlementFundsUsageStatus;
use App\Business\Settlement\Enums\SettlementWriteOffLineStatus;
use App\Business\Settlement\Enums\SettlementWriteOffStatus;
use App\Models\DberpPaymentOrder;
use App\Models\DberpPayableRecord;
use App\Models\DberpPurchaseOrder;
use App\Models\DberpReceiptOrder;
use App\Models\DberpReceivableRecord;
use App\Models\DberpSalesOrder;
use App\Models\DberpSettlementWriteOff;
use App\Models\DberpSettlementWriteOffLine;
use App\Services\DberpAuditService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class SettlementWriteOffService
{
    public function __construct(
        private readonly ReceivableService $receivableService,
        private readonly PayableService $payableService,
        private readonly AdvanceAccountService $advanceAccountService,
        private readonly DberpAuditService $auditService,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function createConfirmed(array $data): DberpSettlementWriteOff
    {
        return DB::transaction(function () use ($data): DberpSettlementWriteOff {
            $side = (string) ($data['settlement_side'] ?? '');
            $counterpartyType = $side === 'receivable' ? 'customer' : 'supplier';
            $counterpartyId = (int) ($data['counterparty_id'] ?? 0);
            $businessDate = $data['business_date'] ?? now();
            $remark = $data['remark'] ?? null;
            $lines = $data['lines'] ?? [];

            if (! in_array($side, ['receivable', 'payable'], true)) {
                throw ValidationException::withMessages([
                    'settlement_side' => '请选择核销方向。',
                ]);
            }

            if ($counterpartyId <= 0) {
                throw ValidationException::withMessages([
                    'counterparty_id' => '请选择往来对象。',
                ]);
            }

            if (! is_array($lines) || $lines === []) {
                throw ValidationException::withMessages([
                    'lines' => '至少需要一条核销明细。',
                ]);
            }

            /** @var DberpSettlementWriteOff $writeOff */
            $writeOff = DberpSettlementWriteOff::query()->create([
                'settlement_side' => $side,
                'counterparty_type' => $counterpartyType,
                'counterparty_id' => $counterpartyId,
                'total_amount' => '0.0000',
                'reversed_amount' => '0.0000',
                'effective_amount' => '0.0000',
                'write_off_status' => SettlementWriteOffStatus::Confirmed->value,
                'business_date' => $businessDate,
                'handler_employee_id' => $data['handler_employee_id'] ?? null,
                'source_system' => $data['source_system'] ?? 'manual',
                'external_document_no' => $data['external_document_no'] ?? null,
                'external_reference_id' => $data['external_reference_id'] ?? null,
                'confirmed_at' => now(),
                'remark' => is_string($remark) ? $remark : null,
            ]);

            $totalAmount = 0.0;
            $touchedSalesOrders = [];
            $touchedPurchaseOrders = [];

            foreach (array_values($lines) as $index => $line) {
                if (! is_array($line)) {
                    continue;
                }

                $amount = number_format((float) ($line['write_off_amount'] ?? 0), 4, '.', '');

                if ((float) $amount <= 0.00005) {
                    throw ValidationException::withMessages([
                        'lines' => '核销金额必须大于 0。',
                    ]);
                }

                if ($side === 'receivable') {
                    $salesOrder = $this->applyReceivableLine(
                        writeOff: $writeOff,
                        lineNo: $index + 1,
                        counterpartyId: $counterpartyId,
                        line: $line,
                        amount: $amount,
                        businessDate: $businessDate,
                    );

                    if ($salesOrder !== null) {
                        $touchedSalesOrders[$salesOrder->getKey()] = $salesOrder;
                    }
                } else {
                    $purchaseOrder = $this->applyPayableLine(
                        writeOff: $writeOff,
                        lineNo: $index + 1,
                        counterpartyId: $counterpartyId,
                        line: $line,
                        amount: $amount,
                        businessDate: $businessDate,
                    );

                    if ($purchaseOrder !== null) {
                        $touchedPurchaseOrders[$purchaseOrder->getKey()] = $purchaseOrder;
                    }
                }

                $totalAmount += (float) $amount;
            }

            $writeOff->forceFill([
                'total_amount' => number_format($totalAmount, 4, '.', ''),
                'effective_amount' => number_format($totalAmount, 4, '.', ''),
            ])->save();

            foreach ($touchedSalesOrders as $salesOrder) {
                $this->receivableService->refreshForSalesOrder(
                    order: $salesOrder,
                    changeType: 'write_off_confirmed',
                    remark: (string) ($remark ?? ''),
                    source: $writeOff,
                    documentNumber: (string) $writeOff->code,
                    businessTime: $businessDate,
                );
            }

            foreach ($touchedPurchaseOrders as $purchaseOrder) {
                $this->payableService->refreshForPurchaseOrder(
                    order: $purchaseOrder,
                    changeType: 'write_off_confirmed',
                    remark: (string) ($remark ?? ''),
                    source: $writeOff,
                    documentNumber: (string) $writeOff->code,
                    businessTime: $businessDate,
                );
            }

            $this->auditService->log('dberp.settlement_write_off.confirmed', $writeOff, null, [
                'total_amount' => (string) $writeOff->total_amount,
                'line_count' => $writeOff->lines()->count(),
            ]);

            return $writeOff->refresh();
        });
    }

    public function reverse(DberpSettlementWriteOff $writeOff): void
    {
        DB::transaction(function () use ($writeOff): void {
            $locked = DberpSettlementWriteOff::query()->whereKey($writeOff->getKey())->lockForUpdate()->firstOrFail();
            $lines = $locked->lines()->orderBy('line_no')->lockForUpdate()->get();

            if (! in_array($locked->write_off_status, [
                SettlementWriteOffStatus::Confirmed->value,
                SettlementWriteOffStatus::PartiallyReversed->value,
            ], true)) {
                throw ValidationException::withMessages([
                    'write_off_status' => '当前核销单不能继续反核销。',
                ]);
            }

            $reversedAmount = 0.0;
            $touchedSalesOrders = [];
            $touchedPurchaseOrders = [];

            foreach ($lines as $line) {
                if ((float) $line->effective_amount <= 0.00005) {
                    continue;
                }

                $amount = number_format((float) $line->effective_amount, 4, '.', '');

                if ($locked->settlement_side === 'receivable') {
                    $salesOrder = $this->reverseReceivableLine($line, $amount);

                    if ($salesOrder !== null) {
                        $touchedSalesOrders[$salesOrder->getKey()] = $salesOrder;
                    }
                } else {
                    $purchaseOrder = $this->reversePayableLine($line, $amount);

                    if ($purchaseOrder !== null) {
                        $touchedPurchaseOrders[$purchaseOrder->getKey()] = $purchaseOrder;
                    }
                }

                $line->forceFill([
                    'reversed_amount' => number_format((float) $line->reversed_amount + (float) $amount, 4, '.', ''),
                    'effective_amount' => '0.0000',
                    'line_status' => SettlementWriteOffLineStatus::Reversed->value,
                    'reversed_at' => now(),
                ])->save();

                $reversedAmount += (float) $amount;
            }

            $effectiveAmount = (float) $locked->effective_amount - $reversedAmount;
            $status = $effectiveAmount <= 0.00005
                ? SettlementWriteOffStatus::Reversed
                : SettlementWriteOffStatus::PartiallyReversed;

            $before = [
                'reversed_amount' => (string) $locked->reversed_amount,
                'effective_amount' => (string) $locked->effective_amount,
                'write_off_status' => (string) $locked->write_off_status,
            ];

            $locked->forceFill([
                'reversed_amount' => number_format((float) $locked->reversed_amount + $reversedAmount, 4, '.', ''),
                'effective_amount' => number_format(max($effectiveAmount, 0), 4, '.', ''),
                'write_off_status' => $status->value,
                'reversed_at' => now(),
            ])->save();

            foreach ($touchedSalesOrders as $salesOrder) {
                $this->receivableService->refreshForSalesOrder(
                    order: $salesOrder,
                    changeType: 'write_off_reversed',
                    remark: $locked->remark,
                    source: $locked,
                    documentNumber: (string) $locked->code,
                    businessTime: now(),
                );
            }

            foreach ($touchedPurchaseOrders as $purchaseOrder) {
                $this->payableService->refreshForPurchaseOrder(
                    order: $purchaseOrder,
                    changeType: 'write_off_reversed',
                    remark: $locked->remark,
                    source: $locked,
                    documentNumber: (string) $locked->code,
                    businessTime: now(),
                );
            }

            $this->auditService->log('dberp.settlement_write_off.reversed', $locked, $before, [
                'reversed_amount' => (string) $locked->reversed_amount,
                'effective_amount' => (string) $locked->effective_amount,
                'write_off_status' => (string) $locked->write_off_status,
            ]);
        });
    }

    /**
     * @param  array<string, mixed>  $line
     */
    private function applyReceivableLine(
        DberpSettlementWriteOff $writeOff,
        int $lineNo,
        int $counterpartyId,
        array $line,
        string $amount,
        mixed $businessDate,
    ): ?DberpSalesOrder {
        /** @var DberpReceiptOrder $receipt */
        $receipt = DberpReceiptOrder::query()->whereKey((int) ($line['receipt_order_id'] ?? 0))->lockForUpdate()->firstOrFail();
        /** @var DberpReceivableRecord $record */
        $record = DberpReceivableRecord::query()->whereKey((int) ($line['receivable_record_id'] ?? 0))->lockForUpdate()->firstOrFail();

        if ((int) $receipt->customer_id !== $counterpartyId || (int) $record->customer_id !== $counterpartyId) {
            throw ValidationException::withMessages([
                'lines' => '核销对象与所选客户不一致。',
            ]);
        }

        if ($receipt->status !== 'effective') {
            throw ValidationException::withMessages([
                'lines' => '只有已确认的收款单才允许核销。',
            ]);
        }

        if ($record->record_status === 'voided') {
            throw ValidationException::withMessages([
                'lines' => '已作废的应收记录不能继续核销。',
            ]);
        }

        if ((float) $receipt->available_amount + 0.00005 < (float) $amount) {
            throw ValidationException::withMessages([
                'lines' => '收款单可用金额不足。',
            ]);
        }

        if ((float) $record->remaining_amount + 0.00005 < (float) $amount) {
            throw ValidationException::withMessages([
                'lines' => '应收记录剩余未核销金额不足。',
            ]);
        }

        /** @var DberpSettlementWriteOffLine $writeOffLine */
        $writeOffLine = $writeOff->lines()->create([
            'line_no' => $lineNo,
            'settlement_side' => 'receivable',
            'receipt_order_id' => $receipt->getKey(),
            'receivable_record_id' => $record->getKey(),
            'write_off_amount' => $amount,
            'reversed_amount' => '0.0000',
            'effective_amount' => $amount,
            'line_status' => SettlementWriteOffLineStatus::Confirmed->value,
            'business_date' => $businessDate,
            'remark' => $line['remark'] ?? null,
        ]);

        $this->syncReceiptAmounts($receipt, $amount, true);

        if ($receipt->receipt_type === ReceiptOrderType::Advance->value) {
            $this->advanceAccountService->consumeReceipt($receipt, $writeOffLine, $amount);
        }

        return $record->source_type === 'sales_order' && $record->source_id !== null
            ? DberpSalesOrder::query()->find($record->source_id)
            : null;
    }

    /**
     * @param  array<string, mixed>  $line
     */
    private function applyPayableLine(
        DberpSettlementWriteOff $writeOff,
        int $lineNo,
        int $counterpartyId,
        array $line,
        string $amount,
        mixed $businessDate,
    ): ?DberpPurchaseOrder {
        /** @var DberpPaymentOrder $payment */
        $payment = DberpPaymentOrder::query()->whereKey((int) ($line['payment_order_id'] ?? 0))->lockForUpdate()->firstOrFail();
        /** @var DberpPayableRecord $record */
        $record = DberpPayableRecord::query()->whereKey((int) ($line['payable_record_id'] ?? 0))->lockForUpdate()->firstOrFail();

        if ((int) $payment->supplier_id !== $counterpartyId || (int) $record->supplier_id !== $counterpartyId) {
            throw ValidationException::withMessages([
                'lines' => '核销对象与所选供应商不一致。',
            ]);
        }

        if ($payment->status !== 'effective') {
            throw ValidationException::withMessages([
                'lines' => '只有已确认的付款单才允许核销。',
            ]);
        }

        if ($record->record_status === 'voided') {
            throw ValidationException::withMessages([
                'lines' => '已作废的应付记录不能继续核销。',
            ]);
        }

        if ((float) $payment->available_amount + 0.00005 < (float) $amount) {
            throw ValidationException::withMessages([
                'lines' => '付款单可用金额不足。',
            ]);
        }

        if ((float) $record->remaining_amount + 0.00005 < (float) $amount) {
            throw ValidationException::withMessages([
                'lines' => '应付记录剩余未核销金额不足。',
            ]);
        }

        /** @var DberpSettlementWriteOffLine $writeOffLine */
        $writeOffLine = $writeOff->lines()->create([
            'line_no' => $lineNo,
            'settlement_side' => 'payable',
            'payment_order_id' => $payment->getKey(),
            'payable_record_id' => $record->getKey(),
            'write_off_amount' => $amount,
            'reversed_amount' => '0.0000',
            'effective_amount' => $amount,
            'line_status' => SettlementWriteOffLineStatus::Confirmed->value,
            'business_date' => $businessDate,
            'remark' => $line['remark'] ?? null,
        ]);

        $this->syncPaymentAmounts($payment, $amount, true);

        if ($payment->payment_type === PaymentOrderType::Advance->value) {
            $this->advanceAccountService->consumePayment($payment, $writeOffLine, $amount);
        }

        return $record->source_type === 'purchase_order' && $record->source_id !== null
            ? DberpPurchaseOrder::query()->find($record->source_id)
            : null;
    }

    private function reverseReceivableLine(DberpSettlementWriteOffLine $line, string $amount): ?DberpSalesOrder
    {
        /** @var DberpReceiptOrder $receipt */
        $receipt = DberpReceiptOrder::query()->whereKey($line->receipt_order_id)->lockForUpdate()->firstOrFail();
        /** @var DberpReceivableRecord $record */
        $record = DberpReceivableRecord::query()->whereKey($line->receivable_record_id)->lockForUpdate()->firstOrFail();

        $this->syncReceiptAmounts($receipt, $amount, false);

        if ($receipt->receipt_type === ReceiptOrderType::Advance->value) {
            $this->advanceAccountService->releaseReceipt($receipt, $line, $amount);
        }

        return $record->source_type === 'sales_order' && $record->source_id !== null
            ? DberpSalesOrder::query()->find($record->source_id)
            : null;
    }

    private function reversePayableLine(DberpSettlementWriteOffLine $line, string $amount): ?DberpPurchaseOrder
    {
        /** @var DberpPaymentOrder $payment */
        $payment = DberpPaymentOrder::query()->whereKey($line->payment_order_id)->lockForUpdate()->firstOrFail();
        /** @var DberpPayableRecord $record */
        $record = DberpPayableRecord::query()->whereKey($line->payable_record_id)->lockForUpdate()->firstOrFail();

        $this->syncPaymentAmounts($payment, $amount, false);

        if ($payment->payment_type === PaymentOrderType::Advance->value) {
            $this->advanceAccountService->releasePayment($payment, $line, $amount);
        }

        return $record->source_type === 'purchase_order' && $record->source_id !== null
            ? DberpPurchaseOrder::query()->find($record->source_id)
            : null;
    }

    private function syncReceiptAmounts(DberpReceiptOrder $receipt, string $amount, bool $consume): void
    {
        $writtenOff = (float) $receipt->written_off_amount + ($consume ? (float) $amount : -(float) $amount);
        $available = (float) $receipt->available_amount + ($consume ? -(float) $amount : (float) $amount);

        $receipt->forceFill([
            'written_off_amount' => number_format($writtenOff, 4, '.', ''),
            'available_amount' => number_format($available, 4, '.', ''),
            'write_off_status' => $this->resolveFundsUsageStatus((float) $receipt->amount, $writtenOff),
        ])->save();
    }

    private function syncPaymentAmounts(DberpPaymentOrder $payment, string $amount, bool $consume): void
    {
        $writtenOff = (float) $payment->written_off_amount + ($consume ? (float) $amount : -(float) $amount);
        $available = (float) $payment->available_amount + ($consume ? -(float) $amount : (float) $amount);

        $payment->forceFill([
            'written_off_amount' => number_format($writtenOff, 4, '.', ''),
            'available_amount' => number_format($available, 4, '.', ''),
            'write_off_status' => $this->resolveFundsUsageStatus((float) $payment->amount, $writtenOff),
        ])->save();
    }

    private function resolveFundsUsageStatus(float $amount, float $writtenOff): string
    {
        if ($writtenOff <= 0.00005) {
            return SettlementFundsUsageStatus::Unwritten->value;
        }

        if (abs($amount - $writtenOff) <= 0.00005) {
            return SettlementFundsUsageStatus::Written->value;
        }

        return SettlementFundsUsageStatus::PartiallyWritten->value;
    }
}