<?php

namespace App\Domains\Ticket\Services;

use App\Domains\Ticket\Enums\ValidityTimeType;
use App\Domains\Ticket\Models\ValidityTime;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection;

/**
 * Representa una intersección de vigencias: todos los ValidityTime del grupo
 * deben cumplirse simultáneamente (AND).
 *
 * Ejemplo: [fecha del evento, horario de almuerzo] significa que el ticket
 * solamente es válido durante la intersección de ambas ventanas.
 */
final readonly class ResolvedValidityGroup
{
    /** @param Collection<int, ValidityTime> $validityTimes */
    public function __construct(public Collection $validityTimes) {}

    /** Comprueba si el instante pertenece a la intersección efectiva del grupo. */
    public function isValid(?CarbonInterface $at = null): bool
    {
        $at ??= now();
        $startsAt = $this->effectiveStartsAt($at);
        $expiresAt = $this->effectiveExpiresAt($at);

        return $this->validityTimes->isNotEmpty()
            && ($startsAt === null || $startsAt->lessThanOrEqualTo($at))
            && ($expiresAt === null || $expiresAt->greaterThan($at));
    }

    /** Un grupo vence cuando termina su intersección efectiva. */
    public function isExpired(?CarbonInterface $at = null): bool
    {
        $at ??= now();
        $expiresAt = $this->effectiveExpiresAt($at);

        return $expiresAt !== null && $expiresAt->lessThanOrEqualTo($at);
    }

    /**
     * En un AND, la intersección comienza en el inicio más tardío.
     * Por ejemplo, fecha 00:00 + horario 12:00 comienza a las 12:00.
     */
    public function effectiveStartsAt(?CarbonInterface $at = null): ?CarbonInterface
    {
        $at ??= now();
        $anchor = $this->dateAnchor() ?? $at;

        return $this->validityTimes
            ->map(fn (ValidityTime $validityTime): ?CarbonInterface => $validityTime->startsAt($anchor))
            ->filter()
            ->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp())
            ->first();
    }

    /**
     * En un AND, la intersección termina en el vencimiento más temprano.
     * Los horarios cuyo fin no supera al inicio se interpretan como nocturnos.
     */
    public function effectiveExpiresAt(?CarbonInterface $at = null): ?CarbonInterface
    {
        $at ??= now();
        $anchor = $this->dateAnchor() ?? $at;

        return $this->validityTimes
            ->map(function (ValidityTime $validityTime) use ($anchor): ?CarbonInterface {
                $startsAt = $validityTime->startsAt($anchor);
                $expiresAt = $validityTime->expiresAt($anchor);

                if ($startsAt !== null && $expiresAt !== null && $expiresAt->lessThanOrEqualTo($startsAt)) {
                    return $expiresAt->addDay();
                }

                return $expiresAt;
            })
            ->filter()
            ->sortBy(fn (CarbonInterface $expiresAt): int => $expiresAt->getTimestamp())
            ->first();
    }

    /**
     * Usa la fecha de un fixed_window como ancla para convertir ventanas que
     * sólo contienen horas (time_window) en instantes concretos.
     */
    private function dateAnchor(): ?CarbonInterface
    {
        return $this->validityTimes
            ->filter(fn (ValidityTime $validityTime): bool => $validityTime->type === ValidityTimeType::FixedWindow)
            ->map(fn (ValidityTime $validityTime): ?CarbonInterface => $validityTime->fixed_starts_at)
            ->filter()
            ->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp())
            ->first();
    }
}
