<?php

namespace App\Modules\Sales\Services;

use App\Modules\Inventory\Reservation\DTOs\ReserveInventoryInput;
use App\Modules\Inventory\Reservation\Services\ReserveInventoryService;
use App\Modules\Inventory\Stock\Exceptions\InsufficientStockException;
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\Support\SalesIdempotencySources;
use Illuminate\Support\Str;

final class SalesReservationApplyExecutor
{
    public function __construct(
        private readonly ReserveInventoryService $reserve,
        private readonly SalesOrderLifecycleWriter $statusWriter,
        private readonly SalesOperationalObservability $obs,
    ) {}

    public function apply(
        int $tenantId,
        SalesOrder $order,
        SalesOrderLine $line,
        string $qty,
        string $idempotencyKey,
        ?string $correlationId,
        ?int $actorUserId,
    ): SalesAllocateLineResult {
        $this->guardOverAllocation($tenantId, $order, $line, $qty, $correlationId);
        $rid = $this->reservationPublicId($line);
        $cid = $this->resolveCorrelation($correlationId, $order);
        $this->invokeReserve($tenantId, $order, $line, $qty, $rid, $cid, $idempotencyKey, $actorUserId, $correlationId);

        return $this->persistAfterReserve($order, $line, $qty, $rid);
    }

    private function guardOverAllocation(
        int $tenantId,
        SalesOrder $order,
        SalesOrderLine $line,
        string $qty,
        ?string $correlationId,
    ): void {
        $remaining = SalesLineRemainingCalculator::allocatableRemaining($line);
        if (DecimalQuantity::gte($remaining, $qty)) {
            return;
        }
        $this->obs->overAllocationAttempt(SalesOutboundEventContext::orderLine($tenantId, $order, $line, $correlationId) + [
            'requested' => $qty,
            'remaining_allocatable' => $remaining,
        ]);
        throw new SalesOperationalException('Over-allocation blocked.', 'sales_over_allocation');
    }

    private function invokeReserve(
        int $tenantId,
        SalesOrder $order,
        SalesOrderLine $line,
        string $qty,
        string $rid,
        string $cid,
        string $idempotencyKey,
        ?int $actorUserId,
        ?string $correlationId,
    ): void {
        $in = $this->reserveInput($tenantId, $order, $line, $qty, $rid, $cid, $idempotencyKey, $actorUserId);
        try {
            $this->reserve->reserve($in);
        } catch (InsufficientStockException $e) {
            $this->obs->allocationFailure(
                'insufficient_available_to_reserve',
                $e->getMessage(),
                SalesOutboundEventContext::orderLine($tenantId, $order, $line, $correlationId),
            );
            throw $e;
        }
    }

    private function reserveInput(
        int $tenantId,
        SalesOrder $order,
        SalesOrderLine $line,
        string $qty,
        string $rid,
        string $cid,
        string $idempotencyKey,
        ?int $actorUserId,
    ): ReserveInventoryInput {
        return new ReserveInventoryInput(
            tenantId: $tenantId,
            warehouseId: (int) $line->warehouse_id,
            itemId: (int) $line->item_id,
            quantity: $qty,
            reservationPublicId: $rid,
            expiresAt: null,
            actorUserId: $actorUserId,
            correlationId: $cid,
            idempotencyKey: $idempotencyKey,
            idempotencySource: SalesIdempotencySources::Allocation,
            sourceType: 'sales_order_line',
            sourceId: (int) $line->id,
            metadata: [
                'sales_order_id' => (int) $order->id,
                'sales_order_line_id' => (int) $line->id,
            ],
        );
    }

    private function persistAfterReserve(
        SalesOrder $order,
        SalesOrderLine $line,
        string $qty,
        string $rid,
    ): SalesAllocateLineResult {
        $newReserved = DecimalQuantity::add(trim((string) $line->quantity_reserved), $qty);
        $line->forceFill([
            'quantity_reserved' => $newReserved,
            'reservation_public_id' => $rid,
        ])->save();
        $this->statusWriter->persistAggregatedStatus($order);

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

    private function reservationPublicId(SalesOrderLine $line): string
    {
        $existing = (string) $line->reservation_public_id;

        return $existing !== '' ? $existing : (string) Str::uuid();
    }

    private function resolveCorrelation(?string $correlationId, SalesOrder $order): string
    {
        if ($correlationId !== null && $correlationId !== '') {
            return $correlationId;
        }
        $oc = (string) $order->correlation_id;

        return $oc !== '' ? $oc : (string) Str::uuid();
    }
}
