<?php

declare(strict_types=1);

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\EmployeeRttSummary;
use App\Dto\Contracts\ContractPhase;
use App\Dto\Rtt\EmployeeRttWeekSummary;
use App\Dto\Rtt\RttMonthPayment;
use App\Dto\Rtt\WeekRecoveryDetail;
use App\Entity\Employee;
use App\Entity\User;
use App\Repository\EmployeeRepository;
use App\Repository\EmployeeRttBalanceRepository;
use App\Repository\EmployeeRttPaymentRepository;
use App\Repository\WorkHourRepository;
use App\Security\EmployeeScopeService;
use App\Service\Contracts\EmployeeContractPhaseResolver;
use App\Service\Exercise\ExerciseYearResolver;
use App\Service\Rtt\RttClosingBalanceService;
use App\Service\Rtt\RttRecoveryComputationService;
use DateTimeImmutable;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

final readonly class EmployeeRttSummaryProvider implements ProviderInterface
{
    private ?string $rttStartDate;

    public function __construct(
        private Security $security,
        private RequestStack $requestStack,
        private EmployeeRepository $employeeRepository,
        private EmployeeScopeService $employeeScopeService,
        private EmployeeRttBalanceRepository $rttBalanceRepository,
        private EmployeeRttPaymentRepository $rttPaymentRepository,
        private RttRecoveryComputationService $rttRecoveryService,
        private WorkHourRepository $workHourRepository,
        private EmployeeContractPhaseResolver $phaseResolver,
        private ExerciseYearResolver $exerciseYearResolver,
        private RttClosingBalanceService $rttClosingService,
        string $rttStartDate = '',
    ) {
        $this->rttStartDate = '' !== $rttStartDate ? $rttStartDate : null;
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): EmployeeRttSummary
    {
        $user = $this->security->getUser();
        if (!$user instanceof User) {
            throw new AccessDeniedHttpException('Authentication required.');
        }

        $employeeId = (int) ($uriVariables['id'] ?? 0);
        if ($employeeId <= 0) {
            throw new UnprocessableEntityHttpException('id must be a positive integer.');
        }

        $employee = $this->employeeRepository->find($employeeId);
        if (!$employee instanceof Employee) {
            throw new NotFoundHttpException('Employee not found.');
        }

        if (!$this->employeeScopeService->canAccessEmployee($user, $employee)) {
            throw new AccessDeniedHttpException('Employee outside your scope.');
        }

        $phase                   = $this->resolveTargetPhase($employee);
        $year                    = $this->resolveYear($phase);
        $today                   = new DateTimeImmutable('today');
        $currentExerciseYear     = $this->resolveCurrentExerciseYear($today);
        [$periodFrom, $periodTo] = $this->rttRecoveryService->resolveExerciseBounds($year);

        // Cap periodTo at the phase endDate for closed phases so the RTT table does
        // not extend past the date the phase ended.
        // Do NOT cap periodFrom at phase.startDate: keep the full exercise
        // displayed so weeks before the employee's hire (or before a past phase
        // started) appear at 0, matching the previous behavior. Weeks outside the
        // contract range contribute 0 minutes to the cumul naturally (no contract
        // ⇒ no reference, no worked hours).
        if (!$phase->isCurrent && null !== $phase->endDate && $phase->endDate < $periodTo) {
            $periodTo = $phase->endDate;
        }

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

        if ($year > $currentExerciseYear) {
            $limitDate = $periodFrom->modify('-1 day');
        } else {
            // Exclude the current (incomplete) week: limit to last Sunday
            $isoDay    = (int) $today->format('N'); // 1=Monday .. 7=Sunday
            $limitDate = 7 === $isoDay ? $today : $today->modify('last sunday');

            // Include the current week if all existing days are admin-validated
            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;
                }
            }
        }

        // For a closed phase: cap the week-computation limit at the phase end date,
        // so weeks beyond the phase are not counted.
        if (!$phase->isCurrent && null !== $phase->endDate && $phase->endDate < $limitDate) {
            $limitDate = $phase->endDate;
        }

        $currentByWeekStart   = $this->rttRecoveryService->computeRecoveryByWeek($employee, $weekRanges, $periodFrom, $periodTo, $limitDate);
        [$carry, $carryMonth] = $this->resolveCarry($employee, $year);

        $summary                               = new EmployeeRttSummary();
        $summary->year                         = $year;
        $summary->carryMonth                   = $carryMonth;
        $summary->carryFromPreviousYearMinutes = $carry->totalMinutes;
        $summary->carryBase25Minutes           = $carry->base25Minutes;
        $summary->carryBonus25Minutes          = $carry->bonus25Minutes;
        $summary->carryBase50Minutes           = $carry->base50Minutes;
        $summary->carryBonus50Minutes          = $carry->bonus50Minutes;
        $summary->currentYearRecoveryMinutes   = array_sum(array_map(static fn ($d) => $d->totalMinutes, $currentByWeekStart));
        $summary->availableMinutes             = $summary->carryFromPreviousYearMinutes + $summary->currentYearRecoveryMinutes;

        // Always expose rttStartDate so the frontend can use it as a hard floor
        // for the year selector. Frontend already uses month-level comparison
        // to hide carry/report rows when the date is outside the exercise.
        $summary->rttStartDate = $this->rttStartDate;
        $summary->weeks        = $this->buildWeekSummaries($weekRanges, $currentByWeekStart, $periodFrom, $periodTo);

        // Post-process: distribute deficit weeks across cumulative balance (50% first, then 25%).
        // Flat-recovery (CUSTOM) weeks are skipped — their deficit only reduces the running cumul.
        $summary->weeks = $this->applyDeficitCascade(
            $summary->weeks,
            $carry->base25Minutes + $carry->bonus25Minutes,
            $carry->base50Minutes + $carry->bonus50Minutes,
        );

        $payments     = $this->rttPaymentRepository->findByEmployeeAndYear($employee, $year);
        $monthBuckets = [];

        foreach ($payments as $payment) {
            $m = $payment->getMonth();
            if (!isset($monthBuckets[$m])) {
                $monthBuckets[$m] = ['base25' => 0, 'bonus25' => 0, 'base50' => 0, 'bonus50' => 0];
            }
            $monthBuckets[$m]['base25'] += $payment->getBase25Minutes();
            $monthBuckets[$m]['bonus25'] += $payment->getBonus25Minutes();
            $monthBuckets[$m]['base50'] += $payment->getBase50Minutes();
            $monthBuckets[$m]['bonus50'] += $payment->getBonus50Minutes();
        }

        $runningCumul = $summary->carryFromPreviousYearMinutes;
        $prevMonth    = null;
        foreach ($summary->weeks as $week) {
            if (null !== $prevMonth && $week->month !== $prevMonth && isset($monthBuckets[$prevMonth])) {
                $b = $monthBuckets[$prevMonth];
                $runningCumul -= $b['base25'] + $b['bonus25'] + $b['base50'] + $b['bonus50'];
            }
            $runningCumul += $week->totalMinutes;
            $week->cumulativeBalanceMinutes = $runningCumul;
            $prevMonth                      = $week->month;
        }

        $monthPayments    = [];
        $totalPaidMinutes = 0;

        foreach ($monthBuckets as $m => $bucket) {
            $monthPayments[] = new RttMonthPayment($m, $bucket['base25'], $bucket['bonus25'], $bucket['base50'], $bucket['bonus50']);
            $totalPaidMinutes += $bucket['base25'] + $bucket['bonus25'] + $bucket['base50'] + $bucket['bonus50'];
        }

        $summary->totalPaidMinutes = $totalPaidMinutes;
        $summary->monthPayments    = $monthPayments;
        $summary->availableMinutes -= $totalPaidMinutes;

        return $summary;
    }

    /**
     * @return array{WeekRecoveryDetail, int} [carry, month]
     */
    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(),
            ];
        }

        // No stored report row yet (before the 1st-June rollover materialises it):
        // compute the previous exercise's full closing (opening + earned − paid) so the
        // carry already reflects retroactive payments and the incoming report — matching
        // what the rollover would persist. Falling back to earned-only would drop both.
        return [
            $this->rttClosingService->computeClosingBalance($employee, $year - 1),
            5,
        ];
    }

    private function resolveYear(ContractPhase $phase): int
    {
        $raw             = (string) ($this->requestStack->getCurrentRequest()?->query->get('year') ?? '');
        $phaseIdRaw      = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
        $phaseIdProvided = null !== $phaseIdRaw && '' !== (string) $phaseIdRaw;

        if ('' === $raw) {
            // When a phaseId is explicitly provided, default to the exercise year derived from
            // the phase's end date (or today if the phase is still current).
            if ($phaseIdProvided) {
                $reference = $phase->endDate ?? new DateTimeImmutable('today');

                return $this->resolveCurrentExerciseYear($reference);
            }

            return $this->resolveCurrentExerciseYear(new DateTimeImmutable('today'));
        }
        if (!preg_match('/^\d{4}$/', $raw)) {
            throw new UnprocessableEntityHttpException('year must use YYYY format.');
        }

        $year = (int) $raw;
        if ($year < 2000 || $year > 2100) {
            throw new UnprocessableEntityHttpException('year must be between 2000 and 2100.');
        }

        // When a phaseId is explicit, silently clamp the requested year to the
        // first/last exercise covered by the phase.
        if ($phaseIdProvided) {
            $year = $this->clampYearToPhase($year, $phase);
        }

        return $year;
    }

    private function clampYearToPhase(int $year, ContractPhase $phase): int
    {
        $firstYear = $this->exerciseYearResolver->forDate($phase->startDate);
        $lastYear  = $phase->endDate instanceof DateTimeImmutable
            ? $this->exerciseYearResolver->forDate($phase->endDate)
            : null;

        if ($year < $firstYear) {
            return $firstYear;
        }
        if (null !== $lastYear && $year > $lastYear) {
            return $lastYear;
        }

        return $year;
    }

    private function resolveTargetPhase(Employee $employee): ContractPhase
    {
        $raw    = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
        $phases = $this->phaseResolver->resolvePhases($employee);
        if ([] === $phases) {
            throw new UnprocessableEntityHttpException('Employee has no contract phase.');
        }

        if (null === $raw || '' === (string) $raw) {
            // Phase courante par défaut = celle marquée isCurrent ou, à défaut, la plus récente.
            foreach ($phases as $phase) {
                if ($phase->isCurrent) {
                    return $phase;
                }
            }

            return $phases[0];
        }

        if (!preg_match('/^\d+$/', (string) $raw)) {
            throw new UnprocessableEntityHttpException('phaseId must be a positive integer.');
        }
        $phaseId = (int) $raw;
        foreach ($phases as $phase) {
            if ($phase->id === $phaseId) {
                return $phase;
            }
        }

        throw new UnprocessableEntityHttpException('phaseId does not match any phase of this employee.');
    }

    private function resolveCurrentExerciseYear(DateTimeImmutable $today): int
    {
        $year  = (int) $today->format('Y');
        $month = (int) $today->format('n');

        return $month >= 6 ? $year + 1 : $year;
    }

    /**
     * If the employee's contract ends within the current week, cap the check range to that end date.
     */
    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;
    }

    /**
     * Distribue les semaines déficitaires sur les tranches 25/50 accumulées (50 % d'abord,
     * puis 25 %), en réécrivant les buckets affichés de chaque semaine déficitaire avec les
     * montants négatifs drainés.
     *
     * Les semaines à récupération plate (CUSTOM 1h = 1h) sont ignorées : elles n'ont pas de
     * tranches 25/50, donc leur déficit ne réduit que le cumul courant (calculé ensuite à
     * partir de totalMinutes) et les colonnes 25/50 restent à 0.
     *
     * @param list<EmployeeRttWeekSummary> $weeks
     *
     * @return list<EmployeeRttWeekSummary>
     */
    private function applyDeficitCascade(array $weeks, int $cumulative25, int $cumulative50): array
    {
        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;
    }

    /**
     * Build week summaries, splitting weeks that span two months into two entries
     * with values distributed proportionally based on daily worked minutes.
     *
     * @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;
            }

            // Week spans two months — split proportionally by daily worked minutes
            $monthMinutes  = [];
            $monthWeekdays = [];
            foreach ($detail->dailyMinutes as $date => $mins) {
                $m                = (int) new DateTimeImmutable($date)->format('n');
                $monthMinutes[$m] = ($monthMinutes[$m] ?? 0) + $mins;
                $isoDay           = (int) new DateTimeImmutable($date)->format('N');
                if ($isoDay < 6) {
                    $monthWeekdays[$m] = ($monthWeekdays[$m] ?? 0) + 1;
                }
            }

            $totalWorked   = array_sum($monthMinutes);
            $totalWeekdays = array_sum($monthWeekdays);

            foreach ([$startMonth, $endMonth] as $month) {
                if ($totalWorked > 0) {
                    $ratio = ($monthMinutes[$month] ?? 0) / $totalWorked;
                } elseif ($totalWeekdays > 0) {
                    $ratio = ($monthWeekdays[$month] ?? 0) / $totalWeekdays;
                } else {
                    $ratio = 0.0;
                }

                $result[] = new EmployeeRttWeekSummary(
                    month: $month,
                    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;
    }
}
