<?php

namespace App\Domains\Catalog\Services;

use App\Domains\Catalog\Enums\CatalogItemType;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Models\BundleComponent;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\Variant;
use Illuminate\Support\Facades\DB;

class CatalogInventoryService
{
    /** @return array<int, int> */
    public function requirementsFor(CatalogItem|Variant $selection, int $quantity = 1): array
    {
        if ($quantity <= 0) {
            throw new \InvalidArgumentException('La cantidad debe ser mayor a cero.');
        }

        return array_map(
            fn (array $requirement): int => $requirement['quantity'] * $quantity,
            $this->inventoryRequirements($selection),
        );
    }

    public function availableQuantity(CatalogItem|Variant $selection): ?int
    {
        if ($selection instanceof CatalogItem
            && $selection->type === CatalogItemType::Standard
            && $selection->relationLoaded('inventory')
            && $selection->inventory !== null) {
            return $selection->inventory_policy === InventoryPolicy::Unlimited
                ? null
                : $selection->inventory->availableStock();
        }

        if ($selection instanceof CatalogItem
            && $selection->type === CatalogItemType::Standard
            && $selection->inventory_id === null) {
            if ($selection->inventory_policy === InventoryPolicy::Unlimited) {
                return null;
            }

            $selection->loadMissing('variants.inventory');

            return $selection->variants->sum(
                fn (Variant $variant): int => $variant->inventory->availableStock(),
            );
        }

        $requirements = $this->inventoryRequirements($selection);
        $trackedRequirements = array_filter(
            $requirements,
            fn (array $requirement): bool => $requirement['tracks_inventory'],
        );

        if ($trackedRequirements === []) {
            return null;
        }

        $inventories = Inventory::query()
            ->whereKey(array_keys($trackedRequirements))
            ->get()
            ->keyBy('id');
        $available = [];

        foreach ($trackedRequirements as $inventoryId => $requirement) {
            $inventory = $inventories->get($inventoryId)
                ?? throw new \InvalidArgumentException('No se encontro el inventario requerido.');
            $available[] = intdiv(
                $inventory->availableStock(),
                $requirement['quantity'],
            );
        }

        return min($available);
    }

    public function reserve(CatalogItem|Variant $selection, int $quantity): void
    {
        $this->mutate($selection, $quantity, 'reserve');
    }

    public function release(CatalogItem|Variant $selection, int $quantity): void
    {
        $this->mutate($selection, $quantity, 'release');
    }

    public function commit(CatalogItem|Variant $selection, int $quantity): void
    {
        $this->mutate($selection, $quantity, 'commit');
    }

    private function mutate(
        CatalogItem|Variant $selection,
        int $quantity,
        string $operation,
    ): void {
        if ($quantity <= 0) {
            throw new \InvalidArgumentException('La cantidad debe ser mayor a cero.');
        }

        DB::transaction(function () use ($selection, $quantity, $operation): void {
            $requirements = $this->inventoryRequirements($selection);
            ksort($requirements);
            $inventories = Inventory::query()
                ->whereKey(array_keys($requirements))
                ->orderBy('id')
                ->lockForUpdate()
                ->get()
                ->keyBy('id');

            foreach ($requirements as $inventoryId => $requirement) {
                $inventory = $inventories->get($inventoryId)
                    ?? throw new \InvalidArgumentException('No se encontro el inventario requerido.');
                $requiredQuantity = $requirement['quantity'] * $quantity;

                if ($operation === 'reserve'
                    && $requirement['tracks_inventory']
                    && $inventory->availableStock() < $requiredQuantity) {
                    throw new \InvalidArgumentException('No hay suficiente stock disponible para reservar.');
                }

                if (in_array($operation, ['release', 'commit'], true)
                    && $inventory->reserved_stock < $requiredQuantity) {
                    throw new \InvalidArgumentException('La cantidad reservada no alcanza para la operacion.');
                }

                if ($operation === 'commit'
                    && $requirement['tracks_inventory']
                    && $inventory->real_stock < $requiredQuantity) {
                    throw new \InvalidArgumentException('No hay suficiente stock real para confirmar la compra.');
                }
            }

            foreach ($requirements as $inventoryId => $requirement) {
                /** @var Inventory $inventory */
                $inventory = $inventories->get($inventoryId);
                $requiredQuantity = $requirement['quantity'] * $quantity;

                match ($operation) {
                    'reserve' => $inventory->reserve($requiredQuantity, $requirement['tracks_inventory']),
                    'release' => $inventory->release($requiredQuantity),
                    'commit' => $inventory->buy($requiredQuantity, $requirement['tracks_inventory']),
                };
            }
        });
    }

    /**
     * @return array<int, array{quantity: int, tracks_inventory: bool}>
     */
    private function inventoryRequirements(CatalogItem|Variant $selection): array
    {
        if ($selection instanceof Variant) {
            $selection->loadMissing('catalogItem');

            return $this->singleRequirement(
                $selection->inventory_id,
                $selection->catalogItem->inventory_policy,
            );
        }

        if ($selection->type !== CatalogItemType::Bundle) {
            return $this->singleRequirement(
                $selection->inventory_id,
                $selection->inventory_policy,
            );
        }

        $selection->loadMissing([
            'bundleComponents.catalogItem',
            'bundleComponents.variant.catalogItem',
        ]);
        $requirements = [];

        foreach ($selection->bundleComponents as $component) {
            $this->addComponentRequirement($requirements, $component);
        }

        if ($requirements === []) {
            throw new \InvalidArgumentException('El bundle no tiene componentes.');
        }

        return $requirements;
    }

    /** @return array<int, array{quantity: int, tracks_inventory: bool}> */
    private function singleRequirement(
        ?int $inventoryId,
        ?InventoryPolicy $policy,
    ): array {
        if ($inventoryId === null || $policy === null) {
            throw new \InvalidArgumentException('El item requiere una variante con inventario.');
        }

        return [
            $inventoryId => [
                'quantity' => 1,
                'tracks_inventory' => $policy === InventoryPolicy::Tracked,
            ],
        ];
    }

    /**
     * @param  array<int, array{quantity: int, tracks_inventory: bool}>  $requirements
     */
    private function addComponentRequirement(array &$requirements, BundleComponent $component): void
    {
        $selectedItem = $component->variant ?? $component->catalogItem;
        $inventoryId = $selectedItem->inventory_id;
        $policy = $component->catalogItem->inventory_policy;

        if ($inventoryId === null || $policy === null) {
            throw new \InvalidArgumentException('Un componente del bundle no tiene inventario.');
        }

        if (isset($requirements[$inventoryId])) {
            $requirements[$inventoryId]['quantity'] += $component->quantity;

            return;
        }

        $requirements[$inventoryId] = [
            'quantity' => $component->quantity,
            'tracks_inventory' => $policy === InventoryPolicy::Tracked,
        ];
    }
}
