<?php

namespace App\Modules\Inventory\Stock\Services;

use App\Modules\Inventory\Stock\DTOs\ApplyMovementInput;
use App\Modules\Inventory\Stock\Exceptions\InventoryTransferOperationalException;
use App\Modules\Inventory\Stock\Models\InventoryStockProjection;
use App\Modules\Inventory\Stock\Models\InventoryStockTransfer;
use App\Modules\Inventory\Stock\Repositories\InventoryStockProjectionRepository;
use App\Modules\Inventory\Stock\Repositories\InventoryStockTransferRepository;
use App\Modules\Inventory\Stock\Support\InventoryStockMovementType;
use App\Modules\Inventory\Stock\Support\InventoryStockTransferLifecycleRules;
use App\Modules\Inventory\Stock\Support\InventoryStockTransferStatus;
use App\Modules\Inventory\Stock\Support\DecimalQuantity;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

final class InventoryStockTransferExecuteService
{
    public function __construct(
        private readonly InventoryStockTransferRepository $transfers,
        private readonly InventoryStockProjectionRepository $projections,
        private readonly InventoryMovementApplyService $movements,
    ) {}

    public function executeApproved(
        int $tenantId,
        int $transferId,
        ?int $actorUserId,
        ?string $correlationId,
    ): void {
        $attempts = (int) config('inventory.transaction_attempts', 5);
        DB::transaction(function () use (
            $tenantId,
            $transferId,
            $actorUserId,
            $correlationId,
        ): void {
            $xfer = $this->transfers->lockByIdForTenant($tenantId, $transferId);
            if ($xfer === null) {
                throw new InventoryTransferOperationalException('Transfer not found.', 'transfer_not_found');
            }
            $cur = $this->statusEnum($xfer);
            if ($cur === InventoryStockTransferStatus::Completed) {
                return;
            }
            if (! InventoryStockTransferLifecycleRules::allowsExecute($cur)) {
                throw new InventoryTransferOperationalException('Execute not allowed.', 'transfer_execute_invalid');
            }
            $qty = trim((string) $xfer->quantity);
            if (! DecimalQuantity::gtZero($qty)) {
                throw new InventoryTransferOperationalException('Transfer quantity invalid.', 'transfer_qty_invalid');
            }
            $srcWh = (int) $xfer->source_warehouse_id;
            $dstWh = (int) $xfer->destination_warehouse_id;
            $itemId = (int) $xfer->item_id;
            if ($srcWh === $dstWh) {
                throw new InventoryTransferOperationalException('Source and destination must differ.', 'transfer_same_warehouse');
            }
            $cid = ($correlationId !== null && $correlationId !== '') ? $correlationId : (string) Str::uuid();
            $xid = (int) $xfer->id;
            [$srcProj, $dstProj] = $this->lockProjectionsOrdered($tenantId, $srcWh, $dstWh, $itemId);
            $idemOut = 'xfer-'.$xid.'-transfer-out';
            $idemIn = 'xfer-'.$xid.'-transfer-in';
            $baseMeta = ['inventory_stock_transfer_id' => $xid];
            $out = $this->movements->applyLedgerWithLockedProjection(new ApplyMovementInput(
                tenantId: $tenantId,
                warehouseId: $srcWh,
                itemId: $itemId,
                type: InventoryStockMovementType::TransferOut,
                quantityInput: $qty,
                actorUserId: $actorUserId,
                correlationId: $cid,
                idempotencyKey: $idemOut,
                idempotencySource: 'inventory.transfer.execute',
                referenceType: 'inventory_stock_transfer',
                referenceId: $xid,
                metadata: $baseMeta + [
                    'leg' => 'transfer_out',
                    'counterparty_warehouse_id' => $dstWh,
                ],
            ), $srcProj);

            $in = $this->movements->applyLedgerWithLockedProjection(new ApplyMovementInput(
                tenantId: $tenantId,
                warehouseId: $dstWh,
                itemId: $itemId,
                type: InventoryStockMovementType::TransferIn,
                quantityInput: $qty,
                actorUserId: $actorUserId,
                correlationId: $cid,
                idempotencyKey: $idemIn,
                idempotencySource: 'inventory.transfer.execute',
                referenceType: 'inventory_stock_transfer',
                referenceId: $xid,
                metadata: $baseMeta + [
                    'leg' => 'transfer_in',
                    'counterparty_warehouse_id' => $srcWh,
                    'paired_transfer_out_movement_id' => $out->movementId,
                ],
            ), $dstProj);

            $xfer->forceFill([
                'status' => InventoryStockTransferStatus::Completed->value,
                'correlation_id' => $cid,
                'transfer_out_movement_id' => $out->movementId,
                'transfer_in_movement_id' => $in->movementId,
                'completed_at' => now(),
            ]);
            $this->transfers->save($xfer);
        }, $attempts);
    }

    /**
     * @return array{0: InventoryStockProjection, 1: InventoryStockProjection}
     */
    private function lockProjectionsOrdered(
        int $tenantId,
        int $sourceWarehouseId,
        int $destinationWarehouseId,
        int $itemId,
    ): array {
        $pairs = [['w' => $sourceWarehouseId, 'role' => 'src'], ['w' => $destinationWarehouseId, 'role' => 'dst']];
        usort($pairs, fn (array $a, array $b): int => $a['w'] <=> $b['w']);
        $map = [];
        foreach ($pairs as $pair) {
            $map[$pair['w']] = $this->projections->lockForWarehouseItem($tenantId, $pair['w'], $itemId);
        }

        return [$map[$sourceWarehouseId], $map[$destinationWarehouseId]];
    }

    private function statusEnum(InventoryStockTransfer $t): InventoryStockTransferStatus
    {
        $s = $t->status instanceof InventoryStockTransferStatus
            ? $t->status
            : InventoryStockTransferStatus::tryFrom((string) $t->status);
        if ($s === null) {
            throw new InventoryTransferOperationalException('Unknown transfer status.', 'transfer_bad_status');
        }

        return $s;
    }
}
