<?php

namespace App\Modules\Sales\Services;

use App\Modules\Inventory\Reservation\Repositories\InventoryReservationIdempotencyRepository;
use App\Modules\Inventory\Stock\Support\DecimalQuantity;
use App\Modules\Sales\DTOs\SalesAllocateLineResult;
use App\Modules\Sales\Exceptions\SalesOperationalException;
use App\Modules\Sales\Models\SalesOrder;
use App\Modules\Sales\Models\SalesOrderLine;
use App\Modules\Sales\Repositories\SalesOrderLineRepository;
use App\Modules\Sales\Repositories\SalesOrderRepository;
use App\Modules\Sales\Support\SalesIdempotencySources;
use App\Modules\Sales\Support\SalesOrderLifecycleStatus;

final class SalesReservationAllocateHandler
{
    public function __construct(
        private readonly SalesOrderRepository $orders,
        private readonly SalesOrderLineRepository $lines,
        private readonly InventoryReservationIdempotencyRepository $reservationIdempotency,
        private readonly SalesReservationApplyExecutor $applyExecutor,
        private readonly SalesOperationalObservability $obs,
    ) {}

    public function execute(
        int $tenantId,
        int $salesOrderId,
        int $lineId,
        string $quantity,
        string $idempotencyKey,
        ?string $correlationId,
        ?int $actorUserId,
    ): SalesAllocateLineResult {
        $qty = $this->assertPositiveQty($quantity);
        [$order, $line] = $this->lockOrderAndLine($tenantId, $salesOrderId, $lineId);
        $this->assertAllocationState(SalesOrderStatusParser::fromOrder($order));

        $replay = $this->reservationIdempotency->findLocked(
            $tenantId,
            $idempotencyKey,
            SalesIdempotencySources::Allocation,
        );
        if ($replay !== null) {
            $this->obs->duplicateAllocationReplay(SalesOutboundEventContext::orderLine($tenantId, $order, $line, $correlationId));

            return $this->replayResult($line, $replay->result_payload);
        }

        return $this->applyExecutor->apply($tenantId, $order, $line, $qty, $idempotencyKey, $correlationId, $actorUserId);
    }

    private function assertPositiveQty(string $quantity): string
    {
        $qty = trim($quantity);
        if (! DecimalQuantity::gtZero($qty)) {
            throw new SalesOperationalException('Allocation quantity must be positive.', 'sales_allocate_qty_invalid');
        }

        return $qty;
    }

    /**
     * @return array{0: SalesOrder, 1: SalesOrderLine}
     */
    private function lockOrderAndLine(int $tenantId, int $salesOrderId, int $lineId): array
    {
        $order = $this->lockOrder($tenantId, $salesOrderId);
        $line = $this->lines->lockLineForTenant($tenantId, $lineId);
        if ($line === null || (int) $line->sales_order_id !== $salesOrderId) {
            throw new SalesOperationalException('Sales order line not found.', 'sales_line_not_found');
        }

        return [$order, $line];
    }

    private function assertAllocationState(SalesOrderLifecycleStatus $status): void
    {
        if (! SalesOrderLifecycleRules::allowsAllocation($status)) {
            throw new SalesOperationalException('Order does not allow allocation.', 'sales_allocate_state_invalid');
        }
    }

    private function replayResult(SalesOrderLine $line, array $payload): SalesAllocateLineResult
    {
        $rid = (string) ($payload['reservation_public_id'] ?? $line->reservation_public_id ?? '');

        return new SalesAllocateLineResult(
            salesOrderLineId: (int) $line->id,
            quantityReservedDelta: '0',
            reservationPublicId: $rid,
            wasReplay: true,
        );
    }

    private function lockOrder(int $tenantId, int $salesOrderId): SalesOrder
    {
        $order = $this->orders->lockByIdForTenant($tenantId, $salesOrderId);
        if ($order === null) {
            throw new SalesOperationalException('Sales order not found.', 'sales_order_not_found');
        }

        return $order;
    }
}
