<?php

declare(strict_types=1);

namespace App\Business\Purchase;

use App\Business\Product\DberpProductUnitConversionService;
use App\Business\Purchase\Enums\PurchaseOrderItemExecutionStatus;
use App\Business\Purchase\Enums\PurchaseOrderStatus;
use App\Models\DberpPurchaseOrder;
use App\Models\DberpPurchaseOrderItem;
use App\Models\DberpPurchaseReceiptOrder;
use App\Services\DberpAuditService;
use App\Support\DberpAuditTrailGuard;
use Illuminate\Support\Collection;

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

    public function syncOrder(DberpPurchaseOrder $order): void
    {
        $before = null;
        $after = null;
        $locked = null;

        DberpAuditTrailGuard::withoutAuditTrail(function () use ($order, &$before, &$after, &$locked): void {
            $locked = DberpPurchaseOrder::query()
                ->with([
                    'items',
                    'receipts' => fn ($query) => $query
                        ->where('status', 'effective')
                        ->with('items')
                        ->orderBy('arrival_date')
                        ->orderBy('id'),
                ])
                ->whereKey($order->getKey())
                ->lockForUpdate()
                ->firstOrFail();

            $receiptItemsByOrderItem = $locked->receipts
                ->flatMap(function (DberpPurchaseReceiptOrder $receipt): Collection {
                    $arrivalDate = $receipt->getRawOriginal('arrival_date') ?: $receipt->business_time?->toDateString();

                    return $receipt->items->map(function ($item) use ($arrivalDate): array {
                        return [
                            'purchase_order_item_id' => (int) $item->purchase_order_item_id,
                            'base_arrival_quantity' => (string) ($item->base_arrival_quantity ?? '0'),
                            'base_received_quantity' => (string) ($item->base_received_quantity ?? '0'),
                            'arrival_date' => $arrivalDate,
                        ];
                    });
                })
                ->groupBy('purchase_order_item_id');

            foreach ($locked->items as $item) {
                $rows = collect($receiptItemsByOrderItem->get((int) $item->getKey(), []));

                $baseReceivedSum = '0.0000';
                $baseArrivedSum = '0.0000';
                $rejectBaseSum = '0.0000';

                foreach ($rows as $row) {
                    $baseReceivedSum = bcadd($baseReceivedSum, (string) ($row['base_received_quantity'] ?? '0'), 4);
                    $baseArrivedSum = bcadd($baseArrivedSum, (string) ($row['base_arrival_quantity'] ?? '0'), 4);
                    $rejectRow = bcsub((string) ($row['base_arrival_quantity'] ?? '0'), (string) ($row['base_received_quantity'] ?? '0'), 4);
                    if (bccomp($rejectRow, '0', 4) === -1) {
                        $rejectRow = '0.0000';
                    }
                    $rejectBaseSum = bcadd($rejectBaseSum, $rejectRow, 4);
                }

                $remainingBase = bcsub((string) ($item->base_quantity ?? '0'), $baseReceivedSum, 4);
                if (bccomp($remainingBase, '0', 4) === -1) {
                    $remainingBase = '0.0000';
                }

                $receivedDisplay = DberpProductUnitConversionService::baseQuantityToLineEnteredString($baseReceivedSum, $item);
                $arrivedDisplay = DberpProductUnitConversionService::baseQuantityToLineEnteredString($baseArrivedSum, $item);
                $rejectedDisplay = DberpProductUnitConversionService::baseQuantityToLineEnteredString($rejectBaseSum, $item);
                $remainingDisplay = DberpProductUnitConversionService::baseQuantityToLineEnteredString($remainingBase, $item);

                $lastArrivalDate = $rows->pluck('arrival_date')->filter()->sort()->last();
                $executionStatus = $this->resolveItemStatus($locked, (float) $baseReceivedSum, (float) $remainingBase);

                $item->forceFill([
                    'arrived_quantity' => $arrivedDisplay,
                    'rejected_quantity' => $rejectedDisplay,
                    'received_quantity' => $receivedDisplay,
                    'base_received_quantity' => $baseReceivedSum,
                    'remaining_arrival_quantity' => $remainingDisplay,
                    'is_completed' => bccomp($remainingBase, '0', 4) !== 1,
                    'last_arrival_date' => $lastArrivalDate,
                    'execution_status' => $executionStatus->value,
                ])->save();
            }

            $locked->refresh()->loadMissing('items');

            $expectedArrivalDate = $locked->items
                ->filter(fn (DberpPurchaseOrderItem $item): bool => (float) $item->remaining_arrival_quantity > 0.00005 && $item->expected_arrival_date !== null)
                ->sortBy(fn (DberpPurchaseOrderItem $item) => $item->getRawOriginal('expected_arrival_date'))
                ->first()?->expected_arrival_date?->toDateString();

            $lastArrivalDate = $locked->items
                ->filter(fn (DberpPurchaseOrderItem $item): bool => $item->last_arrival_date !== null)
                ->sortByDesc(fn (DberpPurchaseOrderItem $item) => $item->getRawOriginal('last_arrival_date'))
                ->first()?->last_arrival_date?->toDateString();

            $totalArrivedQuantity = (float) $locked->items->sum(fn (DberpPurchaseOrderItem $item): float => (float) $item->arrived_quantity);
            $totalReceivedQuantity = (float) $locked->items->sum(fn (DberpPurchaseOrderItem $item): float => (float) $item->received_quantity);
            $totalRejectedQuantity = (float) $locked->items->sum(fn (DberpPurchaseOrderItem $item): float => (float) $item->rejected_quantity);
            $remainingArrivalQuantity = (float) $locked->items->sum(fn (DberpPurchaseOrderItem $item): float => (float) $item->remaining_arrival_quantity);
            $targetStatus = $this->resolveOrderStatus($locked, $totalReceivedQuantity, $remainingArrivalQuantity);

            $before = [
                'status' => $locked->status,
                'expected_arrival_date' => $locked->getRawOriginal('expected_arrival_date') ?: null,
                'last_arrival_date' => $locked->getRawOriginal('last_arrival_date') ?: null,
                'total_arrived_quantity' => (string) $locked->total_arrived_quantity,
                'total_rejected_quantity' => (string) $locked->total_rejected_quantity,
                'remaining_arrival_quantity' => (string) $locked->remaining_arrival_quantity,
                'arrival_progress_status' => $locked->arrival_progress_status,
            ];

            $locked->forceFill([
                'status' => $targetStatus->value,
                'expected_arrival_date' => $expectedArrivalDate,
                'last_arrival_date' => $lastArrivalDate,
                'total_arrived_quantity' => number_format($totalArrivedQuantity, 4, '.', ''),
                'total_rejected_quantity' => number_format($totalRejectedQuantity, 4, '.', ''),
                'remaining_arrival_quantity' => number_format($remainingArrivalQuantity, 4, '.', ''),
                'arrival_progress_status' => $targetStatus->value,
            ])->save();

            $after = [
                'status' => $locked->status,
                'expected_arrival_date' => $expectedArrivalDate,
                'last_arrival_date' => $lastArrivalDate,
                'total_arrived_quantity' => (string) $locked->total_arrived_quantity,
                'total_rejected_quantity' => (string) $locked->total_rejected_quantity,
                'remaining_arrival_quantity' => (string) $locked->remaining_arrival_quantity,
                'arrival_progress_status' => $locked->arrival_progress_status,
            ];
        });

        if ($before !== null && $after !== null && $before !== $after && $locked !== null) {
            $this->auditService->log('dberp.purchase_order.execution_progress_updated', $locked, $before, $after);
        }
    }

    private function resolveItemStatus(DberpPurchaseOrder $order, float $receivedQuantity, float $remainingQuantity): PurchaseOrderItemExecutionStatus
    {
        return match ($order->status) {
            PurchaseOrderStatus::Voided->value => PurchaseOrderItemExecutionStatus::Voided,
            PurchaseOrderStatus::Closed->value => PurchaseOrderItemExecutionStatus::Closed,
            PurchaseOrderStatus::Draft->value => PurchaseOrderItemExecutionStatus::AwaitingRelease,
            default => $remainingQuantity <= 0.00005
                ? PurchaseOrderItemExecutionStatus::Completed
                : ($receivedQuantity > 0.00005 ? PurchaseOrderItemExecutionStatus::PartiallyArrived : PurchaseOrderItemExecutionStatus::PendingArrival),
        };
    }

    private function resolveOrderStatus(DberpPurchaseOrder $order, float $totalReceivedQuantity, float $remainingArrivalQuantity): PurchaseOrderStatus
    {
        if ($order->status === PurchaseOrderStatus::Voided->value) {
            return PurchaseOrderStatus::Voided;
        }

        if ($order->status === PurchaseOrderStatus::Closed->value) {
            return PurchaseOrderStatus::Closed;
        }

        if ($order->status === PurchaseOrderStatus::Draft->value) {
            return PurchaseOrderStatus::Draft;
        }

        if ((float) $order->total_quantity > 0.00005 && $remainingArrivalQuantity <= 0.00005) {
            return PurchaseOrderStatus::FullyReceived;
        }

        if ($totalReceivedQuantity > 0.00005) {
            return PurchaseOrderStatus::PartiallyReceived;
        }

        return PurchaseOrderStatus::Confirmed;
    }
}
