<?php

namespace App\Modules\Inventory\Stock\Services;

use App\Modules\Inventory\Stock\Models\InventoryStockProjection;
use App\Modules\Inventory\Stock\Support\DecimalQuantity;

final class StockProjectionBalanceApplier
{
    public function __construct(
        private readonly InventoryStockObservability $obs,
    ) {}

    public function applySignedDelta(
        InventoryStockProjection $p,
        string $signedLedgerQuantity,
        int $movementId,
    ): void {
        $onHand = DecimalQuantity::add((string) $p->on_hand_quantity, $signedLedgerQuantity);
        $reserved = (string) $p->reserved_quantity;
        $available = DecimalQuantity::add($onHand, DecimalQuantity::negate($reserved));

        $strict = ! config('inventory.allow_negative_on_hand', false);
        if ($strict) {
            if (DecimalQuantity::isNegative($onHand)) {
                throw new \App\Modules\Inventory\Stock\Exceptions\InsufficientStockException('negative_on_hand_disallowed');
            }
            if (! DecimalQuantity::gte($available, '0')) {
                throw new \App\Modules\Inventory\Stock\Exceptions\InsufficientStockException('negative_available');
            }
        }

        if (! DecimalQuantity::gte($reserved, '0')) {
            $this->obs->invariantViolation('reserved_negative', [
                'tenant_id' => (int) $p->tenant_id,
                'projection_id' => $p->id,
            ]);
            throw new \RuntimeException('reserved_quantity_invariant');
        }

        $p->on_hand_quantity = $onHand;
        $p->reserved_quantity = $reserved;
        $p->available_quantity = $available;
        $p->last_movement_id = $movementId;
        $p->row_version = (int) $p->row_version + 1;
    }

    /**
     * Apply a signed delta to reserved_quantity only; recomputes available from on_hand.
     * Does not update last_movement_id (stock ledger cursor remains stock-only).
     */
    public function applyReservedDelta(InventoryStockProjection $p, string $signedReservedDelta): void
    {
        $onHand = (string) $p->on_hand_quantity;
        $reserved = DecimalQuantity::add((string) $p->reserved_quantity, $signedReservedDelta);
        $available = DecimalQuantity::add($onHand, DecimalQuantity::negate($reserved));

        $strict = ! config('inventory.allow_negative_on_hand', false);
        if ($strict) {
            if (! DecimalQuantity::gte($reserved, '0')) {
                throw new \App\Modules\Inventory\Stock\Exceptions\InsufficientStockException('negative_reserved_disallowed');
            }
            if (! DecimalQuantity::gte($available, '0')) {
                throw new \App\Modules\Inventory\Stock\Exceptions\InsufficientStockException('negative_available');
            }
        }

        if (! DecimalQuantity::gte($reserved, '0')) {
            $this->obs->invariantViolation('reserved_negative', [
                'tenant_id' => (int) $p->tenant_id,
                'projection_id' => $p->id,
            ]);
            throw new \RuntimeException('reserved_quantity_invariant');
        }

        $p->reserved_quantity = $reserved;
        $p->available_quantity = $available;
        $p->row_version = (int) $p->row_version + 1;
    }
}
