<?php

declare(strict_types=1);

namespace App\Business\Settlement;

use App\Business\Purchase\Enums\PurchaseOrderStatus;
use App\Business\Settlement\Enums\PayableStatus;
use App\Business\Settlement\Enums\SettlementAdjustmentStatus;
use App\Business\Settlement\Enums\SettlementRecordStatus;
use App\Models\DberpPayableRecord;
use App\Models\DberpPurchaseOrder;
use App\Services\DberpAuditService;
use App\Support\DberpAuditTrailGuard;
use Illuminate\Database\Eloquent\Model;

final class PayableService
{
    public function __construct(
        private readonly SettlementEntryService $entryService,
        private readonly DberpAuditService $auditService,
    ) {}

    public function remainingAmount(DberpPurchaseOrder $order): string
    {
        $fresh = DberpPurchaseOrder::query()->with(['returns', 'payableRecord.writeOffLines', 'payableRecord.adjustments'])->findOrFail($order->getKey());

        return $this->calculateSummary($fresh)['unpaid_amount'];
    }

    public function refreshForPurchaseOrder(
        DberpPurchaseOrder $order,
        string $changeType,
        ?string $remark = null,
        ?object $source = null,
        ?string $documentNumber = null,
        mixed $businessTime = null,
    ): void {
        $fresh = DberpPurchaseOrder::query()->with(['returns', 'payableRecord.writeOffLines', 'payableRecord.adjustments'])->findOrFail($order->getKey());
        $before = [
            'payable_amount' => (string) $fresh->payable_amount,
            'paid_amount' => (string) $fresh->paid_amount,
            'unpaid_amount' => (string) $fresh->unpaid_amount,
            'payable_status' => (string) $fresh->payable_status,
        ];

        $record = $this->syncRecord($fresh);
        $after = $this->calculateSummary($fresh, $record);

        if ($before === $after) {
            return;
        }

        $this->auditService->logSemanticSave(
            'dberp.purchase_order.payable_updated',
            $fresh,
            function () use ($fresh, $after): void {
                $fresh->forceFill($after)->save();
            },
            $before,
            $after,
        );

        $delta = (float) $after['unpaid_amount'] - (float) $before['unpaid_amount'];

        if (abs($delta) >= 0.00005) {
            $this->entryService->record(
                counterpartyType: 'supplier',
                counterpartyId: (int) $fresh->supplier_id,
                settlementSide: 'payable',
                changeType: $changeType,
                changeAmount: number_format($delta, 4, '.', ''),
                balanceAfter: $this->supplierOutstandingAmount((int) $fresh->supplier_id),
                source: $source instanceof Model ? $source : $fresh,
                documentNumber: $documentNumber ?? (string) $fresh->code,
                remark: $remark,
                businessTime: $businessTime ?? $fresh->business_time,
            );
        }
    }

    public function supplierOutstandingAmount(int $supplierId): string
    {
        $amount = DberpPayableRecord::query()
            ->where('supplier_id', $supplierId)
            ->whereNot('record_status', SettlementRecordStatus::Voided->value)
            ->sum('remaining_amount');

        return number_format((float) $amount, 4, '.', '');
    }

    /**
     * @return array<string, string>
     */
    private function calculateSummary(DberpPurchaseOrder $order, ?DberpPayableRecord $record = null): array
    {
        $record ??= $order->payableRecord;
        $payableAmount = (float) ($record?->adjusted_amount ?? 0);
        $paidAmount = (float) ($record?->written_off_amount ?? 0);
        $unpaidAmount = (float) ($record?->remaining_amount ?? 0);
        $status = PayableStatus::Unpaid;

        if ($payableAmount <= 0.00005 && $paidAmount <= 0.00005) {
            $status = PayableStatus::Paid;
        } elseif ($unpaidAmount < -0.00005) {
            $status = PayableStatus::OverPaid;
        } elseif ($paidAmount > 0.00005 && $unpaidAmount > 0.00005) {
            $status = PayableStatus::PartiallyPaid;
        } elseif (abs($unpaidAmount) <= 0.00005) {
            $status = PayableStatus::Paid;
        }

        return [
            'payable_amount' => number_format($payableAmount, 4, '.', ''),
            'paid_amount' => number_format($paidAmount, 4, '.', ''),
            'unpaid_amount' => number_format($unpaidAmount, 4, '.', ''),
            'payable_status' => $status->value,
        ];
    }

    private function syncRecord(DberpPurchaseOrder $order): DberpPayableRecord
    {
        return DberpAuditTrailGuard::withoutAuditTrail(function () use ($order): DberpPayableRecord {
            /** @var DberpPayableRecord $record */
            $record = DberpPayableRecord::query()->firstOrNew([
                'source_type' => 'purchase_order',
                'source_id' => $order->getKey(),
            ]);

            $baseAmount = (float) $order->total_amount;
            $returnedAmount = (float) $order->returns()->where('status', 'effective')->sum('total_amount');
            $writtenOffAmount = (float) $record->writeOffLines()->sum('effective_amount');
            $adjustmentAmount = (float) $record->adjustments()
                ->where('status', SettlementAdjustmentStatus::Effective->value)
                ->sum('effective_amount');
            $adjustedAmount = $order->status === PurchaseOrderStatus::Voided->value
                ? 0.0
                : max($baseAmount - $returnedAmount + $adjustmentAmount, 0.0);
            $remainingAmount = $adjustedAmount - $writtenOffAmount;

            $record->forceFill([
                'supplier_id' => $order->supplier_id,
                'source_document_no' => $order->code,
                'source_document_status' => $order->status,
                'business_date' => $order->business_time,
                'due_date' => $order->business_time?->toDateString(),
                'handler_employee_id' => $order->handler_employee_id,
                'source_system' => 'dberp',
                'external_document_no' => null,
                'external_reference_id' => null,
                'original_amount' => number_format($baseAmount, 4, '.', ''),
                'adjusted_amount' => number_format($adjustedAmount, 4, '.', ''),
                'written_off_amount' => number_format($writtenOffAmount, 4, '.', ''),
                'remaining_amount' => number_format($remainingAmount, 4, '.', ''),
                'record_status' => $this->resolveRecordStatus($order, $adjustedAmount, $remainingAmount),
                'formed_at' => $record->exists ? $record->formed_at : now(),
                'last_write_off_at' => $writtenOffAmount > 0.00005 ? now() : null,
                'closed_at' => $order->closed_at,
                'voided_at' => $order->voided_at,
                'remark' => $order->remark,
            ])->save();

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

    private function resolveRecordStatus(DberpPurchaseOrder $order, float $adjustedAmount, float $remainingAmount): string
    {
        if ($order->status === PurchaseOrderStatus::Voided->value) {
            return SettlementRecordStatus::Voided->value;
        }

        if (abs($remainingAmount) <= 0.00005) {
            if ($order->status === PurchaseOrderStatus::Closed->value) {
                return SettlementRecordStatus::Closed->value;
            }

            return SettlementRecordStatus::WrittenOff->value;
        }

        if ($adjustedAmount - $remainingAmount > 0.00005) {
            return SettlementRecordStatus::PartiallyWrittenOff->value;
        }

        return SettlementRecordStatus::Formed->value;
    }
}
