<?php

namespace App\Modules\Inventory\Stock\Services;

use App\Modules\Inventory\Models\Item;
use App\Modules\Inventory\Stock\DTOs\ApplyMovementInput;
use App\Modules\Inventory\Stock\DTOs\ApplyMovementResult;
use App\Modules\Inventory\Stock\Exceptions\InsufficientStockException;
use App\Modules\Inventory\Stock\Models\InventoryStockProjection;
use App\Modules\Inventory\Stock\Repositories\InventoryStockIdempotencyRepository;
use App\Modules\Inventory\Stock\Repositories\InventoryStockMovementRepository;
use App\Modules\Inventory\Stock\Repositories\InventoryStockProjectionRepository;
use App\Modules\Inventory\Stock\Support\DecimalQuantity;
use App\Modules\Inventory\Stock\Support\InventoryStockMovementType;
use Illuminate\Support\Str;

/**
 * Shared movement application (projection row must be locked when $lockedProjection is passed).
 */
final class InventoryStockMovementApplyCore
{
    public function __construct(
        private readonly InventoryStockMovementRepository $movements,
        private readonly InventoryStockProjectionRepository $projections,
        private readonly InventoryStockIdempotencyRepository $idempotency,
        private readonly StockCatalogGate $catalogGate,
        private readonly StockProjectionBalanceApplier $balanceApplier,
        private readonly InventoryStockObservability $obs,
    ) {}

    public function execute(ApplyMovementInput $in, ?InventoryStockProjection $lockedProjection): ApplyMovementResult
    {
        $existing = $this->maybeReturnIdempotent($in);
        if ($existing !== null) {
            if ($in->idempotencyKey !== null && $in->idempotencyKey !== '') {
                $this->obs->duplicateReplay((string) $in->idempotencyKey, [
                    'tenant_id' => $in->tenantId,
                    'correlation_id' => $in->correlationId,
                ]);
            }

            return $existing;
        }

        $this->catalogGate->assertMovable(
            $in->tenantId,
            $in->warehouseId,
            $in->itemId,
            $in->unitId,
            $in->type,
        );

        $signed = SignedMovementQuantity::ledgerSignedQuantity($in);
        $projection = $lockedProjection ?? $this->projections->lockForWarehouseItem(
            $in->tenantId,
            $in->warehouseId,
            $in->itemId,
        );
        $this->assertLockedProjectionMatches($in, $projection);

        $this->guardStockOut($in->type, $projection, $signed);

        $unitId = $in->unitId ?? (int) Item::withoutGlobalScopes()
            ->where('tenant_id', $in->tenantId)
            ->whereKey($in->itemId)
            ->value('default_unit_id');

        $attrs = [
            'tenant_id' => $in->tenantId,
            'warehouse_id' => $in->warehouseId,
            'item_id' => $in->itemId,
            'warehouse_section_id' => null,
            'movement_type' => $in->type->value,
            'quantity' => $signed,
            'unit_id' => $unitId,
            'occurred_at' => $in->occurredAtOrNow(),
            'actor_user_id' => $in->actorUserId,
            'correlation_id' => $in->correlationId ?? (string) Str::uuid(),
            'idempotency_key' => $in->idempotencyKey,
            'idempotency_source' => $in->idempotencySource,
            'reference_type' => $in->referenceType,
            'reference_id' => $in->referenceId,
            'metadata' => $in->metadata,
        ];
        if ($in->reversesMovementId !== null) {
            $attrs['reverses_movement_id'] = $in->reversesMovementId;
        }
        $movement = $this->movements->insert($attrs);

        $this->balanceApplier->applySignedDelta($projection, $signed, (int) $movement->id);
        $this->projections->save($projection);

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

        if ($in->idempotencyKey !== null && $in->idempotencyKey !== '') {
            $this->idempotency->store(
                $in->tenantId,
                $in->idempotencyKey,
                $in->idempotencySource,
                (int) $movement->id,
                $result->toIdempotencyPayload(),
            );
        }

        return $result;
    }

    private function assertLockedProjectionMatches(
        ApplyMovementInput $in,
        InventoryStockProjection $p,
    ): void {
        if ((int) $p->tenant_id !== $in->tenantId
            || (int) $p->warehouse_id !== $in->warehouseId
            || (int) $p->item_id !== $in->itemId) {
            throw new \RuntimeException('inventory_stock_projection_lock_mismatch');
        }
    }

    private function maybeReturnIdempotent(ApplyMovementInput $in): ?ApplyMovementResult
    {
        if ($in->idempotencyKey === null || $in->idempotencyKey === '') {
            return null;
        }
        $row = $this->idempotency->findLocked(
            $in->tenantId,
            $in->idempotencyKey,
            $in->idempotencySource,
        );
        if ($row === null) {
            return null;
        }

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

    private function guardStockOut(
        InventoryStockMovementType $type,
        InventoryStockProjection $projection,
        string $signed,
    ): void {
        if ($type !== InventoryStockMovementType::StockOut && $type !== InventoryStockMovementType::TransferOut) {
            return;
        }
        $qtyOut = DecimalQuantity::negate($signed);
        if (! config('inventory.allow_negative_on_hand', false)) {
            if (! DecimalQuantity::gte((string) $projection->available_quantity, $qtyOut)) {
                throw new InsufficientStockException('insufficient_available_for_out');
            }
            $newOnHand = DecimalQuantity::add((string) $projection->on_hand_quantity, $signed);
            if (DecimalQuantity::isNegative($newOnHand)) {
                throw new InsufficientStockException('insufficient_stock_for_out');
            }
        }
    }
}
