<?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\SalesFulfillLineResult;
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;

final class SalesFulfillmentExecuteHandler
{
    public function __construct(
        private readonly SalesOrderRepository $orders,
        private readonly SalesOrderLineRepository $lines,
        private readonly InventoryReservationIdempotencyRepository $reservationIdempotency,
        private readonly SalesFulfillmentConsumeRunner $consumeRunner,
        private readonly SalesOperationalObservability $obs,
    ) {}

    public function execute(
        int $tenantId,
        int $salesOrderId,
        int $lineId,
        string $quantity,
        string $idempotencyKey,
        ?string $correlationId,
        ?int $actorUserId,
    ): SalesFulfillLineResult {
        $qty = $this->positiveQty($quantity);
        [$order, $line] = $this->lockedOrderAndLine($tenantId, $salesOrderId, $lineId);
        $this->assertFulfillmentState($order);

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

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

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

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

        return $qty;
    }

    /**
     * @return array{0: SalesOrder, 1: SalesOrderLine}
     */
    private function lockedOrderAndLine(int $tenantId, int $salesOrderId, int $lineId): array
    {
        $order = $this->orders->lockByIdForTenant($tenantId, $salesOrderId);
        if ($order === null) {
            throw new SalesOperationalException('Sales order not found.', 'sales_order_not_found');
        }
        $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 assertFulfillmentState(SalesOrder $order): void
    {
        $s = SalesOrderStatusParser::fromOrder($order);
        if (SalesOrderLifecycleRules::allowsFulfillment($s)) {
            return;
        }
        throw new SalesOperationalException('Order does not allow fulfillment.', 'sales_fulfill_state_invalid');
    }

    private function replayFulfill(SalesOrderLine $line, array $payload): SalesFulfillLineResult
    {
        return new SalesFulfillLineResult(
            salesOrderLineId: (int) $line->id,
            quantityFulfilled: '0',
            reservationConsumeMovementId: (int) ($payload['movement_id'] ?? 0),
            wasReplay: true,
        );
    }
}
