<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Service;

use App\Administration\Application\Port\OfficialCalendarProvider;
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntry;
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryId;
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
use App\Administration\Domain\Model\SchoolCalendar\SchoolZone;

use function array_merge;

use DateTimeImmutable;

use function dirname;
use function file_get_contents;
use function file_put_contents;

use InvalidArgumentException;

use function is_dir;
use function json_decode;
use function json_encode;

use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_UNICODE;
use const LOCK_EX;

use function mkdir;

use Override;

use function preg_match;

use Psr\Log\LoggerInterface;
use RuntimeException;

use function sprintf;

use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;

/**
 * Fournit les données officielles du calendrier scolaire depuis un fichier JSON local.
 *
 * Si le fichier n'existe pas pour l'année demandée, les données sont automatiquement
 * récupérées depuis l'API data.education.gouv.fr puis sauvegardées en cache local.
 */
final readonly class JsonOfficialCalendarProvider implements OfficialCalendarProvider
{
    private const string API_BASE_URL = 'https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-calendrier-scolaire/records';

    public function __construct(
        private string $dataDirectory,
        private HttpClientInterface $httpClient,
        private FrenchPublicHolidaysCalculator $holidaysCalculator,
        private LoggerInterface $logger,
    ) {
    }

    #[Override]
    public function joursFeries(string $academicYear): array
    {
        $data = $this->loadData($academicYear);

        return array_map(
            static fn (array $holiday): CalendarEntry => new CalendarEntry(
                id: CalendarEntryId::generate(),
                type: CalendarEntryType::HOLIDAY,
                startDate: new DateTimeImmutable($holiday['date']),
                endDate: new DateTimeImmutable($holiday['date']),
                label: $holiday['label'],
            ),
            $data['holidays'],
        );
    }

    #[Override]
    public function vacancesParZone(SchoolZone $zone, string $academicYear): array
    {
        $data = $this->loadData($academicYear);
        $vacations = $data['vacations'][$zone->value] ?? [];

        return array_map(
            static fn (array $vacation): CalendarEntry => new CalendarEntry(
                id: CalendarEntryId::generate(),
                type: CalendarEntryType::VACATION,
                startDate: new DateTimeImmutable($vacation['start']),
                endDate: new DateTimeImmutable($vacation['end']),
                label: $vacation['label'],
            ),
            $vacations,
        );
    }

    #[Override]
    public function toutesEntreesOfficielles(SchoolZone $zone, string $academicYear): array
    {
        return array_merge(
            $this->joursFeries($academicYear),
            $this->vacancesParZone($zone, $academicYear),
        );
    }

    /**
     * @return array{holidays: list<array{date: string, label: string}>, vacations: array<string, list<array{start: string, end: string, label: string}>>}
     */
    private function loadData(string $academicYear): array
    {
        if (preg_match('/^\d{4}-\d{4}$/', $academicYear) !== 1) {
            throw new InvalidArgumentException(sprintf(
                'Format d\'année scolaire invalide : "%s". Attendu : "YYYY-YYYY".',
                $academicYear,
            ));
        }

        $filePath = sprintf('%s/official-holidays-%s.json', $this->dataDirectory, $academicYear);

        $content = @file_get_contents($filePath);

        if ($content === false) {
            $this->fetchAndSave($academicYear, $filePath);
            $content = file_get_contents($filePath);

            if ($content === false) {
                throw new RuntimeException(sprintf(
                    'Impossible de lire le fichier calendrier : %s',
                    $filePath,
                ));
            }
        }

        /** @var array{holidays: list<array{date: string, label: string}>, vacations: array<string, list<array{start: string, end: string, label: string}>>} $data */
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        return $data;
    }

    private function fetchAndSave(string $academicYear, string $filePath): void
    {
        $this->logger->info('Fichier calendrier absent, récupération depuis l\'API gouv.fr', [
            'academic_year' => $academicYear,
        ]);

        try {
            $vacations = $this->fetchVacationsFromApi($academicYear);
        } catch (Throwable $e) {
            throw new RuntimeException(sprintf(
                'Impossible de récupérer le calendrier %s depuis l\'API : %s',
                $academicYear,
                $e->getMessage(),
            ), previous: $e);
        }

        $holidays = $this->holidaysCalculator->pourAnneeScolaire($academicYear);

        $data = [
            'academic_year' => $academicYear,
            'holidays' => $holidays,
            'vacations' => $vacations,
        ];

        $directory = dirname($filePath);
        if (!is_dir($directory)) {
            mkdir($directory, 0o755, true);
        }

        $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
        file_put_contents($filePath, $json, LOCK_EX);

        $this->logger->info('Calendrier {year} sauvegardé', [
            'year' => $academicYear,
            'path' => $filePath,
        ]);
    }

    /**
     * @return array<string, list<array{start: string, end: string, label: string}>>
     */
    private function fetchVacationsFromApi(string $academicYear): array
    {
        $where = sprintf(
            'annee_scolaire="%s" AND (zones="Zone A" OR zones="Zone B" OR zones="Zone C")',
            $academicYear,
        );

        $response = $this->httpClient->request('GET', self::API_BASE_URL, [
            'query' => [
                'where' => $where,
                'select' => 'description,start_date,end_date,zones',
                'limit' => 100,
            ],
            'timeout' => 10,
        ]);

        /** @var array{results: list<array{description: string, start_date: string, end_date: string, zones: string}>} $data */
        $data = $response->toArray();

        // Grouper par zone et dédupliquer
        $vacationsByZone = ['A' => [], 'B' => [], 'C' => []];
        $seen = [];

        foreach ($data['results'] as $record) {
            $zone = match ($record['zones']) {
                'Zone A' => 'A',
                'Zone B' => 'B',
                'Zone C' => 'C',
                default => null,
            };

            if ($zone === null) {
                continue;
            }

            // Les dates API sont en ISO 8601 (ex: "2024-12-20T23:00:00+00:00")
            // start_date utilise la convention "veille à 23h UTC" → +1 jour pour obtenir le 1er jour de vacances
            // end_date représente déjà le dernier jour de vacances (pas de décalage)
            $startDate = (new DateTimeImmutable($record['start_date']))->modify('+1 day')->format('Y-m-d');
            $endDate = (new DateTimeImmutable($record['end_date']))->format('Y-m-d');
            $label = $record['description'];

            $key = "$zone|$label|$startDate|$endDate";
            if (isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;

            $vacationsByZone[$zone][] = [
                'start' => $startDate,
                'end' => $endDate,
                'label' => $label,
            ];
        }

        // Trier chaque zone par date de début
        foreach ($vacationsByZone as &$vacations) {
            usort($vacations, static fn (array $a, array $b): int => $a['start'] <=> $b['start']);
        }

        return $vacationsByZone;
    }
}
