<?php

declare(strict_types=1);

namespace App\Administration\Domain\Model\SchoolCalendar;

use App\Administration\Domain\Event\CalendrierConfigure;
use App\Administration\Domain\Event\JourneePedagogiqueAjoutee;
use App\Administration\Domain\Exception\CalendrierEntreeNonTrouveeException;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Shared\Domain\AggregateRoot;
use App\Shared\Domain\Tenant\TenantId;

use function count;

use DateTimeImmutable;
use InvalidArgumentException;

/**
 * Aggregate Root représentant le calendrier scolaire d'un établissement.
 *
 * Un calendrier est identifié par (tenantId, academicYearId).
 * Il contient les jours fériés, vacances et journées pédagogiques.
 *
 * @see FR80: Configurer jours fériés et vacances scolaires
 */
final class SchoolCalendar extends AggregateRoot
{
    /** @var array<string, CalendarEntry> Indexé par CalendarEntryId */
    private array $entries = [];

    private function __construct(
        public private(set) TenantId $tenantId,
        public private(set) AcademicYearId $academicYearId,
        public private(set) ?SchoolZone $zone,
    ) {
    }

    /**
     * Initialise un nouveau calendrier scolaire pour un tenant et une année académique.
     */
    public static function initialiser(
        TenantId $tenantId,
        AcademicYearId $academicYearId,
    ): self {
        return new self(
            tenantId: $tenantId,
            academicYearId: $academicYearId,
            zone: null,
        );
    }

    /**
     * Configure la zone scolaire et importe les entrées officielles.
     *
     * @param CalendarEntry[] $entries
     */
    public function configurer(SchoolZone $zone, array $entries, DateTimeImmutable $at): void
    {
        $this->zone = $zone;
        $this->entries = array_filter(
            $this->entries,
            static fn (CalendarEntry $e): bool => $e->type === CalendarEntryType::PEDAGOGICAL_DAY,
        );

        foreach ($entries as $entry) {
            $this->entries[(string) $entry->id] = $entry;
        }

        $this->recordEvent(new CalendrierConfigure(
            tenantId: $this->tenantId,
            academicYearId: $this->academicYearId,
            zone: $zone,
            nombreEntrees: count($this->entries),
            occurredOn: $at,
        ));
    }

    /**
     * Configure uniquement la zone scolaire du calendrier.
     */
    public function configurerZone(SchoolZone $zone): void
    {
        $this->zone = $zone;
    }

    /**
     * Ajoute une entrée au calendrier.
     */
    public function ajouterEntree(CalendarEntry $entry): void
    {
        $this->entries[(string) $entry->id] = $entry;
    }

    /**
     * Ajoute une journée pédagogique et émet l'événement pour notifier les enseignants.
     */
    public function ajouterJourneePedagogique(CalendarEntry $entry, DateTimeImmutable $at): void
    {
        if ($entry->type !== CalendarEntryType::PEDAGOGICAL_DAY) {
            throw new InvalidArgumentException('L\'entrée doit être de type journée pédagogique.');
        }

        $this->entries[(string) $entry->id] = $entry;

        $this->recordEvent(new JourneePedagogiqueAjoutee(
            entryId: $entry->id,
            tenantId: $this->tenantId,
            academicYearId: $this->academicYearId,
            date: $entry->startDate,
            label: $entry->label,
            occurredOn: $at,
        ));
    }

    /**
     * Supprime une entrée du calendrier.
     *
     * @throws CalendrierEntreeNonTrouveeException
     */
    public function supprimerEntree(CalendarEntryId $entryId): void
    {
        $key = (string) $entryId;

        if (!isset($this->entries[$key])) {
            throw CalendrierEntreeNonTrouveeException::avecId($entryId);
        }

        unset($this->entries[$key]);
    }

    /**
     * Vide toutes les entrées du calendrier.
     */
    public function viderEntrees(): void
    {
        $this->entries = [];
    }

    /**
     * Vérifie si une date est un jour ouvré (pas un weekend, férié, ou vacances).
     */
    public function estJourOuvre(DateTimeImmutable $date): bool
    {
        $dayOfWeek = (int) $date->format('N');
        if ($dayOfWeek >= 6) {
            return false;
        }

        foreach ($this->entries as $entry) {
            if ($entry->couvre($date)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Trouve l'entrée calendrier couvrant une date donnée.
     */
    public function trouverEntreePourDate(DateTimeImmutable $date): ?CalendarEntry
    {
        foreach ($this->entries as $entry) {
            if ($entry->couvre($date)) {
                return $entry;
            }
        }

        return null;
    }

    /**
     * Vérifie si une date tombe pendant les vacances scolaires.
     */
    public function estEnVacances(DateTimeImmutable $date): bool
    {
        foreach ($this->entries as $entry) {
            if ($entry->couvre($date) && $entry->type === CalendarEntryType::VACATION) {
                return true;
            }
        }

        return false;
    }

    /**
     * Vérifie si une date est un jour de retour de vacances.
     */
    public function estJourRetourVacances(DateTimeImmutable $date): bool
    {
        $veille = $date->modify('-1 day');

        foreach ($this->entries as $entry) {
            if ($entry->type === CalendarEntryType::VACATION
                && $veille->format('Y-m-d') === $entry->endDate->format('Y-m-d')
            ) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return CalendarEntry[]
     */
    public function entries(): array
    {
        return array_values($this->entries);
    }

    /**
     * Reconstitue un SchoolCalendar depuis le stockage.
     *
     * @param CalendarEntry[] $entries
     *
     * @internal Pour usage Infrastructure uniquement
     */
    public static function reconstitute(
        TenantId $tenantId,
        AcademicYearId $academicYearId,
        ?SchoolZone $zone,
        array $entries,
    ): self {
        $calendar = new self(
            tenantId: $tenantId,
            academicYearId: $academicYearId,
            zone: $zone,
        );

        foreach ($entries as $entry) {
            $calendar->entries[(string) $entry->id] = $entry;
        }

        return $calendar;
    }
}
