<?php

namespace App\Domains\Ticket\Services;

use App\Domains\Catalog\Models\Variant;
use App\Domains\Catalog\Models\VariantDefinition;
use App\Domains\Ticket\Models\Ticket;
use App\Domains\Ticket\Models\ValidityTime;
use Illuminate\Support\Collection;

/**
 * Deriva la expresión temporal de un ticket desde su variante.
 *
 * Las selecciones alternativas de una misma dimensión (varias fechas u opciones
 * multiselección) se interpretan como OR. Las dimensiones diferentes se combinan
 * mediante AND usando un producto cartesiano.
 */
class TicketValidityResolver
{
    /** Relaciones necesarias para resolver tickets sin consultas N+1. */
    public const RELATIONS = [
        'sourceVariant.eventDates.validityTime',
        'sourceVariant.eventDate.validityTime',
        'sourceVariant.definitions.itemAttribute.attribute.options.validityTime',
    ];

    /**
     * Resuelve la variante fuente del ticket. Un ticket creado legítimamente sin
     * variante es irrestricto; una referencia esperada pero rota es irresoluble.
     */
    public function resolveTicket(Ticket $ticket): ResolvedTicketValidity
    {
        if ($ticket->source_variant_id === null) {
            return ResolvedTicketValidity::unrestricted();
        }

        $ticket->loadMissing(self::RELATIONS);

        if ($ticket->sourceVariant === null) {
            return ResolvedTicketValidity::unresolvable();
        }

        return $this->resolveVariant($ticket->sourceVariant);
    }

    /**
     * Convierte las fechas y definiciones temporales de la variante en grupos
     * normalizados: AND dentro de cada grupo y OR entre grupos.
     */
    public function resolveVariant(Variant $variant): ResolvedTicketValidity
    {
        $variant->loadMissing([
            'eventDates.validityTime',
            'eventDate.validityTime',
            'definitions.itemAttribute.attribute.options.validityTime',
        ]);

        $dimensions = collect();
        $eventDates = $variant->selectedEventDates();

        if ($eventDates->contains(fn ($eventDate): bool => $eventDate->validityTime === null)) {
            return ResolvedTicketValidity::unresolvable();
        }

        if ($eventDates->isNotEmpty()) {
            // Todas las fechas pertenecen a una misma dimensión alternativa:
            // fecha 1 OR fecha 2 OR fecha 3.
            $dimensions->push(
                $eventDates->map(fn ($eventDate): Collection => collect([$eventDate->validityTime]))
            );
        }

        foreach ($variant->definitions->groupBy('item_attribute_id') as $definitions) {
            $itemAttribute = $definitions->first()?->itemAttribute;
            $attribute = $itemAttribute?->attribute;

            if ($itemAttribute === null || $attribute === null) {
                return ResolvedTicketValidity::unresolvable();
            }

            if (! $attribute->type->supportsOptions() || $attribute->type->usesDynamicOptions()) {
                // Texto, números y demás atributos no temporales no restringen
                // la vigencia. EventDate se procesó arriba mediante su relación.
                continue;
            }

            if (! $itemAttribute->allow_multi_select && $definitions->count() > 1) {
                return ResolvedTicketValidity::unresolvable();
            }

            $alternatives = $definitions->map(function (VariantDefinition $definition) use ($attribute): ?Collection {
                $option = $attribute->options->firstWhere('value', $definition->value);

                if ($option === null) {
                    return null;
                }

                return collect([$option->validityTime])->filter()->values();
            });

            if ($alternatives->contains(null)) {
                return ResolvedTicketValidity::unresolvable();
            }

            if ($alternatives->contains(fn (Collection $alternative): bool => $alternative->isNotEmpty())) {
                // Las opciones elegidas del mismo atributo son alternativas OR.
                $dimensions->push($alternatives->values());
            }
        }

        if ($dimensions->isEmpty()) {
            return ResolvedTicketValidity::unrestricted();
        }

        $groups = collect([collect()]);

        foreach ($dimensions as $alternatives) {
            // El producto cartesiano agrega cada dimensión como una condición
            // AND y conserva sus opciones internas como alternativas OR.
            $groups = $groups->flatMap(
                fn (Collection $group): Collection => $alternatives->map(
                    fn (Collection $alternative): Collection => $group
                        ->merge($alternative)
                        ->unique(fn (ValidityTime $time): int => $time->getKey() ?? spl_object_id($time))
                        ->values()
                )
            )->values();
        }

        return new ResolvedTicketValidity(
            $groups->map(fn (Collection $times): ResolvedValidityGroup => new ResolvedValidityGroup($times))
        );
    }
}
