<?php

namespace App\Domains\Catalog\Services;

use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\ItemAttribute;
use App\Domains\Catalog\Models\Variant;
use Illuminate\Support\Collection;

class VariantSelectionService
{
    /**
     * @param  array<string, mixed>  $selectedValues
     * @return array<string, mixed>
     */
    public function options(
        CatalogItem $catalogItem,
        array $selectedValues,
        ?int $includedVariantId = null,
    ): array {
        $catalogItem->load([
            'itemAttributes.attribute',
            'variants' => fn ($query) => $query->orderBy('id'),
            'variants.inventory',
            'variants.eventDate',
            'variants.eventDates',
            'variants.definitions' => fn ($query) => $query->orderBy('id'),
            'variants.definitions.itemAttribute.attribute.options',
        ]);

        $variants = $catalogItem->visibleVariants($includedVariantId)
            ->values();
        $normalizedSelections = collect($selectedValues)
            ->mapWithKeys(fn ($value, string $key): array => [$key => $this->normalizeValue($value)])
            ->filter(fn ($value): bool => $value !== null && $value !== '' && $value !== [])
            ->all();
        $matchingVariants = $variants
            ->filter(fn (Variant $variant): bool => $this->matches($variant, $normalizedSelections))
            ->values();
        $attributeKeys = $this->attributeKeys($catalogItem, $catalogItem->variants->values());
        $isComplete = $attributeKeys->isNotEmpty()
            && $attributeKeys->every(fn (string $key): bool => array_key_exists($key, $normalizedSelections));
        $resolvedVariant = $isComplete && $matchingVariants->count() === 1
            ? $matchingVariants->first()
            : null;

        return [
            'variants' => $variants
                ->map(fn (Variant $variant): array => $this->variantData($catalogItem, $variant))
                ->values()
                ->all(),
            'selectors' => $this->selectors(
                $catalogItem,
                $variants,
                $attributeKeys,
                $matchingVariants->isEmpty() ? [] : $normalizedSelections,
            ),
            'selected_values' => (object) ($matchingVariants->isEmpty() ? [] : $normalizedSelections),
            'resolved_variant' => $resolvedVariant === null
                ? null
                : $this->variantData($catalogItem, $resolvedVariant),
            'valid' => $matchingVariants->isNotEmpty(),
            'available_variant_count' => $variants->count(),
            'matching_variant_count' => $matchingVariants->count(),
            'price_range' => $this->priceRange($catalogItem, $variants),
        ];
    }

    /** @param array<string, mixed> $selectedValues */
    private function matches(Variant $variant, array $selectedValues): bool
    {
        $variantValues = $variant->selectionValues();

        foreach ($selectedValues as $key => $selectedValue) {
            if (! $variantValues->has($key)
                || $this->valueKey($variantValues->get($key)) !== $this->valueKey($selectedValue)) {
                return false;
            }
        }

        return true;
    }

    /** @param Collection<int, Variant> $variants */
    private function attributeKeys(CatalogItem $catalogItem, Collection $variants): Collection
    {
        return $variants
            ->flatMap(fn (Variant $variant): array => $variant
                ->selectorOptions($catalogItem->itemAttributes)
                ->keys()
                ->all())
            ->unique()
            ->values();
    }

    /**
     * @param  Collection<int, Variant>  $variants
     * @param  Collection<int, string>  $attributeKeys
     * @param  array<string, mixed>  $selectedValues
     * @return list<array<string, mixed>>
     */
    private function selectors(
        CatalogItem $catalogItem,
        Collection $variants,
        Collection $attributeKeys,
        array $selectedValues,
    ): array {
        return $attributeKeys
            ->map(function (string $key, int $index) use (
                $catalogItem,
                $variants,
                $attributeKeys,
                $selectedValues,
            ): array {
                $previousKeys = $attributeKeys->take($index);
                $previousSelections = collect($selectedValues)
                    ->only($previousKeys->all())
                    ->all();
                $compatibleVariants = $variants
                    ->filter(fn (Variant $variant): bool => $this->matches($variant, $previousSelections));

                return [
                    'key' => $key,
                    'label' => $this->attributeLabel($catalogItem, $key),
                    'options' => $this->optionsFor($catalogItem, $compatibleVariants, $key),
                    'enabled' => $index === 0 || $previousKeys->every(
                        fn (string $previousKey): bool => array_key_exists($previousKey, $selectedValues),
                    ),
                ];
            })
            ->values()
            ->all();
    }

    /**
     * @param  Collection<int, Variant>  $variants
     * @return list<mixed>
     */
    private function optionsFor(CatalogItem $catalogItem, Collection $variants, string $key): array
    {
        $options = [];
        $seen = [];

        foreach ($variants as $variant) {
            $option = $variant->selectorOptions($catalogItem->itemAttributes)->get($key);
            if ($option === null || $option === '') {
                continue;
            }

            $optionKey = $this->valueKey($option);
            if (isset($seen[$optionKey])) {
                continue;
            }

            $seen[$optionKey] = true;
            $options[] = $option;
        }

        return $options;
    }

    private function attributeLabel(CatalogItem $catalogItem, string $key): string
    {
        if ($key === 'event_date') {
            return 'Fecha';
        }

        return $catalogItem->itemAttributes
            ->first(fn (ItemAttribute $itemAttribute): bool => $itemAttribute->attribute?->codigo === $key)
            ?->attribute
            ?->nombre ?? str($key)->headline()->toString();
    }

    /** @param Collection<int, Variant> $variants */
    private function priceRange(CatalogItem $catalogItem, Collection $variants): array
    {
        $prices = $variants
            ->map(fn (Variant $variant): float => $variant->getPrice())
            ->whenEmpty(fn (Collection $prices): Collection => $prices->push($catalogItem->getPrice()));

        return [
            'minimum' => number_format((float) $prices->min(), 2, '.', ''),
            'maximum' => number_format((float) $prices->max(), 2, '.', ''),
        ];
    }

    /** @return array<string, mixed> */
    private function variantData(CatalogItem $catalogItem, Variant $variant): array
    {
        return [
            'id' => $variant->id,
            'descripcion' => $variant->getDescription(),
            'precio' => number_format($variant->getPrice(), 2, '.', ''),
            'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited
                ? null
                : $variant->inventory?->availableStock(),
            'values' => $variant->selectorOptions($catalogItem->itemAttributes),
        ];
    }

    private function normalizeValue(mixed $value): mixed
    {
        if (is_array($value) && array_key_exists('value', $value)) {
            return (string) $value['value'];
        }

        if (is_array($value)) {
            return array_map(fn ($item) => $this->normalizeValue($item), $value);
        }

        return is_scalar($value) ? (string) $value : null;
    }

    private function valueKey(mixed $value): string
    {
        $normalized = $this->normalizeValue($value);

        if (is_array($normalized)) {
            sort($normalized);
        }

        return json_encode($normalized, JSON_THROW_ON_ERROR);
    }
}
