<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Components;

use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\OfferDate;
use Ferienpass\CoreBundle\Settings\PageConfigManager;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;

/**
 * Renders an offer's date(s) as a "schedule block" — one of four kinds with
 * its own icon and tint, plus an optional "+ N Alternativtermine" badge if
 * the offer has variants.
 *
 *   ongoing    No dates at all → "Laufendes Angebot"
 *   single     Exactly one one-off OfferDate (no recurrence)
 *   recurring  At least one OfferDate carries an RRULE
 *              → "Jeden Di. · 10:00 – 12:00 Uhr"
 *   series     Multiple plain OfferDates → range or first + count
 *
 * Time-of-day handling for series:
 *   - All occurrences share the same begin/end time → one "jeweils HH:MM – HH:MM"
 *     line.
 *   - Different times → omit a unified time line (each date carries its own
 *     time in the date line via the "weitere Termine" suffix).
 */
#[AsTwigComponent(template: '@Contao/components/offer_schedule.html.twig')]
class OfferSchedule
{
    public OfferInterface $offer;

    public ?string $locale = null;

    /**
     * Cached, flattened occurrences across all OfferDates of this offer.
     * The template hits the helpers several times (kind, firstBegin, lastBegin,
     * uniformTimeOfDay, seriesIsRange) and each would otherwise re-flatten —
     * for recurring dates that's a non-trivial Recurr expansion every call.
     *
     * @var list<array{begin: \DateTimeInterface, end: \DateTimeInterface}>|null
     */
    private ?array $cachedOccurrences = null;

    public function __construct(private readonly PageConfigManager $pageConfigManager)
    {
    }

    /**
     * Classify the offer's schedule for icon + tint + headline rendering:
     *
     *   ongoing   no OfferDate at all
     *   single    exactly one OfferDate, no RRULE
     *   recurring every OfferDate carries an RRULE (e.g. "jeden Di. + jeden Do.")
     *   series   anything else with multiple dates — including the mixed case
     *            of one RRULE plus an ad-hoc date, which we render as a series
     *            because no single label fits both
     */
    #[ExposeInTemplate]
    public function kind(): string
    {
        $dates = $this->offer->getDates();
        $total = \count($dates);
        if (0 === $total) {
            return 'ongoing';
        }

        $recurring = 0;
        foreach ($dates as $d) {
            if ($d->getRecurrence()) {
                ++$recurring;
            }
        }

        if ($recurring === $total) {
            return 'recurring';
        }
        if (0 === $recurring) {
            return 1 === $total ? 'single' : 'series';
        }

        // Mixed: at least one recurrence + at least one plain date.
        return 'series';
    }

    /**
     * Number of sibling alternatives for the "+ N Alternativtermine" badge.
     *
     * - For a variant base: number of child variants.
     * - For a variant: number of siblings (other variants of the same base).
     * - Otherwise: 0.
     */
    #[ExposeInTemplate]
    public function alternativesCount(): int
    {
        if ($this->offer->hasVariants()) {
            return \count($this->offer->getVariants());
        }

        $base = $this->offer->getVariantBase();
        if (null !== $base) {
            $siblings = \count($base->getVariants());

            return max(0, $siblings - 1);
        }

        return 0;
    }

    /**
     * @return list<array{begin: \DateTimeInterface, end: \DateTimeInterface}>
     */
    #[ExposeInTemplate]
    public function occurrences(): array
    {
        if (null !== $this->cachedOccurrences) {
            return $this->cachedOccurrences;
        }

        $tz = new \DateTimeZone($this->pageConfigManager->getPageConfig()->getTimezone());

        $out = [];
        foreach ($this->offer->getDates() as $date) {
            foreach ($date->getOccurrences($tz) as $occ) {
                $out[] = $occ;
            }
        }

        return $this->cachedOccurrences = $out;
    }

    #[ExposeInTemplate]
    public function recurrencePhrase(): ?string
    {
        foreach ($this->offer->getDates() as $date) {
            /** @var OfferDate $date */
            if ($p = $date->getRecurrencePhrase()) {
                return $p;
            }
        }

        return null;
    }

    /**
     * True when every occurrence starts and ends at the same time of day.
     * Used by the series view to render a single "jeweils HH:MM – HH:MM" line.
     */
    #[ExposeInTemplate]
    public function uniformTimeOfDay(): bool
    {
        $beginTimes = [];
        $endTimes = [];
        foreach ($this->occurrences() as $occ) {
            $beginTimes[$occ['begin']->format('Hi')] = true;
            $endTimes[$occ['end']->format('Hi')] = true;
            if (\count($beginTimes) > 1 || \count($endTimes) > 1) {
                return false;
            }
        }

        return 1 === \count($beginTimes) && 1 === \count($endTimes);
    }

    /**
     * True when the series' occurrences form a continuous run of days
     * (one occurrence per day, no gaps) — including runs that cross a
     * month boundary like 30.06 – 04.07. Drives the compact "Mo. – Fr.,
     * 30.06. – 04.07." rendering; the template decides whether to show
     * the month on both endpoints based on whether they're in the same
     * month.
     */
    #[ExposeInTemplate]
    public function seriesIsRange(): bool
    {
        $occ = $this->occurrences();
        $n = \count($occ);
        if ($n < 2) {
            return false;
        }

        // diff()->days is always non-negative — Mon→Fri in the same week
        // is 4 days, plus 1 makes 5 occurrences for a no-gap run.
        $first = $occ[0]['begin'];
        $last = end($occ)['begin'];

        return ((int) $last->diff($first)->days + 1) === $n;
    }

    /**
     * True when the series-range spans more than one calendar month — used
     * by the template to decide whether to put the month on both endpoints
     * ("30.06. – 04.07.") or only the second one ("04. – 08.08.").
     */
    #[ExposeInTemplate]
    public function seriesSpansMonths(): bool
    {
        $occ = $this->occurrences();
        if (\count($occ) < 2) {
            return false;
        }

        return $occ[0]['begin']->format('Ym') !== end($occ)['begin']->format('Ym');
    }

    /** First occurrence's begin (used by the template for date formatting). */
    #[ExposeInTemplate]
    public function firstBegin(): ?\DateTimeInterface
    {
        $occ = $this->occurrences();

        return $occ[0]['begin'] ?? null;
    }

    /** First occurrence's end. */
    #[ExposeInTemplate]
    public function firstEnd(): ?\DateTimeInterface
    {
        $occ = $this->occurrences();

        return $occ[0]['end'] ?? null;
    }

    /** Last occurrence's begin — only meaningful when seriesIsRange() is true. */
    #[ExposeInTemplate]
    public function lastBegin(): ?\DateTimeInterface
    {
        $occ = $this->occurrences();
        if ([] === $occ) {
            return null;
        }

        return end($occ)['begin'];
    }

    #[ExposeInTemplate]
    public function occurrenceCount(): int
    {
        return \count($this->occurrences());
    }
}
