<?php

declare(strict_types=1);

namespace App\Business\Settlement;

use App\Business\Sales\Enums\SalesOrderStatus;
use App\Business\Settlement\Enums\ReceivableStatus;
use App\Business\Settlement\Enums\SettlementAdjustmentStatus;
use App\Business\Settlement\Enums\SettlementRecordStatus;
use App\Models\DberpReceivableRecord;
use App\Models\DberpSalesOrder;
use App\Services\DberpAuditService;
use App\Support\DberpAuditTrailGuard;
use Illuminate\Database\Eloquent\Model;

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

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

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

    public function refreshForSalesOrder(
        DberpSalesOrder $order,
        string $changeType,
        ?string $remark = null,
        ?object $source = null,
        ?string $documentNumber = null,
        mixed $businessTime = null,
    ): void {
        $fresh = DberpSalesOrder::query()->with(['returns', 'receivableRecord.writeOffLines', 'receivableRecord.adjustments'])->findOrFail($order->getKey());
        $before = [
            'receivable_amount' => (string) $fresh->receivable_amount,
            'received_amount' => (string) $fresh->received_amount,
            'unreceived_amount' => (string) $fresh->unreceived_amount,
            'receivable_status' => (string) $fresh->receivable_status,
        ];

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

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

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

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

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

    public function customerOutstandingAmount(int $customerId): string
    {
        $amount = DberpReceivableRecord::query()
            ->where('customer_id', $customerId)
            ->whereNot('record_status', SettlementRecordStatus::Voided->value)
            ->sum('remaining_amount');

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

    /**
     * @return array<string, string>
     */
    private function calculateSummary(DberpSalesOrder $order, ?DberpReceivableRecord $record = null): array
    {
        $record ??= $order->receivableRecord;
        $receivableAmount = (float) ($record?->adjusted_amount ?? 0);
        $receivedAmount = (float) ($record?->written_off_amount ?? 0);
        $unreceivedAmount = (float) ($record?->remaining_amount ?? 0);
        $status = ReceivableStatus::Uncollected;

        if ($receivableAmount <= 0.00005 && $receivedAmount <= 0.00005) {
            $status = ReceivableStatus::Collected;
        } elseif ($unreceivedAmount < -0.00005) {
            $status = ReceivableStatus::OverCollected;
        } elseif ($receivedAmount > 0.00005 && $unreceivedAmount > 0.00005) {
            $status = ReceivableStatus::PartiallyCollected;
        } elseif (abs($unreceivedAmount) <= 0.00005) {
            $status = ReceivableStatus::Collected;
        }

        return [
            'receivable_amount' => number_format($receivableAmount, 4, '.', ''),
            'received_amount' => number_format($receivedAmount, 4, '.', ''),
            'unreceived_amount' => number_format($unreceivedAmount, 4, '.', ''),
            'receivable_status' => $status->value,
        ];
    }

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

            $baseAmount = (float) $order->final_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 === SalesOrderStatus::Voided->value
                ? 0.0
                : max($baseAmount - $returnedAmount + $adjustmentAmount, 0.0);
            $remainingAmount = $adjustedAmount - $writtenOffAmount;

            $record->forceFill([
                'customer_id' => $order->customer_id,
                'source_document_no' => $order->code,
                'source_document_status' => $order->status,
                'business_date' => $order->business_time,
                'due_date' => $order->due_date?->toDateString() ?? $order->business_time?->toDateString(),
                'handler_employee_id' => $order->handler_employee_id,
                'source_system' => $order->source_platform ?? 'dberp',
                'external_document_no' => $order->external_order_no,
                'external_reference_id' => $order->external_order_id,
                '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(DberpSalesOrder $order, float $adjustedAmount, float $remainingAmount): string
    {
        if ($order->status === SalesOrderStatus::Voided->value) {
            return SettlementRecordStatus::Voided->value;
        }

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

            return SettlementRecordStatus::WrittenOff->value;
        }

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

        return SettlementRecordStatus::Formed->value;
    }
}
