<?php

declare(strict_types=1);

namespace App\Service\Leave;

use App\Entity\Employee;
use App\Enum\ContractNature;
use App\Enum\ContractType;
use App\Enum\TrackingMode;
use App\Repository\EmployeeRttBalanceRepository;
use App\Repository\EmployeeRttPaymentRepository;
use App\Repository\WorkHourRepository;
use App\Service\Rtt\RttRecoveryComputationService;
use App\State\EmployeeLeaveSummaryProvider;
use DateTimeImmutable;
use Throwable;

final readonly class LeaveRecapRowBuilder
{
    public function __construct(
        private EmployeeLeaveSummaryProvider $leaveSummaryProvider,
        private RttRecoveryComputationService $rttRecoveryService,
        private EmployeeRttBalanceRepository $rttBalanceRepository,
        private EmployeeRttPaymentRepository $rttPaymentRepository,
        private WorkHourRepository $workHourRepository,
    ) {}

    /**
     * Builds a leave recap row for one employee.
     *
     * - $asOfDate = null → live behavior (identical to legacy PDF export): accrual capped at
     *   previous month end, ALL booked absences counted (incl. future ones), RTT uses today
     * - $asOfDate = non-null → frozen snapshot at that date: accrual capped at the previous
     *   month end before asOfDate, absences after asOfDate excluded, RTT uses asOfDate
     *
     * @return array{
     *     lastName: string,
     *     firstName: string,
     *     contractName: ?string,
     *     cpN1Remaining: float|string,
     *     cpN: string,
     *     acquiredSaturdays: string,
     *     rtt: string
     * }
     */
    public function build(Employee $employee, ?DateTimeImmutable $asOfDate = null): array
    {
        $contract     = $employee->getContract();
        $contractName = $contract?->getName();
        $isForfait    = ContractType::FORFAIT === $contract?->getType();
        $nature       = ContractNature::tryFrom($employee->getCurrentContractNature());
        $isInterim    = ContractNature::INTERIM === $nature;

        $rttReference = $asOfDate ?? new DateTimeImmutable('today');

        $cpN1Remaining     = 0.0;
        $cpN               = '-';
        $acquiredSaturdays = '-';
        $rtt               = '-';

        if (!$isInterim) {
            $leaveYear   = $this->leaveSummaryProvider->resolveLeaveYearForToday($employee);
            $yearSummary = $this->leaveSummaryProvider->computeYearSummary($employee, $leaveYear, 0.0, $asOfDate);

            if (null !== $yearSummary) {
                if ($isForfait) {
                    $paidLeaveDays = $this->leaveSummaryProvider->resolvePaidLeaveDays($employee, $yearSummary['ruleCode'], $leaveYear);
                    if ($paidLeaveDays > 0.0) {
                        $recomputed = $this->leaveSummaryProvider->computeYearSummary($employee, $leaveYear, $paidLeaveDays, $asOfDate);
                        if (null !== $recomputed) {
                            $yearSummary = $recomputed;
                        }
                    }
                    $cpN1Remaining     = round($yearSummary['previousYearRemainingDays'], 2);
                    $cpN               = (string) round($yearSummary['remainingDays'], 2);
                    $acquiredSaturdays = '-';
                } else {
                    $cpN1Remaining     = round($yearSummary['remainingDays'], 2);
                    $cpN               = (string) round($yearSummary['accruingDays'], 2);
                    $acquiredSaturdays = (string) round($yearSummary['remainingSaturdays'], 2);
                }
            }

            if (!$isForfait && TrackingMode::PRESENCE->value !== $contract?->getTrackingMode()) {
                try {
                    $rtt = $this->formatMinutes($this->computeAvailableRttMinutes($employee, $rttReference));
                } catch (Throwable) {
                    $rtt = '-';
                }
            }
        }

        return [
            'lastName'          => $employee->getLastName(),
            'firstName'         => $employee->getFirstName(),
            'contractName'      => $contractName,
            'cpN1Remaining'     => $cpN1Remaining,
            'cpN'               => $cpN,
            'acquiredSaturdays' => $acquiredSaturdays,
            'rtt'               => $rtt,
        ];
    }

    private function computeAvailableRttMinutes(Employee $employee, DateTimeImmutable $reference): int
    {
        $month        = (int) $reference->format('n');
        $year         = (int) $reference->format('Y');
        $exerciseYear = $month >= 6 ? $year + 1 : $year;

        // Exclude incomplete current week: limit to last Sunday
        $isoDay    = (int) $reference->format('N');
        $limitDate = 7 === $isoDay ? $reference : $reference->modify('last sunday');

        // Include the current week if all existing days are admin-validated
        if (7 !== $isoDay) {
            $currentWeekStart = $reference->modify('monday this week');
            $currentWeekEnd   = $currentWeekStart->modify('+6 days');
            $checkEnd         = $this->resolveWeekEndForEmployee($employee, $currentWeekStart, $currentWeekEnd, $reference);
            if ($this->workHourRepository->isWeekFullyValidated($employee, $currentWeekStart, $checkEnd)) {
                $limitDate = $currentWeekEnd;
            }
        }

        // Carry from previous exercise
        $carry   = 0;
        $balance = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $exerciseYear);
        if (null !== $balance) {
            $carry = $balance->getTotalOpeningMinutes();
        } else {
            $previousTotal = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $exerciseYear - 1);
            $carry         = $previousTotal->totalMinutes;
        }

        // Current exercise (limited to completed weeks)
        $current = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $exerciseYear, $limitDate);

        // Paid RTT
        $paid     = 0;
        $payments = $this->rttPaymentRepository->findByEmployeeAndYear($employee, $exerciseYear);
        foreach ($payments as $payment) {
            $paid += $payment->getBase25Minutes() + $payment->getBonus25Minutes()
                   + $payment->getBase50Minutes() + $payment->getBonus50Minutes();
        }

        return $carry + $current->totalMinutes - $paid;
    }

    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 formatMinutes(int $minutes): string
    {
        if (0 === $minutes) {
            return '0 h';
        }

        $sign = $minutes < 0 ? '- ' : '';
        $abs  = abs($minutes);
        $h    = intdiv($abs, 60);
        $m    = $abs % 60;

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