<?php

namespace App\Modules\Inventory\Stock\Services;

use App\Modules\Inventory\Stock\DTOs\ApplyMovementInput;
use App\Modules\Inventory\Stock\DTOs\ApplyMovementResult;
use App\Modules\Inventory\Stock\Models\InventoryStockProjection;
use App\Modules\Inventory\Stock\Support\InventoryStockMovementType;
use Illuminate\Support\Facades\DB;

final class InventoryMovementApplyService
{
    public function __construct(
        private readonly InventoryStockMovementApplyCore $core,
        private readonly InventoryStockAdvisoryLock $advisoryLock,
    ) {}

    public function apply(ApplyMovementInput $in): ApplyMovementResult
    {
        $lockKey = $this->advisoryLockKey($in);

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

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

    /**
     * Caller must hold an active DB transaction and have locked $projection FOR UPDATE.
     * Stock idempotency key should be null when reservation idempotency already covers the unit of work.
     */
    public function applyStockOutWithLockedProjection(
        ApplyMovementInput $in,
        InventoryStockProjection $projection,
    ): ApplyMovementResult {
        if ($in->type !== InventoryStockMovementType::StockOut) {
            throw new \InvalidArgumentException('applyStockOutWithLockedProjection_requires_stock_out');
        }

        return $this->core->execute($in, $projection);
    }

    /**
     * Ledger apply when the caller holds an enclosing DB transaction and a locked projection.
     * Bypasses advisory lock + outer transactions (pairs transfer_out / transfer_in).
     */
    public function applyLedgerWithLockedProjection(
        ApplyMovementInput $in,
        InventoryStockProjection $projection,
    ): ApplyMovementResult {
        return $this->core->execute($in, $projection);
    }

    private function advisoryLockKey(ApplyMovementInput $in): ?string
    {
        $k = $in->idempotencyKey;
        if ($k === null || $k === '') {
            return null;
        }

        return $in->tenantId.':'.$in->idempotencySource.':'.$k;
    }
}
