<?php

namespace App\Modules\Inventory\Reservation\Services;

use App\Modules\Inventory\Reservation\DTOs\ConsumeInventoryInput;
use App\Modules\Inventory\Reservation\DTOs\ReservationApplyResult;
use App\Modules\Inventory\Reservation\Repositories\InventoryReservationIdempotencyRepository;
use App\Modules\Inventory\Reservation\Repositories\InventoryReservationMovementRepository;
use App\Modules\Inventory\Reservation\Support\ReservationAdvisoryLockKey;
use App\Modules\Inventory\Reservation\Support\ReservationEntryType;
use App\Modules\Inventory\Stock\DTOs\ApplyMovementInput;
use App\Modules\Inventory\Stock\Exceptions\InsufficientStockException;
use App\Modules\Inventory\Stock\Repositories\InventoryStockProjectionRepository;
use App\Modules\Inventory\Stock\Services\InventoryMovementApplyService;
use App\Modules\Inventory\Stock\Services\InventoryStockAdvisoryLock;
use App\Modules\Inventory\Stock\Services\StockCatalogGate;
use App\Modules\Inventory\Stock\Services\StockProjectionBalanceApplier;
use App\Modules\Inventory\Stock\Support\DecimalQuantity;
use App\Modules\Inventory\Stock\Support\InventoryStockMovementType;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

/**
 * Consume: append reservation consume, adjust reserved, then stock_out on same locked projection row.
 * Lock ordering: GET_LOCK(reservation idempotency) → FOR UPDATE projection → ledger append → stock core (no nested GET_LOCK when idempotency null).
 */
final class ConsumeInventoryService
{
    public function __construct(
        private readonly InventoryReservationMovementRepository $movements,
        private readonly InventoryReservationIdempotencyRepository $idempotency,
        private readonly InventoryStockProjectionRepository $projections,
        private readonly StockProjectionBalanceApplier $balanceApplier,
        private readonly StockCatalogGate $catalogGate,
        private readonly InventoryStockAdvisoryLock $advisoryLock,
        private readonly InventoryMovementApplyService $stockApply,
        private readonly InventoryReservationObservability $obs,
    ) {}

    public function consume(ConsumeInventoryInput $in): ReservationApplyResult
    {
        $lockKey = ReservationAdvisoryLockKey::build($in->tenantId, $in->idempotencyKey, $in->idempotencySource);

        return $this->advisoryLock->runWithLock($lockKey, function () use ($in) {
            $attempts = (int) config('inventory.transaction_attempts', 5);

            return DB::transaction(function () use ($in) {
                return $this->consumeInTransaction($in);
            }, $attempts);
        });
    }

    private function consumeInTransaction(ConsumeInventoryInput $in): ReservationApplyResult
    {
        $idem = $this->maybeIdempotent($in);
        if ($idem !== null) {
            return $idem;
        }

        $qty = trim($in->quantity);
        if (! DecimalQuantity::gtZero($qty)) {
            throw new InsufficientStockException('consume_quantity_positive');
        }

        $this->catalogGate->assertMovable(
            $in->tenantId,
            $in->warehouseId,
            $in->itemId,
            null,
            InventoryStockMovementType::StockOut,
        );

        $projection = $this->projections->lockForWarehouseItem(
            $in->tenantId,
            $in->warehouseId,
            $in->itemId,
        );

        $open = $this->movements->sumOpenQuantity($in->tenantId, $in->reservationPublicId);
        if (! DecimalQuantity::gte($open, $qty)) {
            $this->obs->reservationConflict('consume_exceeds_open', [
                'tenant_id' => $in->tenantId,
                'reservation_public_id' => $in->reservationPublicId,
            ]);
            throw new InsufficientStockException('consume_exceeds_open_reservation');
        }

        $movement = $this->movements->insert([
            'tenant_id' => $in->tenantId,
            'warehouse_id' => $in->warehouseId,
            'item_id' => $in->itemId,
            'reservation_public_id' => $in->reservationPublicId,
            'entry_type' => ReservationEntryType::Consume,
            'quantity' => $qty,
            'expires_at' => null,
            'correlation_id' => $in->correlationId ?? (string) Str::uuid(),
            'idempotency_key' => $in->idempotencyKey,
            'idempotency_source' => $in->idempotencySource,
            'source_type' => $in->sourceType,
            'source_id' => $in->sourceId,
            'actor_user_id' => $in->actorUserId,
            'metadata' => $in->metadata,
        ]);

        $this->balanceApplier->applyReservedDelta($projection, DecimalQuantity::negate($qty));

        $this->stockApply->applyStockOutWithLockedProjection(new ApplyMovementInput(
            tenantId: $in->tenantId,
            warehouseId: $in->warehouseId,
            itemId: $in->itemId,
            type: InventoryStockMovementType::StockOut,
            quantityInput: $qty,
            actorUserId: $in->actorUserId,
            correlationId: $in->correlationId,
            idempotencyKey: null,
            idempotencySource: 'default',
            unitId: null,
            occurredAt: null,
            referenceType: $in->sourceType,
            referenceId: $in->sourceId,
            metadata: array_merge($in->metadata ?? [], ['reservation_consume_movement_id' => (int) $movement->id]),
        ), $projection);

        $result = new ReservationApplyResult(
            movementId: (int) $movement->id,
            reservationPublicId: $in->reservationPublicId,
            onHandAfter: (string) $projection->on_hand_quantity,
            reservedAfter: (string) $projection->reserved_quantity,
            availableAfter: (string) $projection->available_quantity,
        );

        $this->storeIdempotency($in, $result);

        return $result;
    }

    private function maybeIdempotent(ConsumeInventoryInput $in): ?ReservationApplyResult
    {
        if ($in->idempotencyKey === null || $in->idempotencyKey === '') {
            return null;
        }
        $row = $this->idempotency->findLocked(
            $in->tenantId,
            $in->idempotencyKey,
            $in->idempotencySource,
        );
        if ($row === null) {
            return null;
        }
        $this->obs->duplicateReplay((string) $in->idempotencyKey, ['tenant_id' => $in->tenantId]);

        return ReservationApplyResult::fromIdempotencyPayload($row->result_payload);
    }

    private function storeIdempotency(ConsumeInventoryInput $in, ReservationApplyResult $result): void
    {
        if ($in->idempotencyKey === null || $in->idempotencyKey === '') {
            return;
        }
        $this->idempotency->store(
            $in->tenantId,
            $in->idempotencyKey,
            $in->idempotencySource,
            $result->movementId,
            $result->toIdempotencyPayload(),
        );
    }
}
