<?php

namespace App\Domains\Catalog\Services;

use App\Domains\Cart\Models\Cart;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Exceptions\StockReservationExpiredException;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\StockReservation;
use App\Domains\Catalog\Models\StockReservationLine;
use App\Domains\Purchase\Models\Purchase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class StockReservationService
{
    public const REASON_CART_EMPTY = 'cart_empty';

    public const REASON_CART_CHANGED = 'cart_changed';

    public const REASON_PURCHASE_SUPERSEDED = 'purchase_superseded';

    public const REASON_PURCHASE_CANCELLED = 'purchase_cancelled';

    public const REASON_PAYMENT_REJECTED = 'payment_rejected';

    public const REASON_MANUAL_RELEASE = 'manual_release';

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

    public function syncCart(Cart $cart): ?StockReservation
    {
        return DB::transaction(function () use ($cart): ?StockReservation {
            /** @var Cart $lockedCart */
            $lockedCart = Cart::query()->lockForUpdate()->findOrFail($cart->getKey());
            $items = $lockedCart->items()->orderBy('id')->lockForUpdate()->get();
            $this->loadSelections($items);
            $requirements = $this->requirementsForItems($items);

            $reservation = $lockedCart->current_stock_reservation_id === null
                ? null
                : StockReservation::query()->lockForUpdate()->find($lockedCart->current_stock_reservation_id);

            if ($reservation !== null) {
                $this->assertUsableCartReservation($reservation);
            }

            if ($requirements === []) {
                if ($reservation !== null && $reservation->status === StockReservation::STATUS_ACTIVE) {
                    $this->finalizeLocked(
                        $reservation,
                        StockReservation::STATUS_RELEASED,
                        self::REASON_CART_EMPTY,
                    );
                }

                $lockedCart->update(['current_stock_reservation_id' => null]);
                $cart->current_stock_reservation_id = null;

                return null;
            }

            if ($reservation === null) {
                $reservation = StockReservation::query()->create([
                    'status' => StockReservation::STATUS_ACTIVE,
                    'expires_at' => $this->expiration(),
                ]);
                $lockedCart->update(['current_stock_reservation_id' => $reservation->getKey()]);
            }

            if (Purchase::query()->where('stock_reservation_id', $reservation->getKey())->exists()) {
                throw new \InvalidArgumentException('La reserva vinculada a una compra no se puede modificar.');
            }

            $currentLines = StockReservationLine::query()
                ->where('stock_reservation_id', $reservation->getKey())
                ->orderBy('inventory_id')
                ->lockForUpdate()
                ->get()
                ->keyBy('inventory_id');
            $inventoryIds = collect(array_keys($requirements))
                ->merge($currentLines->keys())
                ->map(fn ($id): int => (int) $id)
                ->unique()
                ->sort()
                ->values();
            $inventories = Inventory::query()
                ->whereKey($inventoryIds)
                ->orderBy('id')
                ->lockForUpdate()
                ->get()
                ->keyBy('id');

            foreach ($inventoryIds as $inventoryId) {
                $inventory = $inventories->get($inventoryId)
                    ?? throw new \InvalidArgumentException('No se encontró el inventario requerido.');
                $previous = (int) ($currentLines->get($inventoryId)?->quantity ?? 0);
                $required = (int) ($requirements[$inventoryId]['quantity'] ?? 0);
                $delta = $required - $previous;

                if ($delta > 0
                    && $requirements[$inventoryId]['tracks_inventory']
                    && $inventory->availableStock() < $delta) {
                    throw new \InvalidArgumentException('No hay suficiente stock disponible para reservar el carrito.');
                }

                if ($delta < 0 && $inventory->reserved_stock < abs($delta)) {
                    throw new \InvalidArgumentException('La reserva de stock del carrito es inconsistente.');
                }
            }

            foreach ($inventoryIds as $inventoryId) {
                /** @var Inventory $inventory */
                $inventory = $inventories->get($inventoryId);
                $line = $currentLines->get($inventoryId);
                $previous = (int) ($line?->quantity ?? 0);
                $required = (int) ($requirements[$inventoryId]['quantity'] ?? 0);
                $delta = $required - $previous;

                if ($delta > 0) {
                    $inventory->reserve($delta, $requirements[$inventoryId]['tracks_inventory']);
                } elseif ($delta < 0) {
                    $inventory->release(abs($delta));
                }

                if ($required === 0) {
                    $line?->delete();

                    continue;
                }

                StockReservationLine::query()->updateOrCreate(
                    [
                        'stock_reservation_id' => $reservation->getKey(),
                        'inventory_id' => $inventoryId,
                    ],
                    [
                        'quantity' => $required,
                        'tracks_inventory' => $requirements[$inventoryId]['tracks_inventory'],
                    ],
                );
            }

            $reservation->update([
                'expires_at' => $this->expiration(),
                'release_reason' => null,
            ]);
            $cart->current_stock_reservation_id = $reservation->getKey();

            return $reservation->fresh('lines');
        });
    }

    public function attachToPurchase(
        Cart $cart,
        Purchase $purchase,
        Carbon $expiresAt,
    ): StockReservation {
        return DB::transaction(function () use ($cart, $purchase, $expiresAt): StockReservation {
            /** @var Cart $lockedCart */
            $lockedCart = Cart::query()->lockForUpdate()->findOrFail($cart->getKey());
            /** @var Purchase $lockedPurchase */
            $lockedPurchase = Purchase::query()->lockForUpdate()->findOrFail($purchase->getKey());

            if ($lockedCart->current_stock_reservation_id === null) {
                throw new \InvalidArgumentException('El carrito no tiene una reserva de stock activa.');
            }

            /** @var StockReservation $reservation */
            $reservation = StockReservation::query()->lockForUpdate()->findOrFail($lockedCart->current_stock_reservation_id);
            $this->assertUsableCartReservation($reservation);

            $linkedPurchase = Purchase::query()
                ->where('stock_reservation_id', $reservation->getKey())
                ->whereKeyNot($lockedPurchase->getKey())
                ->exists();
            if ($linkedPurchase) {
                throw new \InvalidArgumentException('La reserva de stock ya pertenece a otra compra.');
            }

            $lockedPurchase->update(['stock_reservation_id' => $reservation->getKey()]);
            $reservation->update(['expires_at' => $expiresAt]);
            $purchase->stock_reservation_id = $reservation->getKey();
            $cart->current_stock_reservation_id = $reservation->getKey();

            return $reservation->fresh('lines');
        });
    }

    public function commit(Purchase $purchase): void
    {
        DB::transaction(function () use ($purchase): void {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()->lockForUpdate()->findOrFail($purchase->getKey());
            if ($purchase->stock_reservation_id === null) {
                throw new \InvalidArgumentException('La compra no tiene una reserva de stock.');
            }

            /** @var StockReservation $reservation */
            $reservation = StockReservation::query()->lockForUpdate()->findOrFail($purchase->stock_reservation_id);
            if ($reservation->status === StockReservation::STATUS_COMMITTED) {
                return;
            }
            if ($reservation->status !== StockReservation::STATUS_ACTIVE) {
                throw new \InvalidArgumentException('La reserva de stock no está activa.');
            }
            if ($reservation->expires_at !== null && ! $reservation->expires_at->isFuture()) {
                throw new StockReservationExpiredException;
            }

            $lines = $this->lockLines($reservation);
            if ($lines->isEmpty()) {
                throw new \InvalidArgumentException('La reserva de stock no tiene inventarios.');
            }

            $inventories = $this->lockInventories($lines);
            foreach ($lines as $line) {
                $inventory = $inventories->get($line->inventory_id)
                    ?? throw new \InvalidArgumentException('No se encontró el inventario reservado.');
                if ($inventory->reserved_stock < $line->quantity
                    || ($line->tracks_inventory && $inventory->real_stock < $line->quantity)) {
                    throw new \InvalidArgumentException('La reserva de stock no alcanza para confirmar la compra.');
                }
            }

            foreach ($lines as $line) {
                $inventories->get($line->inventory_id)->buy(
                    (int) $line->quantity,
                    (bool) $line->tracks_inventory,
                );
            }

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

    public function releaseForPurchase(
        Purchase $purchase,
        string $status = StockReservation::STATUS_RELEASED,
        ?string $reason = null,
    ): void {
        DB::transaction(function () use ($purchase, $status, $reason): void {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()->lockForUpdate()->findOrFail($purchase->getKey());
            if ($purchase->stock_reservation_id === null) {
                return;
            }

            /** @var StockReservation|null $reservation */
            $reservation = StockReservation::query()->lockForUpdate()->find($purchase->stock_reservation_id);
            if ($reservation !== null) {
                $this->finalizeLocked($reservation, $status, $reason);
            }
        });
    }

    public function returnToCart(Purchase $purchase, Cart $cart): StockReservation
    {
        return DB::transaction(function () use ($purchase, $cart): StockReservation {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()->lockForUpdate()->findOrFail($purchase->getKey());
            /** @var Cart $cart */
            $cart = Cart::query()->lockForUpdate()->findOrFail($cart->getKey());

            if ($purchase->stock_reservation_id === null
                || $cart->current_stock_reservation_id !== $purchase->stock_reservation_id) {
                throw new \InvalidArgumentException('La compra y el carrito no comparten la reserva activa.');
            }

            /** @var StockReservation $reservation */
            $reservation = StockReservation::query()
                ->lockForUpdate()
                ->findOrFail($purchase->stock_reservation_id);
            $this->assertUsableCartReservation($reservation);

            $purchase->update(['stock_reservation_id' => null]);
            $cart->update(['current_purchase_id' => null]);
            $reservation->update(['expires_at' => $this->expiration()]);

            return $reservation->fresh('lines');
        });
    }

    public function assertCartReservationUsable(Cart $cart): void
    {
        DB::transaction(function () use ($cart): void {
            /** @var Cart $cart */
            $cart = Cart::query()->lockForUpdate()->findOrFail($cart->getKey());
            if ($cart->current_stock_reservation_id === null) {
                return;
            }

            /** @var StockReservation $reservation */
            $reservation = StockReservation::query()
                ->lockForUpdate()
                ->findOrFail($cart->current_stock_reservation_id);
            $this->assertUsableCartReservation($reservation);
        });
    }

    public function releaseCurrentCartReservation(
        Cart $cart,
        string $reason = self::REASON_CART_CHANGED,
    ): void {
        DB::transaction(function () use ($cart, $reason): void {
            /** @var Cart $cart */
            $cart = Cart::query()->lockForUpdate()->findOrFail($cart->getKey());
            if ($cart->current_stock_reservation_id === null) {
                return;
            }

            /** @var StockReservation|null $reservation */
            $reservation = StockReservation::query()->lockForUpdate()->find($cart->current_stock_reservation_id);
            if ($reservation !== null) {
                $this->finalizeLocked($reservation, StockReservation::STATUS_RELEASED, $reason);
            }
            $cart->update(['current_stock_reservation_id' => null]);
        });
    }

    public function expire(StockReservation $reservation): void
    {
        DB::transaction(function () use ($reservation): void {
            /** @var StockReservation $reservation */
            $reservation = StockReservation::query()->lockForUpdate()->findOrFail($reservation->getKey());
            if ($reservation->status !== StockReservation::STATUS_ACTIVE
                || $reservation->expires_at === null
                || $reservation->expires_at->isFuture()) {
                return;
            }

            $this->finalizeLocked($reservation, StockReservation::STATUS_EXPIRED, null);
        });
    }

    public function clearExpirationForReview(Purchase $purchase): void
    {
        DB::transaction(function () use ($purchase): void {
            /** @var Purchase $purchase */
            $purchase = Purchase::query()->lockForUpdate()->findOrFail($purchase->getKey());
            if ($purchase->stock_reservation_id === null) {
                throw new \InvalidArgumentException('La compra no tiene una reserva de stock.');
            }

            /** @var StockReservation $reservation */
            $reservation = StockReservation::query()
                ->lockForUpdate()
                ->findOrFail($purchase->stock_reservation_id);
            if ($reservation->status !== StockReservation::STATUS_ACTIVE) {
                throw new \InvalidArgumentException('La reserva de stock no está activa.');
            }
            if ($reservation->expires_at !== null && ! $reservation->expires_at->isFuture()) {
                throw new StockReservationExpiredException;
            }

            $reservation->update(['expires_at' => null]);
        });
    }

    /**
     * @param  Collection<int, CartItem>  $items
     * @return array<int, array{quantity: int, tracks_inventory: bool}>
     */
    private function requirementsForItems(Collection $items): array
    {
        $requirements = [];
        foreach ($items as $item) {
            $selection = $item->selectedItem();
            if ($selection === null) {
                throw new \InvalidArgumentException('El carrito contiene un item de catálogo inexistente.');
            }

            foreach ($this->inventory->detailedRequirementsFor($selection, (int) $item->cantidad) as $inventoryId => $requirement) {
                if (isset($requirements[$inventoryId])) {
                    $requirements[$inventoryId]['quantity'] += $requirement['quantity'];
                    $requirements[$inventoryId]['tracks_inventory'] =
                        $requirements[$inventoryId]['tracks_inventory'] || $requirement['tracks_inventory'];

                    continue;
                }

                $requirements[$inventoryId] = $requirement;
            }
        }

        ksort($requirements);

        return $requirements;
    }

    /** @param Collection<int, CartItem> $items */
    private function loadSelections(Collection $items): void
    {
        $items->load([
            'catalogItem.inventory',
            'catalogItem.bundleComponents.catalogItem.inventory',
            'catalogItem.bundleComponents.variant.inventory',
            'catalogItem.bundleComponents.variant.catalogItem',
            'variant.inventory',
            'variant.catalogItem',
        ]);
    }

    /** @return Collection<int, StockReservationLine> */
    private function lockLines(StockReservation $reservation): Collection
    {
        return StockReservationLine::query()
            ->where('stock_reservation_id', $reservation->getKey())
            ->orderBy('inventory_id')
            ->lockForUpdate()
            ->get();
    }

    /**
     * @param  Collection<int, StockReservationLine>  $lines
     * @return Collection<int, Inventory>
     */
    private function lockInventories(Collection $lines): Collection
    {
        return Inventory::query()
            ->whereKey($lines->pluck('inventory_id'))
            ->orderBy('id')
            ->lockForUpdate()
            ->get()
            ->keyBy('id');
    }

    private function finalizeLocked(
        StockReservation $reservation,
        string $status,
        ?string $reason,
    ): void {
        if ($reservation->status !== StockReservation::STATUS_ACTIVE) {
            return;
        }
        if (! in_array($status, [StockReservation::STATUS_RELEASED, StockReservation::STATUS_EXPIRED], true)) {
            throw new \InvalidArgumentException('El estado final de la reserva no es válido.');
        }

        $lines = $this->lockLines($reservation);
        $inventories = $this->lockInventories($lines);
        foreach ($lines as $line) {
            $inventory = $inventories->get($line->inventory_id)
                ?? throw new \InvalidArgumentException('No se encontró el inventario reservado.');
            $inventory->release((int) $line->quantity);
        }

        $now = now();
        $reservation->update([
            'status' => $status,
            'expires_at' => null,
            'released_at' => $status === StockReservation::STATUS_RELEASED ? $now : null,
            'expired_at' => $status === StockReservation::STATUS_EXPIRED ? $now : null,
            'release_reason' => $status === StockReservation::STATUS_RELEASED ? $reason : null,
        ]);

        if ($status === StockReservation::STATUS_RELEASED) {
            Cart::query()
                ->where('current_stock_reservation_id', $reservation->getKey())
                ->update(['current_stock_reservation_id' => null]);
        }
    }

    private function assertUsableCartReservation(StockReservation $reservation): void
    {
        if ($reservation->status === StockReservation::STATUS_EXPIRED
            || ($reservation->expires_at !== null && ! $reservation->expires_at->isFuture())) {
            throw new StockReservationExpiredException;
        }

        if ($reservation->status !== StockReservation::STATUS_ACTIVE
            || $reservation->expires_at === null) {
            throw new \InvalidArgumentException('La reserva de stock no está disponible para operar el carrito.');
        }
    }

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