<?php

namespace App\Domains\Catalog\Services;

use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\StockReservation;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Purchase\Models\Purchase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

class StockReservationService
{
    public function __construct(
        private readonly CatalogInventoryService $inventory,
    ) {}

    public function reserve(CartItem $cartItem, CatalogItem|Variant $selection, int $quantity): void
    {
        DB::transaction(function () use ($cartItem, $selection, $quantity): void {
            $this->inventory->reserve($selection, $quantity);
            $this->recordIncrease($cartItem, $selection, $quantity);
        });
    }

    public function release(
        CartItem $cartItem,
        CatalogItem|Variant $selection,
        int $quantity,
        string $releasedStatus = StockReservation::STATUS_RELEASED,
    ): void {
        DB::transaction(function () use ($cartItem, $selection, $quantity, $releasedStatus): void {
            $this->ensure($cartItem, $selection);
            $this->inventory->release($selection, $quantity);
            $this->recordDecrease($cartItem, $selection, $quantity, $releasedStatus);
        });
    }

    public function commit(
        CartItem $cartItem,
        CatalogItem|Variant $selection,
        Purchase $purchase,
    ): void {
        DB::transaction(function () use ($cartItem, $selection, $purchase): void {
            $this->ensure($cartItem, $selection);
            $this->inventory->commit($selection, (int) $cartItem->cantidad);

            $requirements = $this->inventory->requirementsFor($selection, (int) $cartItem->cantidad);
            foreach ($requirements as $inventoryId => $quantity) {
                $reservation = $this->lockReservation($cartItem, $inventoryId);
                if (
                    $reservation === null
                    || $reservation->status !== StockReservation::STATUS_ACTIVE
                    || $reservation->purchase_id !== $purchase->getKey()
                    || $reservation->quantity !== $quantity
                ) {
                    throw new \InvalidArgumentException('La reserva de stock no coincide con el item del carrito.');
                }

                $reservation->update([
                    'status' => StockReservation::STATUS_COMMITTED,
                    'committed_at' => now(),
                    'expires_at' => null,
                ]);
            }
        });
    }

    public function ensure(CartItem $cartItem, CatalogItem|Variant $selection): void
    {
        $requirements = $this->inventory->requirementsFor($selection, (int) $cartItem->cantidad);

        foreach ($requirements as $inventoryId => $quantity) {
            $reservation = $this->lockReservation($cartItem, $inventoryId);

            if ($reservation === null) {
                StockReservation::query()->create([
                    'inventory_id' => $inventoryId,
                    'cart_item_id' => $cartItem->getKey(),
                    'quantity' => $quantity,
                    'status' => StockReservation::STATUS_ACTIVE,
                    'expires_at' => $this->expiration(),
                ]);

                continue;
            }

            if ($reservation->status !== StockReservation::STATUS_ACTIVE || $reservation->quantity !== $quantity) {
                $reservation->update([
                    'quantity' => $quantity,
                    'status' => StockReservation::STATUS_ACTIVE,
                    'committed_at' => null,
                    'released_at' => null,
                    'expires_at' => $this->expiration(),
                ]);
            }
        }
    }

    public function attachToPurchase(
        CartItem $cartItem,
        CatalogItem|Variant $selection,
        Purchase $purchase,
    ): void {
        DB::transaction(function () use ($cartItem, $selection, $purchase): void {
            $this->ensure($cartItem, $selection);
            StockReservation::query()
                ->where('cart_item_id', $cartItem->getKey())
                ->where('status', StockReservation::STATUS_ACTIVE)
                ->update([
                    'purchase_id' => $purchase->getKey(),
                    'expires_at' => $purchase->expires_at,
                ]);
        });
    }

    public function detachFromPurchase(Purchase $purchase): void
    {
        StockReservation::query()
            ->where('purchase_id', $purchase->getKey())
            ->where('status', StockReservation::STATUS_ACTIVE)
            ->update([
                'purchase_id' => null,
                'expires_at' => $this->expiration(),
            ]);
    }

    public function restore(CartItem $cartItem, CatalogItem|Variant $selection): void
    {
        DB::transaction(function () use ($cartItem, $selection): void {
            $requirements = $this->inventory->requirementsFor(
                $selection,
                (int) $cartItem->cantidad,
            );
            $activeReservations = StockReservation::query()
                ->where('cart_item_id', $cartItem->getKey())
                ->where('status', StockReservation::STATUS_ACTIVE)
                ->lockForUpdate()
                ->get()
                ->keyBy('inventory_id');

            $hasCompleteReservation = collect($requirements)->every(
                fn (int $quantity, int $inventoryId): bool => (int) ($activeReservations->get($inventoryId)?->quantity ?? 0) === $quantity,
            );

            if ($hasCompleteReservation) {
                return;
            }

            if ($activeReservations->isNotEmpty()) {
                throw new \InvalidArgumentException('La reserva de stock del carrito es inconsistente.');
            }

            $this->inventory->reserve($selection, (int) $cartItem->cantidad);
            $this->recordIncrease($cartItem, $selection, (int) $cartItem->cantidad);
        });
    }

    public function syncPurchaseExpiration(Purchase $purchase): void
    {
        StockReservation::query()
            ->where('purchase_id', $purchase->getKey())
            ->where('status', StockReservation::STATUS_ACTIVE)
            ->update(['expires_at' => $purchase->expires_at]);
    }

    public function transfer(CartItem $source, CartItem $target): void
    {
        DB::transaction(function () use ($source, $target): void {
            $sourceReservations = StockReservation::query()
                ->where('cart_item_id', $source->getKey())
                ->where('status', StockReservation::STATUS_ACTIVE)
                ->orderBy('inventory_id')
                ->lockForUpdate()
                ->get();

            foreach ($sourceReservations as $sourceReservation) {
                $targetReservation = $this->lockReservation($target, (int) $sourceReservation->inventory_id);

                if ($targetReservation === null) {
                    $sourceItemQuantity = (int) $source->cantidad;
                    $targetItemQuantity = (int) $target->fresh()->cantidad;
                    $perItemQuantity = intdiv((int) $sourceReservation->quantity, $sourceItemQuantity);
                    $sourceReservation->update([
                        'cart_item_id' => $target->getKey(),
                        'purchase_id' => null,
                        'quantity' => $perItemQuantity * $targetItemQuantity,
                        'expires_at' => $this->expiration(),
                    ]);

                    continue;
                }

                $targetReservation->update([
                    'quantity' => $targetReservation->quantity + $sourceReservation->quantity,
                    'status' => StockReservation::STATUS_ACTIVE,
                    'expires_at' => $this->expiration(),
                ]);
                $sourceReservation->delete();
            }
        });
    }

    private function recordIncrease(CartItem $cartItem, CatalogItem|Variant $selection, int $quantity): void
    {
        foreach ($this->inventory->requirementsFor($selection, $quantity) as $inventoryId => $requiredQuantity) {
            $reservation = $this->lockReservation($cartItem, $inventoryId);

            if ($reservation === null) {
                StockReservation::query()->create([
                    'inventory_id' => $inventoryId,
                    'cart_item_id' => $cartItem->getKey(),
                    'quantity' => $requiredQuantity,
                    'status' => StockReservation::STATUS_ACTIVE,
                    'expires_at' => $this->expiration(),
                ]);

                continue;
            }

            $reservation->update([
                'quantity' => ($reservation->status === StockReservation::STATUS_ACTIVE ? $reservation->quantity : 0) + $requiredQuantity,
                'status' => StockReservation::STATUS_ACTIVE,
                'committed_at' => null,
                'released_at' => null,
                'expires_at' => $this->expiration(),
            ]);
        }
    }

    private function recordDecrease(
        CartItem $cartItem,
        CatalogItem|Variant $selection,
        int $quantity,
        string $releasedStatus,
    ): void {
        foreach ($this->inventory->requirementsFor($selection, $quantity) as $inventoryId => $requiredQuantity) {
            $reservation = $this->lockReservation($cartItem, $inventoryId);
            if ($reservation === null || $reservation->status !== StockReservation::STATUS_ACTIVE || $reservation->quantity < $requiredQuantity) {
                throw new \InvalidArgumentException('La reserva de stock no alcanza para liberar la cantidad solicitada.');
            }

            $remaining = $reservation->quantity - $requiredQuantity;
            $reservation->update([
                'quantity' => $remaining,
                'status' => $remaining === 0 ? $releasedStatus : StockReservation::STATUS_ACTIVE,
                'released_at' => $remaining === 0 ? now() : null,
                'expires_at' => $remaining === 0 ? null : $reservation->expires_at,
            ]);
        }
    }

    private function lockReservation(CartItem $cartItem, int $inventoryId): ?StockReservation
    {
        return StockReservation::query()
            ->where('cart_item_id', $cartItem->getKey())
            ->where('inventory_id', $inventoryId)
            ->lockForUpdate()
            ->first();
    }

    private function expiration(): Carbon
    {
        return now()->addMinutes(
            max(1, (int) config('catalog.stock_reservation_expiration_minutes', 30)),
        );
    }
}
