<?php

declare(strict_types=1);

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\Absence;
use App\Entity\Employee;
use App\Entity\WorkHour;
use App\Enum\TrackingMode;
use App\Repository\AbsenceRepository;
use App\Repository\BonusRepository;
use App\Repository\EmployeeRepository;
use App\Repository\EmployeeRttPaymentRepository;
use App\Repository\MileageAllowanceRepository;
use App\Repository\ObservationRepository;
use App\Repository\WorkHourRepository;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\PublicHolidayServiceInterface;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\NightHoursCalculator;
use DateInterval;
use DateTimeImmutable;
use Dompdf\Dompdf;
use Dompdf\Options;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
use Twig\Environment;

class SalaryRecapPrintProvider implements ProviderInterface
{
    public function __construct(
        private Environment $twig,
        private readonly RequestStack $requestStack,
        private EmployeeRepository $employeeRepository,
        private WorkHourRepository $workHourRepository,
        private AbsenceRepository $absenceRepository,
        private EmployeeRttPaymentRepository $rttPaymentRepository,
        private BonusRepository $bonusRepository,
        private MileageAllowanceRepository $mileageAllowanceRepository,
        private ObservationRepository $observationRepository,
        private EmployeeContractResolver $contractResolver,
        private PublicHolidayServiceInterface $publicHolidayService,
        private EmployeeLeaveSummaryProvider $leaveSummaryProvider,
        private AbsenceSegmentsResolver $absenceSegmentsResolver,
        private NightHoursCalculator $nightHoursCalculator,
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
    {
        $request = $this->requestStack->getCurrentRequest();
        if (!$request) {
            return new Response('Missing request.', Response::HTTP_BAD_REQUEST);
        }

        $month = $request->query->get('month');
        if (!$month || !preg_match('/^\d{4}-\d{2}$/', $month)) {
            return new Response('Missing or invalid month query param (expected YYYY-MM).', Response::HTTP_BAD_REQUEST);
        }

        $from = DateTimeImmutable::createFromFormat('Y-m-d', $month.'-01');
        $to   = $from->modify('last day of this month');

        // N'inclure que les employés ayant un contrat couvrant tout ou partie du mois.
        // Sans ce filtre, un salarié dont le contrat est terminé (ex. parti en février)
        // apparaît à tort sur le récap des mois suivants.
        $employees = array_values(array_filter(
            $this->employeeRepository->findForPrintBySiteIds([]),
            fn (Employee $employee): bool => $this->hasContractInRange($employee, $from, $to)
        ));
        $workHours = $this->workHourRepository->findByDateRangeAndEmployees($from, $to, $employees);
        $absences  = $this->absenceRepository->findForPrint($from, $to, $employees);

        $year        = (int) $from->format('Y');
        $monthNumber = (int) $from->format('n');

        // Congés depuis le début de l'exercice forfait (année civile) jusqu'à la fin du mois :
        // nécessaires pour consommer chronologiquement le budget N-1 d'un forfait (un congé
        // imputé N-1 ne doit ni s'afficher ni manquer en présence sur le récap).
        $yearStart     = new DateTimeImmutable(sprintf('%d-01-01', $year));
        $ytdAbsences   = $this->absenceRepository->findForPrint($yearStart, $to, $employees);
        $ytdAbsenceMap = $this->buildAbsenceMap($ytdAbsences);
        $rttPayments   = $this->rttPaymentRepository->findByYearAndMonth($year, $monthNumber);

        $bonuses      = $this->bonusRepository->findByMonth($from, $to);
        $mileages     = $this->mileageAllowanceRepository->findByMonth($from, $to);
        $observations = $this->observationRepository->findByMonth($from, $to);

        $days        = $this->buildDays($from, $to);
        $contractMap = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
        $driverMap   = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
        $holidayMap  = $this->buildHolidayMap($from, $to);

        $workHourMap    = $this->buildWorkHourMap($workHours);
        $absenceMap     = $this->buildAbsenceMap($absences);
        $rttPaymentMap  = $this->buildRttPaymentMap($rttPayments);
        $bonusMap       = $this->buildBonusMap($bonuses);
        $mileageMap     = $this->buildMileageMap($mileages);
        $observationMap = $this->buildObservationMap($observations);

        $siteGroups = $this->aggregateBySite($employees, $days, $contractMap, $driverMap, $workHourMap, $absenceMap, $rttPaymentMap, $bonusMap, $mileageMap, $observationMap, $holidayMap, $ytdAbsenceMap, $year, $from, $to);

        $options = new Options();
        $options->set('isRemoteEnabled', true);

        $dompdf = new Dompdf($options);
        $html   = $this->twig->render('salary-recap/print.html.twig', [
            'from'       => $from,
            'to'         => $to,
            'siteGroups' => $siteGroups,
        ]);

        $dompdf->loadHtml($html);
        $dompdf->setPaper('A4', 'landscape');
        $dompdf->render();

        $filename = sprintf(
            'recap_salaire_%s.pdf',
            $from->format('Y-m')
        );

        return new Response($dompdf->output(), Response::HTTP_OK, [
            'Content-Type'        => 'application/pdf',
            'Content-Disposition' => 'inline; filename="'.$filename.'"',
        ]);
    }

    private function hasContractInRange(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): bool
    {
        $fromDay = $from->format('Y-m-d');
        $toDay   = $to->format('Y-m-d');

        foreach ($employee->getContractPeriods() as $period) {
            $start = $period->getStartDate()->format('Y-m-d');
            $end   = $period->getEndDate()?->format('Y-m-d');
            if ($start <= $toDay && (null === $end || $end >= $fromDay)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return list<string>
     */
    private function buildDays(DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $days    = [];
        $current = $from;

        while ($current <= $to) {
            $days[]  = $current->format('Y-m-d');
            $current = $current->add(new DateInterval('P1D'));
        }

        return $days;
    }

    /**
     * @return array<int, array<string, WorkHour>>
     */
    private function buildWorkHourMap(array $workHours): array
    {
        $map = [];
        foreach ($workHours as $wh) {
            $employeeId = $wh->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }
            $date                    = $wh->getWorkDate()->format('Y-m-d');
            $map[$employeeId][$date] = $wh;
        }

        return $map;
    }

    /**
     * @return array<int, list<Absence>>
     */
    private function buildAbsenceMap(array $absences): array
    {
        $map = [];
        foreach ($absences as $absence) {
            $employeeId = $absence->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }
            $map[$employeeId][] = $absence;
        }

        return $map;
    }

    /**
     * @return array<int, int>
     */
    /**
     * @return array<int, array{m25: int, m50: int}>
     */
    private function buildRttPaymentMap(array $rttPayments): array
    {
        $map = [];
        foreach ($rttPayments as $payment) {
            $employeeId = $payment->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }
            $map[$employeeId] ??= ['m25' => 0, 'm50' => 0];
            $map[$employeeId]['m25'] += $payment->getBase25Minutes();
            $map[$employeeId]['m50'] += $payment->getBase50Minutes();
        }

        return $map;
    }

    /**
     * @return array<int, float>
     */
    private function buildBonusMap(array $bonuses): array
    {
        $map = [];
        foreach ($bonuses as $bonus) {
            $employeeId = $bonus->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }
            $map[$employeeId] = ($map[$employeeId] ?? 0.0) + $bonus->getAmount();
        }

        return $map;
    }

    /**
     * @return array<int, float>
     */
    private function buildMileageMap(array $mileages): array
    {
        $map = [];
        foreach ($mileages as $mileage) {
            $employeeId = $mileage->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }
            $map[$employeeId] = ($map[$employeeId] ?? 0.0) + $mileage->getKilometers();
        }

        return $map;
    }

    /**
     * @return array<string, string> Y-m-d → label
     */
    private function buildHolidayMap(DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $map       = [];
        $startYear = (int) $from->format('Y');
        $endYear   = (int) $to->format('Y');

        try {
            for ($year = $startYear; $year <= $endYear; ++$year) {
                $holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year);
                foreach ($holidays as $date => $label) {
                    $map[(string) $date] = (string) $label;
                }
            }
        } catch (Throwable) {
            return [];
        }

        return $map;
    }

    /**
     * @return array<int, string>
     */
    private function buildObservationMap(array $observations): array
    {
        $map = [];
        foreach ($observations as $observation) {
            $employeeId = $observation->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }
            $map[$employeeId] = $observation->getContent();
        }

        return $map;
    }

    private function aggregateBySite(
        array $employees,
        array $days,
        array $contractMap,
        array $driverMap,
        array $workHourMap,
        array $absenceMap,
        array $rttPaymentMap,
        array $bonusMap,
        array $mileageMap,
        array $observationMap,
        array $holidayMap,
        array $ytdAbsenceMap,
        int $year,
        DateTimeImmutable $monthFrom,
        DateTimeImmutable $monthTo,
    ): array {
        $siteGroups = [];

        foreach ($employees as $employee) {
            $employeeId = $employee->getId();
            $site       = $employee->getSite();
            $siteName   = $site ? $site->getName() : 'Sans site';
            $siteId     = $site ? $site->getId() : 0;

            $row = $this->buildEmployeeRow(
                $employee,
                $employeeId,
                $days,
                $contractMap[$employeeId] ?? [],
                $driverMap[$employeeId] ?? [],
                $workHourMap[$employeeId] ?? [],
                $absenceMap[$employeeId] ?? [],
                $rttPaymentMap[$employeeId] ?? ['m25' => 0, 'm50' => 0],
                $bonusMap[$employeeId] ?? 0.0,
                $mileageMap[$employeeId] ?? 0.0,
                $observationMap[$employeeId] ?? '',
                $holidayMap,
                $ytdAbsenceMap[$employeeId] ?? [],
                $year,
                $monthFrom,
                $monthTo,
            );

            if (!isset($siteGroups[$siteId])) {
                $siteGroups[$siteId] = [
                    'name'      => $siteName,
                    'color'     => $site?->getColor() ?? '#ffd7d7',
                    'employees' => [],
                ];
            }

            $siteGroups[$siteId]['employees'][] = $row;
        }

        return $siteGroups;
    }

    private function buildEmployeeRow(
        Employee $employee,
        int $employeeId,
        array $days,
        array $contractsByDate,
        array $driverByDate,
        array $workHoursByDate,
        array $absences,
        array $rttPaid,
        float $bonusAmount,
        float $mileageKm,
        string $observation,
        array $holidayMap,
        array $ytdAbsences,
        int $year,
        DateTimeImmutable $monthFrom,
        DateTimeImmutable $monthTo,
    ): array {
        $contractName        = null;
        $presenceDays        = 0.0;
        $nightMinutesTotal   = 0;
        $nightBasketCount    = 0;
        $sundayMinutesTotal  = 0;
        $holidayMinutesTotal = 0;
        $isDriverAnyDay      = false;
        $driverBreakfast     = 0;
        $driverMeals         = 0;
        $driverOvernight     = 0;
        $driverSaturdays     = 0;
        $isForfait           = false;

        foreach ($days as $date) {
            $contract = $contractsByDate[$date] ?? null;
            $isDriver = $driverByDate[$date] ?? false;
            $wh       = $workHoursByDate[$date] ?? null;

            if ($contract && null === $contractName) {
                $contractName = $contract->getName();
                $isForfait    = TrackingMode::PRESENCE === $contract->getTrackingModeEnum();
            }

            if ($isDriver) {
                $isDriverAnyDay = true;
            }

            if (!$wh) {
                continue;
            }

            $dayOfWeek = (int) new DateTimeImmutable($date)->format('N');

            $isHoliday = isset($holidayMap[$date]);

            if ($isDriver) {
                $nightMinutesTotal += $wh->getNightHoursMinutes() ?? 0;
                $dayMin      = $wh->getDayHoursMinutes() ?? 0;
                $nightMin    = $wh->getNightHoursMinutes() ?? 0;
                $workshopMin = $wh->getWorkshopHoursMinutes() ?? 0;
                // Le panier de nuit ne s'applique pas aux conducteurs (primes repas/nuitée
                // dédiées). Aucun panier de nuit crédité ici.

                if ($wh->getHasBreakfast()) {
                    ++$driverBreakfast;
                }
                if ($wh->getHasLunch()) {
                    ++$driverMeals;
                }
                if ($wh->getHasDinner()) {
                    ++$driverMeals;
                }
                if ($wh->getHasOvernight()) {
                    ++$driverOvernight;
                }

                if (6 === $dayOfWeek && ($dayMin > 0 || $nightMin > 0 || $workshopMin > 0)) {
                    ++$driverSaturdays;
                }

                if (7 === $dayOfWeek) {
                    $sundayMinutesTotal += $dayMin + $nightMin + $workshopMin;
                }

                if ($isHoliday) {
                    $holidayMinutesTotal += $dayMin + $nightMin + $workshopMin;
                }
            } else {
                $metrics = $this->computeNightMinutes($wh);
                $nightMinutesTotal += $metrics['nightMinutes'];
                if (($metrics['nightMinutes'] > $metrics['dayMinutes'] && $metrics['nightMinutes'] > 0) || $metrics['nightMinutes'] >= 240) {
                    ++$nightBasketCount;
                }

                if (7 === $dayOfWeek) {
                    $sundayMinutesTotal += $metrics['dayMinutes'] + $metrics['nightMinutes'];
                }

                // Samedi : les minutes après minuit débordent sur le dimanche
                if (6 === $dayOfWeek) {
                    $sundayMinutesTotal += $this->computeOverflowAfterMidnight($wh);
                }

                if ($isHoliday) {
                    $holidayMinutesTotal += $metrics['dayMinutes'] + $metrics['nightMinutes'];
                }

                if ($isForfait) {
                    if ($wh->getIsPresentMorning()) {
                        $presenceDays += 0.5;
                    }
                    if ($wh->getIsPresentAfternoon()) {
                        $presenceDays += 0.5;
                    }
                }
            }
        }

        // Forfait : un congé imputé sur le stock N-1 ne doit pas s'afficher dans le récap
        // et doit compter comme jour de présence. On consomme le budget N-1 chronologiquement
        // sur tous les congés de l'exercice (année civile) jusqu'à la fin du mois imprimé.
        $n1Budget = $isForfait ? $this->leaveSummaryProvider->resolvePreviousYearTakenDays($employee, $year) : 0.0;
        if ($isForfait && $n1Budget > 0.0) {
            $ytdConges = array_values(array_filter(
                $ytdAbsences,
                static fn (Absence $a): bool => 'C' === $a->getType()?->getCode()
            ));
            $split  = $this->splitForfaitCongesByN1($ytdConges, $n1Budget, $monthFrom, $monthTo);
            $conges = ['count' => $split['count'], 'dates' => $split['dates']];
            $presenceDays += $split['n1PresenceDays'];
        } else {
            $conges = $this->countAbsencesByCode($absences, ['C']);
        }
        $maladie = $this->countAbsencesByCode($absences, ['M', 'AT']);

        $nightHours   = round($nightMinutesTotal / 60, 2);
        $paid25Hours  = round(($rttPaid['m25'] ?? 0) / 60, 2);
        $paid50Hours  = round(($rttPaid['m50'] ?? 0) / 60, 2);
        $sundayHours  = round($sundayMinutesTotal / 60, 2);
        $holidayHours = round($holidayMinutesTotal / 60, 2);

        return [
            'lastName'         => mb_strimwidth($employee->getLastName() ?? '', 0, 15, '...'),
            'firstName'        => mb_strimwidth($employee->getFirstName() ?? '', 0, 15, '...'),
            'contractName'     => $contractName,
            'presenceDays'     => $presenceDays,
            'mileageKm'        => $mileageKm,
            'nightHours'       => $nightHours,
            'nightBasketCount' => $nightBasketCount,
            'paid25Hours'      => $paid25Hours,
            'paid50Hours'      => $paid50Hours,
            'sundayHours'      => $sundayHours,
            'holidayHours'     => $holidayHours,
            'bonusAmount'      => $bonusAmount,
            'congesCount'      => $conges['count'],
            'congesDates'      => $conges['dates'],
            'maladieCount'     => $maladie['count'],
            'maladieDates'     => $maladie['dates'],
            'isDriver'         => $isDriverAnyDay,
            'driverBreakfast'  => $driverBreakfast,
            'driverMeals'      => $driverMeals,
            'driverOvernight'  => $driverOvernight,
            'driverSaturdays'  => $driverSaturdays,
            'observation'      => $observation,
        ];
    }

    /**
     * @return array{nightMinutes: int, dayMinutes: int}
     */
    private function computeNightMinutes(WorkHour $workHour): array
    {
        $ranges = [
            [$workHour->getMorningFrom(), $workHour->getMorningTo()],
            [$workHour->getAfternoonFrom(), $workHour->getAfternoonTo()],
            [$workHour->getEveningFrom(), $workHour->getEveningTo()],
        ];

        $totalMinutes = 0;

        foreach ($ranges as [$from, $to]) {
            $totalMinutes += $this->intervalMinutes($from, $to);
        }

        $nightMinutes = $this->nightHoursCalculator->nightMinutesFromRanges($workHour);
        $dayMinutes   = max(0, $totalMinutes - $nightMinutes);

        return [
            'nightMinutes' => $nightMinutes,
            'dayMinutes'   => $dayMinutes,
        ];
    }

    /**
     * @return null|array{int, int}
     */
    private function resolveInterval(?string $from, ?string $to): ?array
    {
        $fromMinutes = $this->toMinutes($from);
        $toMinutes   = $this->toMinutes($to);
        if (null === $fromMinutes || null === $toMinutes) {
            return null;
        }

        $end = $toMinutes <= $fromMinutes ? $toMinutes + 1440 : $toMinutes;

        return [$fromMinutes, $end];
    }

    private function toMinutes(?string $time): ?int
    {
        if (null === $time || '' === $time) {
            return null;
        }

        [$hours, $minutes] = array_map('intval', explode(':', $time));

        return ($hours * 60) + $minutes;
    }

    private function intervalMinutes(?string $from, ?string $to): int
    {
        $interval = $this->resolveInterval($from, $to);
        if (null === $interval) {
            return 0;
        }

        [$start, $end] = $interval;

        return max(0, $end - $start);
    }

    /**
     * Calcule les minutes qui débordent après minuit (> 1440) pour les créneaux d'un WorkHour.
     * Ex: créneau soir 21:00-05:00 → interval [1260, 1740] → overflow = 1740-1440 = 300 min (5h).
     */
    private function computeOverflowAfterMidnight(WorkHour $workHour): int
    {
        $ranges = [
            [$workHour->getMorningFrom(), $workHour->getMorningTo()],
            [$workHour->getAfternoonFrom(), $workHour->getAfternoonTo()],
            [$workHour->getEveningFrom(), $workHour->getEveningTo()],
        ];

        $overflow = 0;

        foreach ($ranges as [$from, $to]) {
            $interval = $this->resolveInterval($from, $to);
            if (null === $interval) {
                continue;
            }

            [$start, $end] = $interval;

            // Si le créneau dépasse minuit (1440), la partie au-delà est sur le jour suivant
            if ($end > 1440) {
                $overflow += $end - max($start, 1440);
            }
        }

        return $overflow;
    }

    /**
     * Répartit les congés ('C') d'un forfait entre N-1 (budget consommé chronologiquement,
     * non affiché et compté en présence) et N (affiché en congé). Seuls les jours tombant
     * dans le mois imprimé alimentent le retour ; les congés des mois antérieurs ne servent
     * qu'à consommer le budget N-1.
     *
     * @param list<Absence> $ytdConges congés depuis le début d'exercice jusqu'à la fin du mois
     *
     * @return array{count: float, dates: string, n1PresenceDays: float}
     */
    private function splitForfaitCongesByN1(
        array $ytdConges,
        float $n1Budget,
        DateTimeImmutable $monthFrom,
        DateTimeImmutable $monthTo
    ): array {
        usort($ytdConges, static fn (Absence $a, Absence $b): int => $a->getStartDate() <=> $b->getStartDate());

        $remaining      = $n1Budget;
        $count          = 0.0;
        $n1PresenceDays = 0.0;
        $dayKeys        = [];

        foreach ($ytdConges as $absence) {
            $start = DateTimeImmutable::createFromInterface($absence->getStartDate())->setTime(0, 0);
            $end   = DateTimeImmutable::createFromInterface($absence->getEndDate())->setTime(0, 0);

            for ($day = $start; $day <= $end; $day = $day->modify('+1 day')) {
                if ((int) $day->format('N') >= 6) {
                    continue; // week-ends ignorés
                }
                [$am, $pm] = $this->absenceSegmentsResolver->resolveForDate($absence, $day->format('Y-m-d'));
                $amount    = ($am ? 0.5 : 0.0) + ($pm ? 0.5 : 0.0);
                if ($amount <= 0.0) {
                    continue;
                }

                $covered = 0.0;
                if ($remaining > 0.0) {
                    $covered = min($remaining, $amount);
                    $remaining -= $covered;
                }
                $displayed = $amount - $covered;

                // Seul le mois imprimé alimente le récap ; les mois antérieurs ne font que consommer.
                if ($day < $monthFrom || $day > $monthTo) {
                    continue;
                }

                $n1PresenceDays += $covered;
                if ($displayed > 0.0) {
                    $count += $displayed;
                    $dayKeys[] = $day->format('Y-m-d');
                }
            }
        }

        sort($dayKeys);
        $dayKeys = array_unique($dayKeys);

        return [
            'count'          => $count,
            'dates'          => implode(', ', $this->mergeDaysIntoPeriods($dayKeys)),
            'n1PresenceDays' => $n1PresenceDays,
        ];
    }

    /**
     * @param list<Absence> $absences
     * @param list<string>  $codes
     *
     * @return array{count: float, dates: string}
     */
    private function countAbsencesByCode(array $absences, array $codes): array
    {
        $count   = 0.0;
        $dayKeys = [];

        foreach ($absences as $absence) {
            $type = $absence->getType();
            if (!$type || !in_array($type->getCode(), $codes, true)) {
                continue;
            }

            // Un congé (C) posé un dimanche n'est pas décompté comme congé pris : un dimanche
            // ne fait pas partie des congés (cf. récap congés / rollover qui l'ignorent déjà).
            // Le calendrier et son impression continuent d'afficher la ligne (volonté RH).
            // Hors périmètre : maladie/AT et le samedi (budget samedis dédié) sont inchangés.
            if ('C' === $type->getCode() && 7 === (int) $absence->getStartDate()->format('N')) {
                continue;
            }

            $startHalf = $absence->getStartHalf();
            $endHalf   = $absence->getEndHalf();

            if ($startHalf === $endHalf) {
                $count += 0.5;
            } else {
                $count += 1.0;
            }

            $dayKeys[] = $absence->getStartDate()->format('Y-m-d');
        }

        sort($dayKeys);
        $dayKeys = array_unique($dayKeys);

        $periods = $this->mergeDaysIntoPeriods($dayKeys);

        return [
            'count' => $count,
            'dates' => implode(', ', $periods),
        ];
    }

    /**
     * @param list<string> $sortedDates Y-m-d sorted
     *
     * @return list<string>
     */
    private function mergeDaysIntoPeriods(array $sortedDates): array
    {
        if ([] === $sortedDates) {
            return [];
        }

        $periods    = [];
        $rangeStart = $sortedDates[0];
        $rangeEnd   = $sortedDates[0];

        for ($i = 1, $len = count($sortedDates); $i < $len; ++$i) {
            $prev    = new DateTimeImmutable($rangeEnd);
            $current = new DateTimeImmutable($sortedDates[$i]);

            if (1 === $current->diff($prev)->days) {
                $rangeEnd = $sortedDates[$i];
            } else {
                $periods[]  = $this->formatPeriod($rangeStart, $rangeEnd);
                $rangeStart = $sortedDates[$i];
                $rangeEnd   = $sortedDates[$i];
            }
        }

        $periods[] = $this->formatPeriod($rangeStart, $rangeEnd);

        return $periods;
    }

    private function formatPeriod(string $start, string $end): string
    {
        $s = new DateTimeImmutable($start)->format('d/m');

        if ($start === $end) {
            return $s;
        }

        return 'Du '.$s.' au '.new DateTimeImmutable($end)->format('d/m');
    }
}
