<?php

declare(strict_types=1);

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\WorkHourWeeklySummary;
use App\Dto\WorkHours\WeeklyDaySummary;
use App\Dto\WorkHours\WeeklySummaryRow;
use App\Dto\WorkHours\WorkMetrics;
use App\Entity\Absence;
use App\Entity\Contract;
use App\Entity\Employee;
use App\Entity\EmployeeWeekComment;
use App\Entity\User;
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\EmployeeScopedRepositoryInterface;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Repository\EmployeeWeekCommentRepository;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\PublicHolidayServiceInterface;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\DailyReferenceMinutesResolver;
use App\Service\WorkHours\HolidayVirtualHoursResolver;
use App\Service\WorkHours\NightHoursCalculator;
use App\Service\WorkHours\WorkedHoursCreditPolicy;
use DateTimeImmutable;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Throwable;

final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
{
    public function __construct(
        private Security $security,
        private RequestStack $requestStack,
        private EmployeeScopedRepositoryInterface $employeeRepository,
        private WorkHourReadRepositoryInterface $workHourRepository,
        private AbsenceReadRepositoryInterface $absenceRepository,
        private AbsenceSegmentsResolver $absenceSegmentsResolver,
        private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
        private EmployeeContractResolver $contractResolver,
        private DailyReferenceMinutesResolver $dailyReferenceResolver,
        private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
        private PublicHolidayServiceInterface $publicHolidayService,
        private EmployeeWeekCommentRepository $weekCommentRepository,
        private NightHoursCalculator $nightHoursCalculator,
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
    {
        $user = $this->security->getUser();
        // Endpoint protégé: résumé hebdo réservé aux utilisateurs authentifiés.
        if (!$user instanceof User) {
            throw new AccessDeniedHttpException('Authentication required.');
        }

        $anchorDate                   = $this->resolveAnchorDate();
        [$weekStart, $weekEnd, $days] = $this->resolveWeek($anchorDate);

        $employees = $this->employeeRepository->findScoped($user);
        $workHours = $this->workHourRepository->findByDateRangeAndEmployees($weekStart, $weekEnd, $employees);
        $absences  = $this->absenceRepository->findForPrint($weekStart, $weekEnd, $employees);

        $weekComments = $this->weekCommentRepository->findByWeekAndEmployees($weekStart, $employees);

        $summary            = new WorkHourWeeklySummary();
        $summary->weekStart = $weekStart->format('Y-m-d');
        $summary->weekEnd   = $weekEnd->format('Y-m-d');
        $summary->days      = $days;
        $summary->rows      = $this->buildRows($employees, $workHours, $absences, $days, $anchorDate->format('Y-m-d'), $weekComments);

        return $summary;
    }

    private function resolveAnchorDate(): DateTimeImmutable
    {
        $query = $this->requestStack->getCurrentRequest()?->query;
        $raw   = (string) ($query?->get('weekStart') ?? '');

        // Sans paramètre, on ancre la semaine sur aujourd'hui.
        if ('' === $raw) {
            return new DateTimeImmutable('today');
        }

        $date = DateTimeImmutable::createFromFormat('Y-m-d', $raw);
        // Validation stricte du format attendu.
        if (!$date || $date->format('Y-m-d') !== $raw) {
            throw new UnprocessableEntityHttpException('weekStart must use Y-m-d format.');
        }

        return $date;
    }

    /**
     * @return array{DateTimeImmutable, DateTimeImmutable, list<string>}
     */
    private function resolveWeek(DateTimeImmutable $anchorDate): array
    {
        // Convention ISO: semaine de lundi (1) à dimanche (7).
        $dayOfWeek = (int) $anchorDate->format('N');
        $weekStart = $anchorDate->modify(sprintf('-%d days', $dayOfWeek - 1));
        $weekEnd   = $weekStart->modify('+6 days');

        $days = [];
        for ($i = 0; $i < 7; ++$i) {
            $days[] = $weekStart->modify(sprintf('+%d days', $i))->format('Y-m-d');
        }

        return [$weekStart, $weekEnd, $days];
    }

    /**
     * @param list<Employee>                  $employees
     * @param list<WorkHour>                  $workHours
     * @param list<Absence>                   $absences
     * @param list<string>                    $days
     * @param array<int, EmployeeWeekComment> $weekComments
     *
     * @return list<WeeklySummaryRow>
     */
    private function buildRows(array $employees, array $workHours, array $absences, array $days, string $anchorDateYmd, array $weekComments = []): array
    {
        $contractsByEmployeeDate       = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
        $contractNaturesByEmployeeDate = $this->contractResolver->resolveNaturesForEmployeesAndDays($employees, $days);
        $isDriverByEmployeeDate        = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
        $workDaysByEmployeeDate        = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
        $holidayLabelsByDate           = $this->buildHolidayLabelsForDays($days);
        $metricsByEmployeeDate         = [];
        foreach ($workHours as $workHour) {
            $employeeId = $workHour->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }

            // Pré-calcul des métriques par salarié/date pour simplifier l'agrégation finale.
            $dateKey                                      = $workHour->getWorkDate()->format('Y-m-d');
            $metricsByEmployeeDate[$employeeId][$dateKey] = [
                'metrics'              => $this->computeMetrics($workHour),
                'isPresentMorning'     => $workHour->getIsPresentMorning(),
                'isPresentAfternoon'   => $workHour->getIsPresentAfternoon(),
                'dayHoursMinutes'      => $workHour->getDayHoursMinutes(),
                'nightHoursMinutes'    => $workHour->getNightHoursMinutes(),
                'workshopHoursMinutes' => $workHour->getWorkshopHoursMinutes(),
                'hasBreakfast'         => $workHour->getHasBreakfast(),
                'hasLunch'             => $workHour->getHasLunch(),
                'hasDinner'            => $workHour->getHasDinner(),
                'hasOvernight'         => $workHour->getHasOvernight(),
            ];
        }

        $creditedByEmployeeDate         = [];
        $creditedPresenceByEmployeeDate = [];
        $absenceByEmployeeDate          = [];
        $absentMorningByEmployeeDate    = [];
        $absentAfternoonByEmployeeDate  = [];
        $absenceLabelByEmployeeDate     = [];
        $absenceColorByEmployeeDate     = [];
        foreach ($absences as $absence) {
            $employeeId = $absence->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }

            $start = $absence->getStartDate()->format('Y-m-d');
            $end   = $absence->getEndDate()->format('Y-m-d');
            foreach ($days as $date) {
                // On ne crédite que les dates couvertes par l'intervalle d'absence.
                if ($date < $start || $date > $end) {
                    continue;
                }

                [$absentMorning, $absentAfternoon] = $this->absenceSegmentsResolver->resolveForDate($absence, $date);
                if ($absentMorning || $absentAfternoon) {
                    $absenceByEmployeeDate[$employeeId][$date]         = true;
                    $absentMorningByEmployeeDate[$employeeId][$date]   = ($absentMorningByEmployeeDate[$employeeId][$date] ?? false)   || $absentMorning;
                    $absentAfternoonByEmployeeDate[$employeeId][$date] = ($absentAfternoonByEmployeeDate[$employeeId][$date] ?? false) || $absentAfternoon;
                    if (!isset($absenceLabelByEmployeeDate[$employeeId][$date])) {
                        $absenceLabelByEmployeeDate[$employeeId][$date] = $absence->getType()?->getLabel();
                    }
                    if (!isset($absenceColorByEmployeeDate[$employeeId][$date])) {
                        $absenceColorByEmployeeDate[$employeeId][$date] = $absence->getType()?->getColor();
                    }
                }
                $creditedByEmployeeDate[$employeeId][$date] = ($creditedByEmployeeDate[$employeeId][$date] ?? 0)
                    + $this->workedHoursCreditPolicy->computeCreditedMinutes($absence, $date, $absentMorning, $absentAfternoon);
                $creditedPresenceByEmployeeDate[$employeeId][$date] = ($creditedPresenceByEmployeeDate[$employeeId][$date] ?? 0.0)
                    + $this->workedHoursCreditPolicy->computeCreditedPresenceUnits($absence, $date, $absentMorning, $absentAfternoon);
            }
        }

        $rows = [];
        foreach ($employees as $employee) {
            $employeeId = $employee->getId();
            if (!$employeeId) {
                continue;
            }

            $weeklyDayMinutes       = 0;
            $weeklyNightMinutes     = 0;
            $weeklyWorkshopMinutes  = 0;
            $weeklyTotalMinutes     = 0;
            $weeklyPresenceCount    = 0.0;
            $weeklyNightBasketCount = 0;
            $weeklyBreakfastCount   = 0;
            $weeklyLunchCount       = 0;
            $weeklyDinnerCount      = 0;
            $weeklyOvernightCount   = 0;
            $daily                  = [];
            // Les contrats au suivi "présence" ne manipulent pas les heures, mais des demi-journées.
            $weekAnchorContract = $contractsByEmployeeDate[$employeeId][$anchorDateYmd]
                ?? $contractsByEmployeeDate[$employeeId][$days[0]]
                ?? null;
            $isDriver = $isDriverByEmployeeDate[$employeeId][$anchorDateYmd]
                ?? $isDriverByEmployeeDate[$employeeId][$days[0]]
                ?? false;
            $weekAnchorContractNature = $contractNaturesByEmployeeDate[$employeeId][$anchorDateYmd]
                ?? $contractNaturesByEmployeeDate[$employeeId][$days[0]]
                ?? ContractNature::CDI;
            $employeeContractsByDate = [];
            $hasContractForWeek      = false;
            foreach ($days as $date) {
                $employeeContractsByDate[$date] = $contractsByEmployeeDate[$employeeId][$date] ?? null;
                if (null !== $employeeContractsByDate[$date]) {
                    $hasContractForWeek = true;
                }
            }

            foreach ($days as $date) {
                $entry              = $metricsByEmployeeDate[$employeeId][$date] ?? null;
                $creditedMinutes    = $creditedByEmployeeDate[$employeeId][$date] ?? 0;
                $contractAtDate     = $employeeContractsByDate[$date] ?? null;
                $isPresenceTracking = TrackingMode::PRESENCE->value === $contractAtDate?->getTrackingMode();
                $isDateDriver       = $isDriverByEmployeeDate[$employeeId][$date] ?? false;

                $hasBreakfast = false;
                $hasLunch     = false;
                $hasDinner    = false;
                $hasOvernight = false;

                if ($isDateDriver) {
                    $dayMinutes      = ($entry['dayHoursMinutes'] ?? 0);
                    $nightMinutes    = ($entry['nightHoursMinutes'] ?? 0);
                    $workshopMinutes = ($entry['workshopHoursMinutes'] ?? 0);
                    $totalMinutes    = $dayMinutes + $nightMinutes + $workshopMinutes + $creditedMinutes;
                    $dayMinutes += $creditedMinutes;
                    $hasBreakfast = $entry['hasBreakfast'] ?? false;
                    $hasLunch     = $entry['hasLunch'] ?? false;
                    $hasDinner    = $entry['hasDinner'] ?? false;
                    $hasOvernight = $entry['hasOvernight'] ?? false;
                    if ($hasBreakfast) {
                        ++$weeklyBreakfastCount;
                    }
                    if ($hasLunch) {
                        ++$weeklyLunchCount;
                    }
                    if ($hasDinner) {
                        ++$weeklyDinnerCount;
                    }
                    if ($hasOvernight) {
                        ++$weeklyOvernightCount;
                    }
                } else {
                    $metrics = $entry['metrics'] ?? new WorkMetrics();
                    // Les absences "comptées comme travaillées" alimentent le total du jour.
                    $metrics->addCreditedMinutes($creditedMinutes);
                    $dayMinutes      = $metrics->dayMinutes;
                    $nightMinutes    = $metrics->nightMinutes;
                    $workshopMinutes = 0;
                    $totalMinutes    = $metrics->totalMinutes;
                }

                $present = null;
                if ($isPresenceTracking && !$isDateDriver) {
                    $absentMorning    = $absentMorningByEmployeeDate[$employeeId][$date] ?? false;
                    $absentAfternoon  = $absentAfternoonByEmployeeDate[$employeeId][$date] ?? false;
                    $morning          = (($entry['isPresentMorning'] ?? false) && !$absentMorning) ? 0.5 : 0.0;
                    $afternoon        = (($entry['isPresentAfternoon'] ?? false) && !$absentAfternoon) ? 0.5 : 0.0;
                    $creditedPresence = $creditedPresenceByEmployeeDate[$employeeId][$date] ?? 0.0;
                    $present          = min(1.0, $morning + $afternoon + $creditedPresence);
                }

                // Le panier de nuit ne s'applique pas aux conducteurs (ils ont leurs propres
                // primes repas/nuitée). Réservé aux non-conducteurs.
                $hasNightBasket = !$isDateDriver
                    && (($nightMinutes > $dayMinutes && $nightMinutes > 0) || $nightMinutes >= 240);
                if ($hasNightBasket) {
                    ++$weeklyNightBasketCount;
                }

                // Apply the Mon-Fri public holiday credit rule: for non-Forfait contracts,
                // if the total worked is below the contract-expected daily hours, top it up.
                // Virtual minutes are always accounted against the "day" bucket.
                // When an absence is declared on the day, the holiday credit is bypassed —
                // the absence (via WorkedHoursCreditPolicy) dictates the hours.
                $virtualHolidayMinutes = $this->holidayVirtualHoursResolver
                    ->resolveVirtualCredit(
                        $contractAtDate,
                        new DateTimeImmutable($date),
                        $absenceByEmployeeDate[$employeeId][$date] ?? false,
                        $workDaysByEmployeeDate[$employeeId][$date] ?? null,
                    )
                ;
                if ($virtualHolidayMinutes > $totalMinutes) {
                    $delta = $virtualHolidayMinutes - $totalMinutes;
                    $dayMinutes += $delta;
                    $totalMinutes = $virtualHolidayMinutes;
                }

                $weeklyDayMinutes += $dayMinutes;
                $weeklyNightMinutes += $nightMinutes;
                $weeklyWorkshopMinutes += $workshopMinutes;
                $weeklyTotalMinutes += $totalMinutes;
                if (null !== $present) {
                    $weeklyPresenceCount += $present;
                }

                $daily[] = new WeeklyDaySummary(
                    date: $date,
                    dayMinutes: $dayMinutes,
                    nightMinutes: $nightMinutes,
                    workshopMinutes: $workshopMinutes,
                    totalMinutes: $totalMinutes,
                    present: $present,
                    hasAbsence: $absenceByEmployeeDate[$employeeId][$date] ?? false,
                    absenceLabel: $absenceLabelByEmployeeDate[$employeeId][$date] ?? null,
                    absenceColor: $absenceColorByEmployeeDate[$employeeId][$date] ?? null,
                    hasNightBasket: $hasNightBasket,
                    hasBreakfast: $hasBreakfast,
                    hasLunch: $hasLunch,
                    hasDinner: $hasDinner,
                    hasOvernight: $hasOvernight,
                    virtualHolidayMinutes: $virtualHolidayMinutes,
                    holidayLabel: $holidayLabelsByDate[$date] ?? null,
                );
            }

            $isWeekPresenceTracking     = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode();
            $disableOvertimeBonuses     = $isDriver || $this->hasDisabledOvertimeBonuses($weekAnchorContract, $weekAnchorContractNature);
            $overtimeReferenceMinutes   = $this->computeWeeklyOvertimeReferenceMinutes($days, $employeeContractsByDate);
            $overtime25StartMinutes     = $this->computeWeeklyOvertime25StartMinutes($days, $employeeContractsByDate);
            $weeklyOvertimeTotalMinutes = ($isWeekPresenceTracking || $isDriver)
                ? 0
                : max(0, $weeklyTotalMinutes - $overtimeReferenceMinutes);
            $weeklyOvertime25Minutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
                ? 0
                : $this->computeOvertime25BonusMinutes($weeklyTotalMinutes, $overtime25StartMinutes);
            $weeklyOvertime50Minutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
                ? 0
                : $this->computeOvertime50BonusMinutes($weeklyTotalMinutes);
            $weeklyRecoveryMinutes = ($isWeekPresenceTracking || $disableOvertimeBonuses)
                ? 0
                : $weeklyOvertimeTotalMinutes + $weeklyOvertime25Minutes + $weeklyOvertime50Minutes;

            $rows[] = new WeeklySummaryRow(
                employeeId: $employeeId,
                firstName: $employee->getFirstName(),
                lastName: $employee->getLastName(),
                siteName: $employee->getSite()?->getName(),
                contractName: $weekAnchorContract?->getName(),
                contractType: $weekAnchorContract?->getType()->value,
                trackingMode: $weekAnchorContract?->getTrackingMode(),
                daily: $daily,
                weeklyDayMinutes: $weeklyDayMinutes,
                weeklyNightMinutes: $weeklyNightMinutes,
                weeklyWorkshopMinutes: $weeklyWorkshopMinutes,
                weeklyTotalMinutes: $weeklyTotalMinutes,
                weeklyPresenceCount: $weeklyPresenceCount,
                weeklyOvertimeTotalMinutes: $weeklyOvertimeTotalMinutes,
                weeklyOvertime25Minutes: $weeklyOvertime25Minutes,
                weeklyOvertime50Minutes: $weeklyOvertime50Minutes,
                weeklyRecoveryMinutes: $weeklyRecoveryMinutes,
                weeklyNightBasketCount: $weeklyNightBasketCount,
                isDriver: $isDriver,
                weeklyBreakfastCount: $weeklyBreakfastCount,
                weeklyLunchCount: $weeklyLunchCount,
                weeklyDinnerCount: $weeklyDinnerCount,
                weeklyOvernightCount: $weeklyOvernightCount,
                hasContractForWeek: $hasContractForWeek,
                contractNature: $weekAnchorContractNature->value,
                comment: ($weekComments[$employeeId] ?? null)?->getContent(),
                commentId: ($weekComments[$employeeId] ?? null)?->getId(),
            );
        }

        return $rows;
    }

    /**
     * @param list<string> $days
     *
     * @return array<string, string>
     */
    private function buildHolidayLabelsForDays(array $days): array
    {
        if ([] === $days) {
            return [];
        }

        $years = [];
        foreach ($days as $day) {
            $years[substr($day, 0, 4)] = true;
        }

        $map = [];

        try {
            foreach (array_keys($years) as $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 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;
        }

        // Si fin <= début, on considère un passage à minuit.
        $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);
    }

    /**
     * @param array<string, ?Contract> $contractsByDate
     */
    private function computeWeeklyOvertimeReferenceMinutes(array $days, array $contractsByDate): int
    {
        $total = 0;
        foreach ($days as $date) {
            $isoDay         = (int) new DateTimeImmutable($date)->format('N');
            $contract       = $contractsByDate[$date] ?? null;
            $hours          = $contract?->getWeeklyHours();
            $referenceHours = (null !== $hours && $hours > 0) ? max(35, $hours) : null;
            $total += $this->resolveDailyReferenceMinutes($referenceHours, $isoDay);
        }

        return $total;
    }

    /**
     * @param array<string, ?Contract> $contractsByDate
     */
    private function computeWeeklyOvertime25StartMinutes(array $days, array $contractsByDate): int
    {
        $total = 0;
        foreach ($days as $date) {
            $isoDay     = (int) new DateTimeImmutable($date)->format('N');
            $contract   = $contractsByDate[$date] ?? null;
            $hours      = $contract?->getWeeklyHours();
            $startHours = (null !== $hours && $hours >= 39) ? 39 : 35;
            $total += $this->resolveDailyReferenceMinutes($startHours, $isoDay);
        }

        return $total;
    }

    private function computeOvertime25BonusMinutes(int $weeklyTotalMinutes, int $startMinutes): int
    {
        $trancheMinutes = max(0, min($weeklyTotalMinutes, 43 * 60) - $startMinutes);

        return (int) round($trancheMinutes * 0.25);
    }

    private function computeOvertime50BonusMinutes(int $weeklyTotalMinutes): int
    {
        // Bonus 50% appliqué au-delà de 43h.
        $trancheMinutes = max(0, $weeklyTotalMinutes - (43 * 60));

        return (int) round($trancheMinutes * 0.5);
    }

    private function hasDisabledOvertimeBonuses(?Contract $contract, ContractNature $contractNature): bool
    {
        if (ContractNature::INTERIM === $contractNature) {
            return true;
        }

        $type = ContractType::resolve(
            $contract?->getName(),
            $contract?->getTrackingMode(),
            $contract?->getWeeklyHours()
        );

        return ContractType::INTERIM === $type;
    }

    private function resolveDailyReferenceMinutes(?int $weeklyHours, int $isoWeekDay): int
    {
        return $this->dailyReferenceResolver->resolve($weeklyHours, $isoWeekDay);
    }
}
