<?php

declare(strict_types=1);

namespace App\Business\Purchase;

use App\Business\Purchase\Enums\PurchaseExceptionResolutionResult;
use App\Business\Purchase\Enums\PurchaseExceptionStatus;
use App\Business\Purchase\Enums\PurchaseExceptionType;
use App\Models\DberpPurchaseException;
use App\Models\DberpPurchaseOrder;
use App\Models\DberpPurchaseReceiptOrder;
use App\Models\DberpPurchaseReceiptOrderItem;
use App\Services\DberpAuditService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;

final class PurchaseExceptionService
{
    public function __construct(
        private readonly PurchaseOrderExecutionService $purchaseOrderExecutionService,
        private readonly DberpAuditService $auditService,
    ) {}

    public function query(): Builder
    {
        return DberpPurchaseException::query()->with([
            'purchaseOrder',
            'purchaseOrderItem.product',
            'purchaseReceiptOrder',
            'supplier',
            'product',
            'handler',
        ]);
    }

    public function syncFromReceiptItem(DberpPurchaseReceiptOrder $receipt, DberpPurchaseReceiptOrderItem $item, float $remainingReceivableBeforeApply): ?DberpPurchaseException
    {
        $arrivalQuantity = (float) $item->arrival_quantity;
        $receivedQuantity = (float) $item->received_quantity;
        $rejectedQuantity = max($arrivalQuantity - $receivedQuantity, 0);
        $oversupplyQuantity = max($arrivalQuantity - max($remainingReceivableBeforeApply, 0), 0);
        $explicitType = $item->exception_type !== null && $item->exception_type !== '' ? (string) $item->exception_type : null;
        $type = $explicitType ?? $this->detectType($rejectedQuantity, $oversupplyQuantity);

        if ($type === null && trim((string) $item->exception_reason_detail) === '' && trim((string) $item->handling_suggestion) === '') {
            return null;
        }

        $differenceQuantity = $this->differenceQuantityForType($type, $arrivalQuantity, $receivedQuantity, $rejectedQuantity, $oversupplyQuantity, $remainingReceivableBeforeApply);
        $pendingQuantity = max($differenceQuantity, $rejectedQuantity);
        $exceptionType = $type ?? PurchaseExceptionType::Rejected->value;

        $record = DberpPurchaseException::query()->firstOrNew([
            'purchase_receipt_item_id' => $item->getKey(),
        ]);

        $record->fill([
            'purchase_order_id' => $receipt->purchase_order_id,
            'purchase_order_code' => $receipt->purchaseOrder?->code,
            'purchase_order_item_id' => $item->purchase_order_item_id,
            'purchase_receipt_order_id' => $receipt->getKey(),
            'purchase_receipt_code' => $receipt->code,
            'supplier_id' => $receipt->supplier_id,
            'product_id' => $item->product_id,
            'exception_type' => $exceptionType,
            'status' => PurchaseExceptionStatus::Pending->value,
            'resolution_result' => null,
            'arrival_quantity' => number_format($arrivalQuantity, 4, '.', ''),
            'received_quantity' => number_format($receivedQuantity, 4, '.', ''),
            'rejected_quantity' => number_format($rejectedQuantity, 4, '.', ''),
            'difference_quantity' => number_format($differenceQuantity, 4, '.', ''),
            'pending_quantity' => number_format($pendingQuantity, 4, '.', ''),
            'reason_detail' => $item->exception_reason_detail,
            'handling_suggestion' => $item->handling_suggestion,
            'resolution_remark' => null,
            'handled_by' => null,
            'occurred_at' => $receipt->arrival_date ?? $receipt->business_time ?? now(),
            'handled_at' => null,
            'closed_at' => null,
            'context_snapshot' => [
                'remaining_receivable_before_apply' => number_format($remainingReceivableBeforeApply, 4, '.', ''),
                'receipt_status' => $receipt->status,
                'purchase_order_item_id' => $item->purchase_order_item_id,
                'exception_type_input' => $item->exception_type,
                'item_remark' => $item->remark,
            ],
        ]);
        $this->auditService->logSemanticSave(
            'dberp.purchase_exception.registered',
            $record,
            function () use ($record): void {
                $record->save();
            },
            null,
            [
                'exception_type' => $record->exception_type,
                'difference_quantity' => $record->difference_quantity,
                'pending_quantity' => $record->pending_quantity,
            ],
        );

        return $record;
    }

    public function submitResolution(DberpPurchaseException $exception, PurchaseExceptionResolutionResult $result, ?string $remark = null): DberpPurchaseException
    {
        if ($result === PurchaseExceptionResolutionResult::AmendOrder) {
            $this->applyOrderAmendment($exception);
        }

        $status = in_array($result, [PurchaseExceptionResolutionResult::Resupply, PurchaseExceptionResolutionResult::FollowUp], true)
            ? PurchaseExceptionStatus::Processing
            : PurchaseExceptionStatus::Resolved;

        $before = $exception->only(['status', 'resolution_result', 'resolution_remark', 'pending_quantity', 'handled_by', 'handled_at']);

        $this->auditService->logSemanticSave(
            'dberp.purchase_exception.resolution_submitted',
            $exception,
            function () use ($exception, $status, $result, $remark): void {
                $exception->forceFill([
                    'status' => $status->value,
                    'resolution_result' => $result->value,
                    'resolution_remark' => $remark,
                    'handled_by' => Auth::id(),
                    'handled_at' => now(),
                    'pending_quantity' => $status === PurchaseExceptionStatus::Resolved ? '0.0000' : (string) $exception->pending_quantity,
                ])->save();
            },
            $before,
            [
                'status' => $exception->status,
                'resolution_result' => $exception->resolution_result,
                'resolution_remark' => $exception->resolution_remark,
                'pending_quantity' => $exception->pending_quantity,
                'handled_by' => $exception->handled_by,
                'handled_at' => $exception->handled_at?->toDateTimeString(),
            ],
        );

        return $exception;
    }

    public function close(DberpPurchaseException $exception, ?string $remark = null): DberpPurchaseException
    {
        $before = $exception->only(['status', 'resolution_result', 'resolution_remark', 'closed_at']);

        $this->auditService->logSemanticSave(
            'dberp.purchase_exception.closed',
            $exception,
            function () use ($exception, $remark): void {
                $exception->forceFill([
                    'status' => PurchaseExceptionStatus::Closed->value,
                    'resolution_result' => $exception->resolution_result ?? PurchaseExceptionResolutionResult::Close->value,
                    'resolution_remark' => $remark ?: $exception->resolution_remark,
                    'handled_by' => Auth::id(),
                    'handled_at' => now(),
                    'closed_at' => now(),
                    'pending_quantity' => '0.0000',
                ])->save();
            },
            $before,
            [
                'status' => $exception->status,
                'resolution_result' => $exception->resolution_result,
                'resolution_remark' => $exception->resolution_remark,
                'closed_at' => $exception->closed_at?->toDateTimeString(),
            ],
        );

        return $exception;
    }

    public function reopen(DberpPurchaseException $exception, ?string $remark = null): DberpPurchaseException
    {
        $before = $exception->only(['status', 'resolution_result', 'resolution_remark', 'closed_at']);

        $this->auditService->logSemanticSave(
            'dberp.purchase_exception.reopened',
            $exception,
            function () use ($exception, $remark): void {
                $exception->forceFill([
                    'status' => PurchaseExceptionStatus::Pending->value,
                    'resolution_result' => null,
                    'resolution_remark' => $remark,
                    'handled_by' => Auth::id(),
                    'handled_at' => now(),
                    'closed_at' => null,
                    'pending_quantity' => (string) max((float) $exception->difference_quantity, (float) $exception->rejected_quantity),
                ])->save();
            },
            $before,
            [
                'status' => $exception->status,
                'resolution_remark' => $exception->resolution_remark,
            ],
        );

        return $exception;
    }

    public function closeByReceiptVoid(DberpPurchaseReceiptOrder $receipt, ?string $remark = null): void
    {
        DberpPurchaseException::query()
            ->where('purchase_receipt_order_id', $receipt->getKey())
            ->whereIn('status', [PurchaseExceptionStatus::Pending->value, PurchaseExceptionStatus::Processing->value, PurchaseExceptionStatus::Resolved->value])
            ->get()
            ->each(fn (DberpPurchaseException $exception) => $this->close($exception, $remark ?? '来源到货单已作废，异常同步关闭。'));
    }

    private function detectType(float $rejectedQuantity, float $oversupplyQuantity): ?string
    {
        if ($oversupplyQuantity > 0.00005) {
            return PurchaseExceptionType::ExcessSupply->value;
        }

        if ($rejectedQuantity > 0.00005) {
            return PurchaseExceptionType::Rejected->value;
        }

        return null;
    }

    private function differenceQuantityForType(?string $type, float $arrivalQuantity, float $receivedQuantity, float $rejectedQuantity, float $oversupplyQuantity, float $remainingReceivableBeforeApply): float
    {
        return match ($type) {
            PurchaseExceptionType::ExcessSupply->value => $oversupplyQuantity,
            PurchaseExceptionType::ShortSupply->value => max($remainingReceivableBeforeApply - $arrivalQuantity, 0),
            PurchaseExceptionType::WrongDelivery->value,
            PurchaseExceptionType::NearExpiry->value,
            PurchaseExceptionType::AppearanceIssue->value,
            PurchaseExceptionType::Rejected->value => max($rejectedQuantity, $arrivalQuantity - $receivedQuantity, 0),
            default => max($rejectedQuantity, $oversupplyQuantity, 0),
        };
    }

    private function applyOrderAmendment(DberpPurchaseException $exception): void
    {
        if ($exception->purchase_order_id === null || $exception->purchase_order_item_id === null) {
            return;
        }

        $order = DberpPurchaseOrder::query()->with('items')->whereKey($exception->purchase_order_id)->lockForUpdate()->first();

        if ($order === null) {
            return;
        }

        $item = $order->items->firstWhere('id', $exception->purchase_order_item_id);

        if ($item === null) {
            return;
        }

        $newQuantity = max((float) $item->quantity - (float) $exception->pending_quantity, (float) $item->received_quantity);

        $item->forceFill([
            'quantity' => number_format($newQuantity, 4, '.', ''),
        ])->save();

        app(PurchaseOrderService::class)->refreshTotals($order);
        $this->purchaseOrderExecutionService->syncOrder($order);
    }
}
