<?php

declare(strict_types=1);

namespace App\Command;

use App\Dto\Rtt\EmployeeRttWeekSummary;
use App\Dto\Rtt\WeekRecoveryDetail;
use App\Entity\Employee;
use App\Enum\ContractType;
use App\Enum\TrackingMode;
use App\Repository\AbsenceRepository;
use App\Repository\EmployeeContractPeriodRepository;
use App\Repository\EmployeeRepository;
use App\Repository\EmployeeRttBalanceRepository;
use App\Repository\EmployeeRttPaymentRepository;
use App\Repository\WorkHourRepository;
use App\Service\Leave\LeaveRecapRowBuilder;
use App\Service\Rtt\RttRecoveryComputationService;
use App\State\EmployeeLeaveSummaryProvider;
use DateTimeImmutable;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

#[AsCommand(
    name: 'app:verification:snapshot',
    description: 'Dump per-employee Markdown snapshot of RTT (monthly tab view) and leave balances, to serve as a regression baseline before business-rule refactors.'
)]
final class DumpVerificationSnapshotCommand extends Command
{
    private const array MONTH_LABELS = [
        1 => 'Janvier', 2 => 'Février', 3 => 'Mars', 4 => 'Avril',
        5 => 'Mai', 6 => 'Juin', 7 => 'Juillet', 8 => 'Août',
        9 => 'Septembre', 10 => 'Octobre', 11 => 'Novembre', 12 => 'Décembre',
    ];

    public function __construct(
        private readonly EmployeeRepository $employeeRepository,
        private readonly EmployeeContractPeriodRepository $contractPeriodRepository,
        private readonly EmployeeLeaveSummaryProvider $leaveSummaryProvider,
        private readonly LeaveRecapRowBuilder $leaveRecapRowBuilder,
        private readonly RttRecoveryComputationService $rttRecoveryService,
        private readonly EmployeeRttBalanceRepository $rttBalanceRepository,
        private readonly EmployeeRttPaymentRepository $rttPaymentRepository,
        private readonly WorkHourRepository $workHourRepository,
        private readonly AbsenceRepository $absenceRepository,
        #[Autowire('%kernel.project_dir%')]
        private readonly string $projectDir,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addArgument(
                'employee_ids',
                InputArgument::IS_ARRAY | InputArgument::REQUIRED,
                'Employee IDs to snapshot (space-separated).'
            )
            ->addOption(
                'output-dir',
                null,
                InputOption::VALUE_OPTIONAL,
                'Output directory (relative to project root, or absolute).',
                'docs/verifications'
            )
            ->addOption(
                'rtt-year',
                null,
                InputOption::VALUE_OPTIONAL,
                'RTT exercise year (ending year, e.g. 2026 = June 2025 → May 2026). Defaults to current exercise.'
            )
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io  = new SymfonyStyle($input, $output);
        $ids = array_map('intval', $input->getArgument('employee_ids'));

        $outputDirOpt = (string) $input->getOption('output-dir');
        $outputDir    = str_starts_with($outputDirOpt, '/')
            ? $outputDirOpt
            : $this->projectDir.'/'.$outputDirOpt;

        if (!is_dir($outputDir) && !mkdir($outputDir, 0o755, true) && !is_dir($outputDir)) {
            $io->error('Could not create output directory: '.$outputDir);

            return Command::FAILURE;
        }

        $today      = new DateTimeImmutable('today');
        $rttYearOpt = $input->getOption('rtt-year');
        $rttYear    = null !== $rttYearOpt && '' !== (string) $rttYearOpt
            ? (int) $rttYearOpt
            : $this->resolveCurrentRttExerciseYear($today);

        foreach ($ids as $id) {
            $employee = $this->employeeRepository->find($id);
            if (!$employee instanceof Employee) {
                $io->warning(sprintf('Employee id=%d not found — skipped.', $id));

                continue;
            }

            $markdown = $this->buildEmployeeDoc($employee, $rttYear, $today);
            $slug     = $this->slugify($employee->getFirstName().'-'.$employee->getLastName());
            $filename = sprintf('%s/verification-rtt-conges-%s.md', $outputDir, $slug);
            file_put_contents($filename, $markdown);
            $io->success(sprintf('Wrote %s', $filename));
        }

        return Command::SUCCESS;
    }

    private function buildEmployeeDoc(Employee $employee, int $rttYear, DateTimeImmutable $today): string
    {
        $parts   = [];
        $parts[] = $this->buildHeader($employee, $rttYear, $today);
        $parts[] = $this->buildProfileSection($employee);
        $parts[] = $this->buildLeaveSection($employee, $today);
        $parts[] = $this->buildRecapRowSection($employee, $today);
        $parts[] = $this->buildRttSection($employee, $rttYear, $today);

        return implode("\n\n", $parts)."\n";
    }

    private function buildHeader(Employee $employee, int $rttYear, DateTimeImmutable $today): string
    {
        $rttFrom = sprintf('01/06/%d', $rttYear - 1);
        $rttTo   = sprintf('31/05/%d', $rttYear);

        return sprintf(
            "# Vérification RTT & Congés — %s %s (id=%d)\n\n"
            ."Généré le %s.  \n"
            ."Exercice RTT de référence : **%d** (%s → %s).  \n"
            ."Pour les contrats Forfait, l'exercice de congés est l'année civile.",
            $employee->getFirstName(),
            $employee->getLastName(),
            (int) $employee->getId(),
            $today->format('Y-m-d'),
            $rttYear,
            $rttFrom,
            $rttTo
        );
    }

    private function buildProfileSection(Employee $employee): string
    {
        $contract     = $employee->getContract();
        $contractName = $contract?->getName() ?? '—';
        $tracking     = $contract?->getTrackingMode() ?? '—';
        $weekly       = $contract?->getWeeklyHours();
        $weeklyLabel  = null === $weekly ? '—' : ($weekly.'h');
        $nature       = $employee->getCurrentContractNature();

        $lines   = [];
        $lines[] = '## 1. Profil';
        $lines[] = '';
        $lines[] = sprintf('- **ID** : %d', (int) $employee->getId());
        $lines[] = sprintf('- **Nom / Prénom** : %s %s', $employee->getLastName(), $employee->getFirstName());
        $lines[] = sprintf('- **Contrat actif** : %s — tracking `%s` — %s', $contractName, $tracking, $weeklyLabel);
        $lines[] = sprintf('- **Nature** : %s', $nature);

        $lines[] = '';
        $lines[] = '### Périodes de contrat';
        $lines[] = '';
        $lines[] = '| Début | Fin | Contrat | Nature | Conducteur | Solde CP soldé | Commentaire |';
        $lines[] = '|-------|-----|---------|--------|------------|----------------|-------------|';

        $periods = $this->contractPeriodRepository->findBy(['employee' => $employee], ['startDate' => 'ASC']);
        foreach ($periods as $period) {
            $lines[] = sprintf(
                '| %s | %s | %s | %s | %s | %s | %s |',
                $period->getStartDate()->format('Y-m-d'),
                null !== $period->getEndDate() ? $period->getEndDate()->format('Y-m-d') : '—',
                $period->getContract()?->getName() ?? '—',
                $period->getContractNature(),
                $period->getIsDriver() ? 'oui' : 'non',
                $period->isPaidLeaveSettled() ? 'oui' : 'non',
                str_replace("\n", ' ', (string) ($period->getComment() ?? ''))
            );
        }

        return implode("\n", $lines);
    }

    private function buildLeaveSection(Employee $employee, DateTimeImmutable $today): string
    {
        $lines   = [];
        $lines[] = '## 2. Congés';
        $lines[] = '';

        $leaveYear   = $this->leaveSummaryProvider->resolveLeaveYearForToday($employee);
        $isForfait   = ContractType::FORFAIT === $employee->getContract()?->getType();
        $yearSummary = $this->leaveSummaryProvider->computeYearSummary($employee, $leaveYear);

        if (null === $yearSummary) {
            $lines[] = '_Aucun résumé congés disponible (contrat non supporté : INTERIM ou autre)._';

            return implode("\n", $lines);
        }

        // Forfait: recompute with paid leave days if any.
        $paidLeaveDays = $this->leaveSummaryProvider->resolvePaidLeaveDays($employee, $yearSummary['ruleCode'], $leaveYear);
        if ($paidLeaveDays > 0.0) {
            $recomputed = $this->leaveSummaryProvider->computeYearSummary($employee, $leaveYear, $paidLeaveDays);
            if (null !== $recomputed) {
                $yearSummary = $recomputed;
            }
        }

        [$from, $to] = $isForfait
            ? [
                new DateTimeImmutable(sprintf('%d-01-01', $leaveYear)),
                new DateTimeImmutable(sprintf('%d-12-31', $leaveYear)),
            ]
            : [
                new DateTimeImmutable(sprintf('%d-06-01', $leaveYear - 1)),
                new DateTimeImmutable(sprintf('%d-05-31', $leaveYear)),
            ];

        $lines[] = sprintf('**Règle applicable** : `%s`', $yearSummary['ruleCode']);
        $lines[] = sprintf('**Période** : %s → %s', $from->format('Y-m-d'), $to->format('Y-m-d'));
        $lines[] = '';
        $lines[] = '### 2.1 Soldes (tels que calculés aujourd\'hui)';
        $lines[] = '';
        $lines[] = '| Indicateur | Valeur |';
        $lines[] = '|------------|--------|';
        $lines[] = sprintf('| Acquis (report N-1) | %s j |', $this->fmtDays($yearSummary['acquiredDays']));
        $lines[] = sprintf('| Acquis samedis | %s j |', $this->fmtDays($yearSummary['acquiredSaturdays']));
        $lines[] = sprintf('| En cours d\'acquisition | %s j |', $this->fmtDays($yearSummary['accruingDays']));
        $lines[] = sprintf('| Pris | %s j |', $this->fmtDays($yearSummary['takenDays']));
        $lines[] = sprintf('| Pris samedis | %s j |', $this->fmtDays($yearSummary['takenSaturdays']));
        $lines[] = sprintf('| Restant (report N-1) | %s j |', $this->fmtDays($yearSummary['remainingDays']));
        $lines[] = sprintf('| Restant samedis | %s j |', $this->fmtDays($yearSummary['remainingSaturdays']));
        if ($isForfait) {
            $lines[] = sprintf('| N-1 acquis | %s j |', $this->fmtDays($yearSummary['previousYearAcquiredDays']));
            $lines[] = sprintf('| N-1 pris | %s j |', $this->fmtDays($yearSummary['previousYearTakenDays']));
            $lines[] = sprintf('| N-1 restant | %s j |', $this->fmtDays($yearSummary['previousYearRemainingDays']));
            $lines[] = sprintf('| N-1 payés | %s j |', $this->fmtDays($paidLeaveDays));
        }

        $lines[]  = '';
        $lines[]  = '### 2.2 Absences de la période';
        $lines[]  = '';
        $absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $from, $to);
        if ([] === $absences) {
            $lines[] = '_Aucune absence sur la période._';
        } else {
            $lines[] = '| Début | Fin | Demi-début | Demi-fin | Type | Commentaire |';
            $lines[] = '|-------|-----|------------|----------|------|-------------|';
            foreach ($absences as $absence) {
                $lines[] = sprintf(
                    '| %s | %s | %s | %s | %s (%s) | %s |',
                    $absence->getStartDate()->format('Y-m-d'),
                    $absence->getEndDate()->format('Y-m-d'),
                    $absence->getStartHalf()->value,
                    $absence->getEndHalf()->value,
                    $absence->getType()?->getCode() ?? '—',
                    $absence->getType()?->getLabel() ?? '—',
                    str_replace("\n", ' ', (string) ($absence->getComment() ?? ''))
                );
            }
        }

        $lines[]             = '';
        $lines[]             = '### 2.3 Jours de présence par mois (calcul provider)';
        $lines[]             = '';
        $presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $leaveYear);
        if ([] === $presenceDaysByMonth) {
            $lines[] = '_Aucun jour de présence sur la période._';
        } else {
            $lines[] = '| Mois | Jours de présence |';
            $lines[] = '|------|-------------------|';
            ksort($presenceDaysByMonth);
            foreach ($presenceDaysByMonth as $monthKey => $days) {
                $lines[] = sprintf('| %s | %s |', $monthKey, $this->fmtDays($days));
            }
        }

        return implode("\n", $lines);
    }

    /**
     * @return array<string, float>
     */
    private function computePresenceDaysByMonth(Employee $employee, int $leaveYear): array
    {
        // The provider method is private; we re-invoke `provide()` via its public path by
        // calling computeYearSummary then reading $summary->presenceDaysByMonth.
        // But computeYearSummary doesn't populate that. So we call the provider publicly
        // through LeaveRecapRowBuilder? No — we just call the summary API resource directly
        // via a small helper below.
        //
        // Workaround: reuse the provider's provide() would require security; instead we
        // rebuild the map from WorkHour/absences here, mirroring the provider logic.
        $isForfait   = ContractType::FORFAIT === $employee->getContract()?->getType();
        [$from, $to] = $isForfait
            ? [
                new DateTimeImmutable(sprintf('%d-01-01', $leaveYear)),
                new DateTimeImmutable(sprintf('%d-12-31', $leaveYear)),
            ]
            : [
                new DateTimeImmutable(sprintf('%d-06-01', $leaveYear - 1)),
                new DateTimeImmutable(sprintf('%d-05-31', $leaveYear)),
            ];

        // Leave this aggregated figure available only for forfait (this is where the UI
        // shows it). For non-forfait we skip — the UI doesn't show presence per month.
        if (!$isForfait) {
            return [];
        }

        $weekendWorkedDays = $this->workHourRepository->countWeekendWorkedDaysByMonth($employee, $from, $to);
        $absences          = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $from, $to);

        $absenceDaysByMonth = [];
        foreach ($absences 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;
                }
                $startDate = $absence->getStartDate()->format('Y-m-d');
                $endDate   = $absence->getEndDate()->format('Y-m-d');
                $startHalf = $absence->getStartHalf()->value;
                $endHalf   = $absence->getEndHalf()->value;
                $dateStr   = $day->format('Y-m-d');
                $isStart   = $dateStr === $startDate;
                $isEnd     = $dateStr === $endDate;
                if ($startDate === $endDate) {
                    $am = 'AM' === $startHalf;
                    $pm = 'PM' === $endHalf;
                } elseif ($isStart) {
                    $am = 'AM' === $startHalf;
                    $pm = true;
                } elseif ($isEnd) {
                    $am = true;
                    $pm = 'PM' === $endHalf;
                } else {
                    $am = true;
                    $pm = true;
                }
                $dayAmount = ($am ? 0.5 : 0.0) + ($pm ? 0.5 : 0.0);
                if ($dayAmount <= 0.0) {
                    continue;
                }
                $mk                      = $day->format('Y-m');
                $absenceDaysByMonth[$mk] = ($absenceDaysByMonth[$mk] ?? 0.0) + $dayAmount;
            }
        }

        $result = [];
        $cursor = $from->modify('first day of this month')->setTime(0, 0);
        while ($cursor <= $to) {
            $monthKey   = $cursor->format('Y-m');
            $monthStart = $cursor < $from ? $from : $cursor;
            $monthEnd   = $cursor->modify('last day of this month')->setTime(0, 0);
            if ($monthEnd > $to) {
                $monthEnd = $to;
            }
            $businessDays = 0;
            for ($day = $monthStart; $day <= $monthEnd; $day = $day->modify('+1 day')) {
                if ((int) $day->format('N') <= 5) {
                    ++$businessDays;
                }
            }
            $weekend  = $weekendWorkedDays[$monthKey] ?? 0.0;
            $absenced = $absenceDaysByMonth[$monthKey] ?? 0.0;
            $presence = max(0.0, (float) $businessDays + $weekend - $absenced);
            if ($presence > 0.0) {
                $result[$monthKey] = $presence;
            }
            $cursor = $cursor->modify('first day of next month');
        }

        return $result;
    }

    private function buildRecapRowSection(Employee $employee, DateTimeImmutable $today): string
    {
        $row   = $this->leaveRecapRowBuilder->build($employee);
        $lines = [];

        $lines[] = '## 3. Ligne écran « Récap. congés » (live, as of today)';
        $lines[] = '';
        $lines[] = '| CP N-1 restant | CP N | Samedis | RTT |';
        $lines[] = '|----------------|------|---------|-----|';
        $lines[] = sprintf(
            '| %s | %s | %s | %s |',
            (string) $row['cpN1Remaining'],
            $row['cpN'],
            $row['acquiredSaturdays'],
            $row['rtt']
        );

        return implode("\n", $lines);
    }

    private function buildRttSection(Employee $employee, int $rttYear, DateTimeImmutable $today): string
    {
        $lines   = [];
        $lines[] = '## 4. RTT — Onglet par mois';
        $lines[] = '';

        $contract     = $employee->getContract();
        $trackingMode = $contract?->getTrackingMode();
        if (TrackingMode::PRESENCE->value === $trackingMode) {
            $lines[] = '_Contrat en mode `PRESENCE` (Forfait) : aucun calcul RTT (heures supplémentaires)._';
            $lines[] = '_Sur l\'UI, l\'onglet RTT ne contient aucune donnée exploitable._';
            $lines[] = '';
            $lines[] = '> Voir toutefois la section Congés pour les bonus week-end / jours fériés travaillés intégrés au stock Forfait (acquisDays).';

            return implode("\n", $lines);
        }

        [$periodFrom, $periodTo] = $this->rttRecoveryService->resolveExerciseBounds($rttYear);
        $weeks                   = $this->rttRecoveryService->buildWeeksForExercise($periodFrom, $periodTo);
        $weekRanges              = array_map(
            static fn (array $w): array => ['weekNumber' => (int) $w['weekNumber'], 'start' => $w['start'], 'end' => $w['end']],
            $weeks
        );

        $currentExerciseYear = $this->resolveCurrentRttExerciseYear($today);
        if ($rttYear > $currentExerciseYear) {
            $limitDate = $periodFrom->modify('-1 day');
        } else {
            $isoDay    = (int) $today->format('N');
            $limitDate = 7 === $isoDay ? $today : $today->modify('last sunday');
            if (7 !== $isoDay) {
                $currentWeekStart = $today->modify('monday this week');
                $currentWeekEnd   = $currentWeekStart->modify('+6 days');
                $checkEnd         = $this->resolveWeekEndForEmployee($employee, $currentWeekStart, $currentWeekEnd, $today);
                if ($this->workHourRepository->isWeekFullyValidated($employee, $currentWeekStart, $checkEnd)) {
                    $limitDate = $currentWeekEnd;
                }
            }
        }

        $recoveryByWeek       = $this->rttRecoveryService->computeRecoveryByWeek($employee, $weekRanges, $periodFrom, $periodTo, $limitDate);
        [$carry, $carryMonth] = $this->resolveCarry($employee, $rttYear);
        $weekSummaries        = $this->buildWeekSummaries($weekRanges, $recoveryByWeek, $periodFrom, $periodTo);
        $weekSummaries        = $this->distributeDeficits($weekSummaries, $carry);

        // Aggregate payments per month.
        $paymentsByMonth = [];
        foreach ($this->rttPaymentRepository->findByEmployeeAndYear($employee, $rttYear) as $payment) {
            $m = $payment->getMonth();
            if (!isset($paymentsByMonth[$m])) {
                $paymentsByMonth[$m] = ['base25' => 0, 'bonus25' => 0, 'base50' => 0, 'bonus50' => 0];
            }
            $paymentsByMonth[$m]['base25'] += $payment->getBase25Minutes();
            $paymentsByMonth[$m]['bonus25'] += $payment->getBonus25Minutes();
            $paymentsByMonth[$m]['base50'] += $payment->getBase50Minutes();
            $paymentsByMonth[$m]['bonus50'] += $payment->getBonus50Minutes();
        }

        $lines[] = sprintf('**Limite des semaines prises en compte** : %s (exclut la semaine en cours incomplète)', $limitDate->format('Y-m-d'));
        $lines[] = sprintf('**Report N-1 (carry)** : `Base 25%%=%s` / `+25%%=%s` / `Base 50%%=%s` / `+50%%=%s` — **Total %s**', $this->fmtMin($carry->base25Minutes), $this->fmtMin($carry->bonus25Minutes), $this->fmtMin($carry->base50Minutes), $this->fmtMin($carry->bonus50Minutes), $this->fmtMin($carry->totalMinutes));
        $lines[] = '';

        // Iterate the 12 exercise months (June → May).
        $cumulativeCarry = [
            'base25'  => $carry->base25Minutes,
            'bonus25' => $carry->bonus25Minutes,
            'base50'  => $carry->base50Minutes,
            'bonus50' => $carry->bonus50Minutes,
        ];

        $monthsInExercise = [6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5];
        foreach ($monthsInExercise as $i => $month) {
            $calYear = $month >= 6 ? $rttYear - 1 : $rttYear;
            $label   = self::MONTH_LABELS[$month].' '.$calYear;

            $lines[] = '### '.$label;
            $lines[] = '';
            $lines[] = '| Ligne | Heure | Base 25% | +25% | Total 25% | Base 50% | +50% | Total 50% | Total |';
            $lines[] = '|-------|-------|----------|------|-----------|----------|------|-----------|-------|';

            // Report line only on the first month (June).
            if (6 === $month) {
                $lines[] = sprintf(
                    '| Report N-1 | | %s | %s | %s | %s | %s | %s | %s |',
                    $this->fmtMin($carry->base25Minutes),
                    $this->fmtMin($carry->bonus25Minutes),
                    $this->fmtMin($carry->base25Minutes + $carry->bonus25Minutes),
                    $this->fmtMin($carry->base50Minutes),
                    $this->fmtMin($carry->bonus50Minutes),
                    $this->fmtMin($carry->base50Minutes + $carry->bonus50Minutes),
                    $this->fmtMin($carry->totalMinutes),
                );
            }

            $monthWeeks = array_values(array_filter($weekSummaries, static fn (EmployeeRttWeekSummary $w): bool => $w->month === $month));
            $totals     = ['over' => 0, 'b25' => 0, 's25' => 0, 'b50' => 0, 's50' => 0, 'total' => 0];

            foreach ($monthWeeks as $w) {
                $lines[] = sprintf(
                    '| Semaine %d (%s → %s) | %s | %s | %s | %s | %s | %s | %s | %s |',
                    $w->weekNumber,
                    $w->weekStart,
                    $w->weekEnd,
                    $this->fmtMin($w->overtimeMinutes),
                    $this->fmtMin($w->base25Minutes),
                    $this->fmtMin($w->bonus25Minutes),
                    $this->fmtMin($w->base25Minutes + $w->bonus25Minutes),
                    $this->fmtMin($w->base50Minutes),
                    $this->fmtMin($w->bonus50Minutes),
                    $this->fmtMin($w->base50Minutes + $w->bonus50Minutes),
                    $this->fmtMin($w->totalMinutes),
                );
                $totals['over'] += $w->overtimeMinutes;
                $totals['b25'] += $w->base25Minutes;
                $totals['s25'] += $w->bonus25Minutes;
                $totals['b50'] += $w->base50Minutes;
                $totals['s50'] += $w->bonus50Minutes;
                $totals['total'] += $w->totalMinutes;
            }

            if ([] === $monthWeeks && 6 !== $month) {
                $lines[] = '| _aucune semaine_ | | | | | | | | |';
            }

            $lines[] = sprintf(
                '| **Total** | %s | %s | %s | %s | %s | %s | %s | **%s** |',
                $this->fmtMin($totals['over']),
                $this->fmtMin($totals['b25']),
                $this->fmtMin($totals['s25']),
                $this->fmtMin($totals['b25'] + $totals['s25']),
                $this->fmtMin($totals['b50']),
                $this->fmtMin($totals['s50']),
                $this->fmtMin($totals['b50'] + $totals['s50']),
                $this->fmtMin($totals['total']),
            );

            $p          = $paymentsByMonth[$month] ?? null;
            $hasPayment = null !== $p;
            if ($hasPayment) {
                $lines[] = sprintf(
                    '| Payé | | -%s | -%s | -%s | -%s | -%s | -%s | -%s |',
                    $this->fmtMin($p['base25']),
                    $this->fmtMin($p['bonus25']),
                    $this->fmtMin($p['base25'] + $p['bonus25']),
                    $this->fmtMin($p['base50']),
                    $this->fmtMin($p['bonus50']),
                    $this->fmtMin($p['base50'] + $p['bonus50']),
                    $this->fmtMin($p['base25'] + $p['bonus25'] + $p['base50'] + $p['bonus50']),
                );
            } else {
                $lines[] = '| Payé | | 0h | 0h | 0h | 0h | 0h | 0h | 0h |';
            }

            // Cumulative carry update — add month totals, subtract payments.
            $cumulativeCarry['base25'] += $totals['b25'] - ($p['base25'] ?? 0);
            $cumulativeCarry['bonus25'] += $totals['s25'] - ($p['bonus25'] ?? 0);
            $cumulativeCarry['base50'] += $totals['b50'] - ($p['base50'] ?? 0);
            $cumulativeCarry['bonus50'] += $totals['s50'] - ($p['bonus50'] ?? 0);

            $cb25    = $cumulativeCarry['base25'];
            $cs25    = $cumulativeCarry['bonus25'];
            $cb50    = $cumulativeCarry['base50'];
            $cs50    = $cumulativeCarry['bonus50'];
            $cTotal  = $cb25 + $cs25 + $cb50 + $cs50;
            $lines[] = sprintf(
                '| **Reste (cumul)** | | %s | %s | %s | %s | %s | %s | **%s** |',
                $this->fmtMin($cb25),
                $this->fmtMin($cs25),
                $this->fmtMin($cb25 + $cs25),
                $this->fmtMin($cb50),
                $this->fmtMin($cs50),
                $this->fmtMin($cb50 + $cs50),
                $this->fmtMin($cTotal),
            );

            $lines[] = '';
        }

        // Final summary.
        $currentYearRecovery = array_sum(array_map(static fn (EmployeeRttWeekSummary $w): int => $w->totalMinutes, $weekSummaries));
        $totalPaid           = 0;
        foreach ($paymentsByMonth as $p) {
            $totalPaid += $p['base25'] + $p['bonus25'] + $p['base50'] + $p['bonus50'];
        }
        $available = $carry->totalMinutes + $currentYearRecovery - $totalPaid;

        $lines[] = '### Solde RTT total (fin de période calculée)';
        $lines[] = '';
        $lines[] = sprintf('- Report N-1 (opening) : **%s**', $this->fmtMin($carry->totalMinutes));
        $lines[] = sprintf('- Cumul récupération exercice : **%s**', $this->fmtMin($currentYearRecovery));
        $lines[] = sprintf('- Total payé : **%s**', $this->fmtMin($totalPaid));
        $lines[] = sprintf('- **Disponible** : **%s**', $this->fmtMin($available));

        return implode("\n", $lines);
    }

    /**
     * Mirrors EmployeeRttSummaryProvider::buildWeekSummaries().
     *
     * @param list<array{weekNumber:int,start:DateTimeImmutable,end:DateTimeImmutable}> $weekRanges
     * @param array<string, WeekRecoveryDetail>                                         $recoveryByWeek
     *
     * @return list<EmployeeRttWeekSummary>
     */
    private function buildWeekSummaries(array $weekRanges, array $recoveryByWeek, DateTimeImmutable $periodFrom, DateTimeImmutable $periodTo): array
    {
        $result = [];
        foreach ($weekRanges as $week) {
            $weekStart = $week['start'];
            $weekEnd   = $week['end'];
            $weekKey   = $weekStart->format('Y-m-d');
            $detail    = $recoveryByWeek[$weekKey] ?? new WeekRecoveryDetail();

            $effectiveStart = $weekStart < $periodFrom ? $periodFrom : $weekStart;
            $effectiveEnd   = $weekEnd > $periodTo ? $periodTo : $weekEnd;
            $startMonth     = (int) $effectiveStart->format('n');
            $endMonth       = (int) $effectiveEnd->format('n');

            if ($startMonth === $endMonth) {
                $result[] = new EmployeeRttWeekSummary(
                    month: $startMonth,
                    weekNumber: (int) $week['weekNumber'],
                    weekStart: $weekStart->format('Y-m-d'),
                    weekEnd: $weekEnd->format('Y-m-d'),
                    overtimeMinutes: $detail->overtimeMinutes,
                    base25Minutes: $detail->base25Minutes,
                    bonus25Minutes: $detail->bonus25Minutes,
                    base50Minutes: $detail->base50Minutes,
                    bonus50Minutes: $detail->bonus50Minutes,
                    totalMinutes: $detail->totalMinutes,
                    isFlatRecovery: $detail->isFlatRecovery,
                );

                continue;
            }

            $monthMinutes  = [];
            $monthWeekdays = [];
            foreach ($detail->dailyMinutes as $date => $mins) {
                $m                = (int) new DateTimeImmutable($date)->format('n');
                $monthMinutes[$m] = ($monthMinutes[$m] ?? 0) + $mins;
                if ((int) new DateTimeImmutable($date)->format('N') < 6) {
                    $monthWeekdays[$m] = ($monthWeekdays[$m] ?? 0) + 1;
                }
            }
            $totalWorked   = array_sum($monthMinutes);
            $totalWeekdays = array_sum($monthWeekdays);

            foreach ([$startMonth, $endMonth] as $m) {
                if ($totalWorked > 0) {
                    $ratio = ($monthMinutes[$m] ?? 0) / $totalWorked;
                } elseif ($totalWeekdays > 0) {
                    $ratio = ($monthWeekdays[$m] ?? 0) / $totalWeekdays;
                } else {
                    $ratio = 0.0;
                }
                $result[] = new EmployeeRttWeekSummary(
                    month: $m,
                    weekNumber: (int) $week['weekNumber'],
                    weekStart: $weekStart->format('Y-m-d'),
                    weekEnd: $weekEnd->format('Y-m-d'),
                    overtimeMinutes: (int) round($detail->overtimeMinutes * $ratio),
                    base25Minutes: (int) round($detail->base25Minutes * $ratio),
                    bonus25Minutes: (int) round($detail->bonus25Minutes * $ratio),
                    base50Minutes: (int) round($detail->base50Minutes * $ratio),
                    bonus50Minutes: (int) round($detail->bonus50Minutes * $ratio),
                    totalMinutes: (int) round($detail->totalMinutes * $ratio),
                    isFlatRecovery: $detail->isFlatRecovery,
                );
            }
        }

        return $result;
    }

    /**
     * Mirrors the deficit-distribution step in EmployeeRttSummaryProvider::provide().
     *
     * @param list<EmployeeRttWeekSummary> $weeks
     *
     * @return list<EmployeeRttWeekSummary>
     */
    private function distributeDeficits(array $weeks, WeekRecoveryDetail $carry): array
    {
        $cumulative50 = $carry->base50Minutes + $carry->bonus50Minutes;
        $cumulative25 = $carry->base25Minutes + $carry->bonus25Minutes;

        foreach ($weeks as $i => $week) {
            if ($week->totalMinutes >= 0 || $week->isFlatRecovery) {
                $cumulative50 += $week->base50Minutes + $week->bonus50Minutes;
                $cumulative25 += $week->base25Minutes + $week->bonus25Minutes;

                continue;
            }
            $deficit = -$week->totalMinutes;
            $from50  = min($deficit, max(0, $cumulative50));
            $from25  = $deficit - $from50;
            $cumulative50 -= $from50;
            $cumulative25 -= $from25;
            $weeks[$i] = new EmployeeRttWeekSummary(
                month: $week->month,
                weekNumber: $week->weekNumber,
                weekStart: $week->weekStart,
                weekEnd: $week->weekEnd,
                overtimeMinutes: $week->overtimeMinutes,
                base25Minutes: $from25 > 0 ? -$from25 : 0,
                bonus25Minutes: 0,
                base50Minutes: $from50 > 0 ? -$from50 : 0,
                bonus50Minutes: 0,
                totalMinutes: $week->totalMinutes,
                isFlatRecovery: $week->isFlatRecovery,
            );
        }

        return $weeks;
    }

    /**
     * @return array{WeekRecoveryDetail, int}
     */
    private function resolveCarry(Employee $employee, int $year): array
    {
        $balance = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $year);
        if (null !== $balance) {
            return [
                new WeekRecoveryDetail(
                    base25Minutes: $balance->getOpeningBase25Minutes(),
                    bonus25Minutes: $balance->getOpeningBonus25Minutes(),
                    base50Minutes: $balance->getOpeningBase50Minutes(),
                    bonus50Minutes: $balance->getOpeningBonus50Minutes(),
                    totalMinutes: $balance->getTotalOpeningMinutes(),
                ),
                $balance->getMonth(),
            ];
        }

        return [$this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $year - 1), 5];
    }

    private function resolveWeekEndForEmployee(Employee $employee, DateTimeImmutable $weekStart, DateTimeImmutable $weekEnd, DateTimeImmutable $today): DateTimeImmutable
    {
        foreach ($employee->getContractPeriods() as $period) {
            if ($period->getStartDate() > $today) {
                continue;
            }
            $endDate = $period->getEndDate();
            if (null === $endDate) {
                continue;
            }
            if ($endDate >= $weekStart && $endDate <= $weekEnd) {
                return $endDate;
            }
        }

        return $weekEnd;
    }

    private function resolveCurrentRttExerciseYear(DateTimeImmutable $today): int
    {
        $y = (int) $today->format('Y');
        $m = (int) $today->format('n');

        return $m >= 6 ? $y + 1 : $y;
    }

    private function fmtMin(int $minutes): string
    {
        if (0 === $minutes) {
            return '0h';
        }
        $sign = $minutes < 0 ? '-' : '';
        $abs  = abs($minutes);
        $h    = intdiv($abs, 60);
        $m    = $abs % 60;

        return 0 === $m ? sprintf('%s%dh', $sign, $h) : sprintf('%s%dh%02d', $sign, $h, $m);
    }

    private function fmtDays(float $value): string
    {
        if (abs($value - round($value)) < 0.001) {
            return (string) (int) round($value);
        }

        return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
    }

    private function slugify(string $value): string
    {
        $value = trim($value);
        $ascii = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
        if (false === $ascii) {
            $ascii = $value;
        }
        $ascii = strtolower($ascii);
        $ascii = preg_replace('/[^a-z0-9]+/', '-', $ascii) ?? $ascii;

        return trim($ascii, '-');
    }
}
