<?php

namespace App\Modules\Sales\Services;

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

final class SalesFulfillmentConsumeRunner
{
    public function __construct(
        private readonly ConsumeInventoryService $consume,
        private readonly SalesFulfillmentLedgerGuard $ledger,
        private readonly SalesOrderLifecycleWriter $statusWriter,
        private readonly SalesOperationalObservability $obs,
    ) {}

    public function run(
        int $tenantId,
        SalesOrder $order,
        SalesOrderLine $line,
        string $qty,
        string $idempotencyKey,
        ?string $correlationId,
        ?int $actorUserId,
    ): SalesFulfillLineResult {
        $rid = $this->requireReservationId($tenantId, $order, $line, $correlationId);
        $this->assertFulfillable($tenantId, $order, $line, $qty, $correlationId);
        $this->ledger->assertOpenMatchesLine($tenantId, $order, $line, $rid, $correlationId);
        $out = $this->invokeConsume($tenantId, $order, $line, $rid, $qty, $idempotencyKey, $correlationId, $actorUserId);

        return $this->persistFulfilled($order, $line, $qty, $out->movementId);
    }

    private function requireReservationId(
        int $tenantId,
        SalesOrder $order,
        SalesOrderLine $line,
        ?string $correlationId,
    ): string {
        $rid = trim((string) $line->reservation_public_id);
        if ($rid !== '') {
            return $rid;
        }
        $this->obs->fulfillmentFailure(
            'sales_fulfill_no_reservation',
            'Missing reservation.',
            SalesOutboundEventContext::orderLine($tenantId, $order, $line, $correlationId),
        );
        throw new SalesOperationalException('Fulfillment requires reservation.', 'sales_fulfill_no_reservation');
    }

    private function assertFulfillable(
        int $tenantId,
        SalesOrder $order,
        SalesOrderLine $line,
        string $qty,
        ?string $correlationId,
    ): void {
        $cap = SalesLineRemainingCalculator::fulfillableFromReservation($line);
        if (DecimalQuantity::gte($cap, $qty)) {
            return;
        }
        $this->obs->fulfillmentFailure(
            'sales_fulfill_qty_exceeds_reserved',
            'Quantity exceeds reserved.',
            SalesOutboundEventContext::orderLine($tenantId, $order, $line, $correlationId),
        );
        throw new SalesOperationalException('Fulfillment exceeds reserved quantity.', 'sales_fulfill_qty_exceeds_reserved');
    }

    private function invokeConsume(
        int $tenantId,
        SalesOrder $order,
        SalesOrderLine $line,
        string $rid,
        string $qty,
        string $idempotencyKey,
        ?string $correlationId,
        ?int $actorUserId,
    ): ReservationApplyResult {
        $in = $this->consumeInput($tenantId, $order, $line, $rid, $qty, $idempotencyKey, $correlationId, $actorUserId);
        try {
            return $this->consume->consume($in);
        } catch (InsufficientStockException $e) {
            $this->obs->fulfillmentFailure(
                'consume_exceeds_open_reservation',
                $e->getMessage(),
                SalesOutboundEventContext::orderLine($tenantId, $order, $line, $correlationId),
            );
            throw $e;
        }
    }

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

    private function persistFulfilled(
        SalesOrder $order,
        SalesOrderLine $line,
        string $qty,
        int $consumeMovementId,
    ): SalesFulfillLineResult {
        $newReserved = DecimalQuantity::add(trim((string) $line->quantity_reserved), DecimalQuantity::negate($qty));
        $newFulfilled = DecimalQuantity::add(trim((string) $line->quantity_fulfilled), $qty);
        $line->forceFill([
            'quantity_reserved' => $newReserved,
            'quantity_fulfilled' => $newFulfilled,
        ])->save();
        $this->statusWriter->persistAggregatedStatus($order);

        return new SalesFulfillLineResult(
            salesOrderLineId: (int) $line->id,
            quantityFulfilled: $qty,
            reservationConsumeMovementId: $consumeMovementId,
            wasReplay: false,
        );
    }

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

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