<?php

declare(strict_types=1);

namespace App\Service\WorkHours;

use App\Dto\WorkHours\WorkMetrics;
use App\Entity\Absence;
use App\Entity\Employee;
use App\Entity\WorkHour;
use App\Enum\ContractNature;
use App\Enum\ContractType;
use App\Enum\TrackingMode;
use App\Repository\Contract\AbsenceReadRepositoryInterface;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\PublicHolidayServiceInterface;
use DateInterval;
use DateTimeImmutable;
use Throwable;

class YearlyHoursExportBuilder
{
    public function __construct(
        private WorkHourReadRepositoryInterface $workHourRepository,
        private AbsenceReadRepositoryInterface $absenceRepository,
        private EmployeeContractResolver $contractResolver,
        private AbsenceSegmentsResolver $absenceSegmentsResolver,
        private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
        private PublicHolidayServiceInterface $publicHolidayService,
        private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
        private NightHoursCalculator $nightHoursCalculator,
    ) {}

    /**
     * @return list<string>
     */
    public 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;
    }

    /**
     * @param list<Employee> $employees
     *
     * @return list<array{employeeName: string, contractLabel: ?string, segments: list<array>}>
     */
    public function buildForEmployees(array $employees, DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $days = $this->buildDays($from, $to);

        $workHours   = $this->workHourRepository->findByDateRangeAndEmployees($from, $to, $employees);
        $absences    = $this->absenceRepository->findForPrint($from, $to, $employees);
        $contractMap = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
        $driverMap   = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
        $workDaysMap = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
        $holidayMap  = $this->buildHolidayMap($from, $to);

        $workHourMap = $this->buildWorkHourMap($workHours);
        $absenceMap  = $this->buildAbsenceMap($absences, $days);

        $results = [];
        foreach ($employees as $employee) {
            $employeeId  = $employee->getId();
            $absenceData = $this->resolveAbsenceDataForEmployee($absenceMap[$employeeId] ?? [], $days, $employee);

            $segments = $this->buildSegments(
                $days,
                $contractMap[$employeeId] ?? [],
                $driverMap[$employeeId] ?? [],
                $workHourMap[$employeeId] ?? [],
                $absenceData,
                $workDaysMap[$employeeId] ?? [],
                $holidayMap,
            );

            if ([] === $segments) {
                continue;
            }

            $results[] = [
                'employeeName'  => trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? '')),
                'contractLabel' => $this->buildContractLabel($employee),
                'segments'      => $segments,
            ];
        }

        return $results;
    }

    /**
     * @return list<array{employeeName: string, contractLabel: ?string, segments: list<array>}>
     */
    public function buildForEmployee(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        return $this->buildForEmployees([$employee], $from, $to);
    }

    /**
     * Construit une ligne par employé pour une seule journée (vue Jour de l'écran Heures).
     * Réutilise les helpers de calcul de cellule pour rester l'unique source de vérité.
     * Les employés sans contrat ce jour sont exclus (comme l'écran).
     *
     * @param list<Employee> $employees
     *
     * @return list<array{employeeId:int, employeeName:string, statut:?string, statutLabel:?string, statutColor:?string,
     *   morningFrom:string, morningTo:string, afternoonFrom:string, afternoonTo:string,
     *   eveningFrom:string, eveningTo:string, dayHours:string, nightHours:string,
     *   total:string, isWeekend:bool, isHoliday:bool}>
     */
    public function buildDayRowsForEmployees(array $employees, DateTimeImmutable $date): array
    {
        $ymd  = $date->format('Y-m-d');
        $days = [$ymd];

        $workHours   = $this->workHourRepository->findByDateRangeAndEmployees($date, $date, $employees);
        $absences    = $this->absenceRepository->findForPrint($date, $date, $employees);
        $contractMap = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
        $driverMap   = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
        $workDaysMap = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
        $holidayMap  = $this->buildHolidayMap($date, $date);

        $workHourMap = $this->buildWorkHourMap($workHours);
        $absenceMap  = $this->buildAbsenceMap($absences, $days);

        $isoDay       = (int) $date->format('N');
        $isWeekend    = $isoDay >= 6;
        $holidayLabel = $holidayMap[$ymd] ?? null;

        $rows = [];
        foreach ($employees as $employee) {
            $employeeId = $employee->getId();
            $contract   = $contractMap[$employeeId][$ymd] ?? null;

            // Hors contrat ce jour → exclu (avant embauche / après départ / suspension).
            if (null === $contract) {
                continue;
            }

            $wh          = $workHourMap[$employeeId][$ymd] ?? null;
            $absenceData = $this->resolveAbsenceDataForEmployee($absenceMap[$employeeId] ?? [], $days, $employee);
            $hasAbsence  = $absenceData['hasDayAbsence'][$ymd] ?? false;

            $isDriver        = $driverMap[$employeeId][$ymd] ?? false;
            $mode            = $this->resolveSegmentMode($contract->getTrackingMode(), $isDriver);
            $creditedMinutes = $absenceData['credited'][$ymd] ?? 0;
            $virtualMinutes  = $this->holidayVirtualHoursResolver->resolveVirtualCredit(
                $contract,
                $date,
                $hasAbsence,
                $workDaysMap[$employeeId][$ymd] ?? null,
            );

            // Colonne Statut = code d'absence (ex. « AT »), pas le libellé.
            $statut      = ($absenceData['codes'][$ymd] ?? '') ?: null;
            $statutLabel = $absenceData['labels'][$ymd] ?? null;
            $statutColor = ($absenceData['colors'][$ymd] ?? '') ?: null;
            if (null === $statut && null !== $holidayLabel) {
                // Férié sans absence : badge bleu clair, comme la vue Jour.
                $statut      = $holidayLabel;
                $statutLabel = null;
                $statutColor = '#b3e5fc';
            }

            $row = [
                'employeeId'    => $employeeId,
                'employeeName'  => trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? '')),
                'statut'        => $statut,
                'statutLabel'   => $statutLabel,
                'statutColor'   => $statutColor,
                'morningFrom'   => '',
                'morningTo'     => '',
                'afternoonFrom' => '',
                'afternoonTo'   => '',
                'eveningFrom'   => '',
                'eveningTo'     => '',
                'dayHours'      => '',
                'nightHours'    => '',
                'total'         => '',
                'isWeekend'     => $isWeekend,
                'isHoliday'     => null !== $holidayLabel,
            ];

            if ('presence' === $mode) {
                $absentMorning   = $absenceData['absentMorning'][$ymd] ?? false;
                $absentAfternoon = $absenceData['absentAfternoon'][$ymd] ?? false;
                $morning         = (($wh?->getIsPresentMorning() ?? false) && !$absentMorning) ? 0.5 : 0.0;
                $afternoon       = (($wh?->getIsPresentAfternoon() ?? false) && !$absentAfternoon) ? 0.5 : 0.0;
                $total           = $morning + $afternoon;
                $row['total']    = $total > 0 ? (string) $total : '';
            } elseif ('driver' === $mode) {
                $dayMin   = $wh?->getDayHoursMinutes() ?? 0;
                $nightMin = $wh?->getNightHoursMinutes() ?? 0;
                $workshop = $wh?->getWorkshopHoursMinutes() ?? 0;
                $totalMin = $dayMin + $nightMin + $workshop + $creditedMinutes;
                if ($virtualMinutes > $totalMin) {
                    $totalMin = $virtualMinutes;
                }
                $row['dayHours']   = $dayMin > 0 ? $this->formatMinutes($dayMin) : '';
                $row['nightHours'] = $nightMin > 0 ? $this->formatMinutes($nightMin) : '';
                $row['total']      = $totalMin > 0 ? $this->formatMinutes($totalMin) : '';
            } else {
                $metrics = null !== $wh ? $this->computeMetrics($wh) : new WorkMetrics();
                $metrics->addCreditedMinutes($creditedMinutes);
                $dayMin   = $metrics->dayMinutes;
                $nightMin = $metrics->nightMinutes;
                $totalMin = $metrics->totalMinutes;
                if ($virtualMinutes > $totalMin) {
                    $dayMin += $virtualMinutes - $totalMin;
                    $totalMin = $virtualMinutes;
                }

                $row['morningFrom']   = $wh?->getMorningFrom() ?? '';
                $row['morningTo']     = $wh?->getMorningTo() ?? '';
                $row['afternoonFrom'] = $wh?->getAfternoonFrom() ?? '';
                $row['afternoonTo']   = $wh?->getAfternoonTo() ?? '';
                $row['eveningFrom']   = $wh?->getEveningFrom() ?? '';
                $row['eveningTo']     = $wh?->getEveningTo() ?? '';
                $row['dayHours']      = $dayMin > 0 ? $this->formatMinutes($dayMin) : '';
                $row['nightHours']    = $nightMin > 0 ? $this->formatMinutes($nightMin) : '';
                $row['total']         = $totalMin > 0 ? $this->formatMinutes($totalMin) : '';
            }

            $rows[] = $row;
        }

        return $rows;
    }

    public function buildContractLabel(Employee $employee): ?string
    {
        $contract = $employee->getContract();
        if (null === $contract) {
            return null;
        }

        $natureRaw   = $employee->getCurrentContractNature();
        $nature      = ContractNature::tryFrom($natureRaw) ?? ContractNature::CDI;
        $natureLabel = match ($nature) {
            ContractNature::CDI     => 'CDI',
            ContractNature::CDD     => 'CDD',
            ContractNature::INTERIM => 'Intérim',
        };

        $contractType = $contract->getType();
        if (ContractType::FORFAIT === $contractType) {
            return $natureLabel.' Forfait';
        }

        $weeklyHours = $contract->getWeeklyHours();
        if (null !== $weeklyHours && $weeklyHours > 0) {
            return sprintf('%s %d heures', $natureLabel, $weeklyHours);
        }

        $name = $contract->getName();

        return null !== $name && '' !== $name ? $natureLabel.' '.$name : $natureLabel;
    }

    /**
     * @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 $days): array
    {
        $map = [];
        foreach ($absences as $absence) {
            $employeeId = $absence->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }
            $map[$employeeId][] = $absence;
        }

        return $map;
    }

    /**
     * @return array{credited: array<string, int>, codes: array<string, string>, labels: array<string, string>, colors: array<string, string>, absentMorning: array<string, bool>, absentAfternoon: array<string, bool>, hasDayAbsence: array<string, bool>}
     */
    private function resolveAbsenceDataForEmployee(array $absences, array $days, Employee $employee): array
    {
        $credited        = [];
        $codes           = [];
        $labels          = [];
        $colors          = [];
        $absentMorning   = [];
        $absentAfternoon = [];
        $hasDayAbsence   = [];

        foreach ($absences as $absence) {
            $start = $absence->getStartDate()->format('Y-m-d');
            $end   = $absence->getEndDate()->format('Y-m-d');

            foreach ($days as $date) {
                if ($date < $start || $date > $end) {
                    continue;
                }

                [$isMorning, $isAfternoon] = $this->absenceSegmentsResolver->resolveForDate($absence, $date);
                if ($isMorning || $isAfternoon) {
                    $hasDayAbsence[$date]   = true;
                    $absentMorning[$date]   = ($absentMorning[$date] ?? false)   || $isMorning;
                    $absentAfternoon[$date] = ($absentAfternoon[$date] ?? false) || $isAfternoon;
                    if (!isset($labels[$date])) {
                        $codes[$date]  = $absence->getType()?->getCode() ?? '';
                        $labels[$date] = $absence->getType()?->getLabel() ?? '';
                        $colors[$date] = $absence->getType()?->getColor() ?? '';
                    }
                }

                $credited[$date] = ($credited[$date] ?? 0)
                    + $this->workedHoursCreditPolicy->computeCreditedMinutes($absence, $date, $isMorning, $isAfternoon);
            }
        }

        return [
            'credited'        => $credited,
            'codes'           => $codes,
            'labels'          => $labels,
            'colors'          => $colors,
            'absentMorning'   => $absentMorning,
            'absentAfternoon' => $absentAfternoon,
            'hasDayAbsence'   => $hasDayAbsence,
        ];
    }

    /**
     * @param array<string, ?array<int, int>> $workDaysMinutesByDate
     * @param array<string, string>           $holidayMap
     *
     * @return list<array{mode: string, contractName: ?string, rows: list<array>}>
     */
    private function buildSegments(
        array $days,
        array $contractsByDate,
        array $driverByDate,
        array $workHoursByDate,
        array $absenceData,
        array $workDaysMinutesByDate,
        array $holidayMap,
    ): array {
        $segments    = [];
        $currentMode = null;
        $currentRows = [];
        $currentName = null;

        $firstDataDate = null;
        foreach ($days as $date) {
            $hasRow = null !== ($workHoursByDate[$date] ?? null)
                || ($absenceData['hasDayAbsence'][$date] ?? false)
                || isset($holidayMap[$date]);
            if ($hasRow) {
                $firstDataDate = $date;

                break;
            }
        }

        if (null === $firstDataDate) {
            return [];
        }

        $todayYmd = new DateTimeImmutable('today')->format('Y-m-d');

        foreach ($days as $date) {
            if ($date < $firstDataDate || $date > $todayYmd) {
                continue;
            }

            $contract     = $contractsByDate[$date] ?? null;
            $isDriver     = $driverByDate[$date] ?? false;
            $wh           = $workHoursByDate[$date] ?? null;
            $hasData      = null !== $wh || ($absenceData['hasDayAbsence'][$date] ?? false);
            $holidayLabel = $holidayMap[$date] ?? null;
            $isHoliday    = null !== $holidayLabel;
            $isoDay       = (int) new DateTimeImmutable($date)->format('N');
            $isWeekend    = $isoDay >= 6;

            // Tous les jours contractés sont affichés, même vides ou non saisis (lignes
            // « manquantes » signalées par la RH). Seuls les jours hors contrat (avant
            // embauche, après départ, suspension) sont omis.
            if (!$hasData && null === $contract) {
                continue;
            }

            $trackingMode = $contract?->getTrackingMode() ?? TrackingMode::TIME->value;
            $mode         = $this->resolveSegmentMode($trackingMode, $isDriver);
            $contractName = $contract?->getName();

            if ($mode !== $currentMode) {
                if (null !== $currentMode && [] !== $currentRows) {
                    $segments[] = [
                        'mode'         => $currentMode,
                        'contractName' => $currentName,
                        'rows'         => $currentRows,
                    ];
                }
                $currentMode = $mode;
                $currentRows = [];
                $currentName = $contractName;
            }

            $creditedMinutes = $absenceData['credited'][$date] ?? 0;
            $absenceLabel    = $absenceData['labels'][$date] ?? null;
            $hasAbsence      = $absenceData['hasDayAbsence'][$date] ?? false;
            $virtualMinutes  = $this->holidayVirtualHoursResolver->resolveVirtualCredit(
                $contract,
                new DateTimeImmutable($date),
                $hasAbsence,
                $workDaysMinutesByDate[$date] ?? null,
            );

            $row = [
                'date'         => new DateTimeImmutable($date)->format('d/m/Y'),
                'absenceLabel' => $absenceLabel,
                'holidayLabel' => $holidayLabel,
                'isWeekend'    => $isWeekend,
            ];

            if ('presence' === $mode) {
                $absentMorning   = $absenceData['absentMorning'][$date] ?? false;
                $absentAfternoon = $absenceData['absentAfternoon'][$date] ?? false;
                $morning         = (($wh?->getIsPresentMorning() ?? false) && !$absentMorning) ? 0.5 : 0.0;
                $afternoon       = (($wh?->getIsPresentAfternoon() ?? false) && !$absentAfternoon) ? 0.5 : 0.0;
                $total           = $morning + $afternoon;

                $row['presentMorning']   = $morning > 0;
                $row['presentAfternoon'] = $afternoon > 0;
                $row['total']            = $total > 0 ? (string) $total : '';
            } elseif ('driver' === $mode) {
                $dayMin      = $wh?->getDayHoursMinutes() ?? 0;
                $nightMin    = $wh?->getNightHoursMinutes() ?? 0;
                $workshopMin = $wh?->getWorkshopHoursMinutes() ?? 0;
                $totalMin    = $dayMin + $nightMin + $workshopMin + $creditedMinutes;
                if ($virtualMinutes > $totalMin) {
                    $totalMin = $virtualMinutes;
                }

                $row['dayHours']      = $this->formatMinutes($dayMin);
                $row['nightHours']    = $this->formatMinutes($nightMin);
                $row['workshopHours'] = $this->formatMinutes($workshopMin);
                $row['total']         = $this->formatMinutes($totalMin);
            } else {
                $metrics = null !== $wh ? $this->computeMetrics($wh) : new WorkMetrics();
                $metrics->addCreditedMinutes($creditedMinutes);
                $totalMin = $metrics->totalMinutes;
                if ($virtualMinutes > $totalMin) {
                    $totalMin = $virtualMinutes;
                }

                $row['morningFrom']   = $wh?->getMorningFrom() ?? '';
                $row['morningTo']     = $wh?->getMorningTo() ?? '';
                $row['afternoonFrom'] = $wh?->getAfternoonFrom() ?? '';
                $row['afternoonTo']   = $wh?->getAfternoonTo() ?? '';
                $row['eveningFrom']   = $wh?->getEveningFrom() ?? '';
                $row['eveningTo']     = $wh?->getEveningTo() ?? '';
                $row['total']         = $this->formatMinutes($totalMin);
            }

            $currentRows[] = $row;
        }

        if (null !== $currentMode && [] !== $currentRows) {
            $segments[] = [
                'mode'         => $currentMode,
                'contractName' => $currentName,
                'rows'         => $currentRows,
            ];
        }

        return $segments;
    }

    /**
     * @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;
    }

    private function resolveSegmentMode(string $trackingMode, bool $isDriver): string
    {
        if ($isDriver) {
            return 'driver';
        }

        if (TrackingMode::PRESENCE->value === $trackingMode) {
            return 'presence';
        }

        return 'time';
    }

    private function computeMetrics(WorkHour $workHour): WorkMetrics
    {
        $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 new WorkMetrics(
            dayMinutes: $dayMinutes,
            nightMinutes: $nightMinutes,
            totalMinutes: $totalMinutes,
        );
    }

    /**
     * @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);
    }

    private function formatMinutes(int $minutes): string
    {
        if (0 === $minutes) {
            return '';
        }

        $h = intdiv($minutes, 60);
        $m = $minutes % 60;

        return 0 === $m ? "{$h}h" : "{$h}h{$m}m";
    }
}
