<?php

declare(strict_types=1);

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\Employee;
use App\Entity\Formation;
use App\Enum\ContractNature;
use App\Enum\HalfDay;
use App\Repository\AbsenceRepository;
use App\Repository\Contract\FormationReadRepositoryInterface;
use App\Repository\EmployeeRepository;
use App\Service\PublicHolidayServiceInterface;
use DateInterval;
use DateTimeImmutable;
use Dompdf\Dompdf;
use Dompdf\Options;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;

class AbsencePrintProvider implements ProviderInterface
{
    public function __construct(
        private Environment $twig,
        private readonly RequestStack $requestStack,
        private EmployeeRepository $employeeRepository,
        private AbsenceRepository $absenceRepository,
        private FormationReadRepositoryInterface $formationRepository,
        private PublicHolidayServiceInterface $publicHolidayService,
    ) {}

    /**
     * @throws SyntaxError
     * @throws RuntimeError
     * @throws LoaderError
     */
    public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
    {
        $request = $this->requestStack->getCurrentRequest();
        if (!$request) {
            return new Response('Missing request.', Response::HTTP_BAD_REQUEST);
        }

        $from = $request->query->get('from');
        $to   = $request->query->get('to');
        if (!$from || !$to) {
            return new Response('Missing from/to query params.', Response::HTTP_BAD_REQUEST);
        }

        $fromDate = DateTimeImmutable::createFromFormat('Y-m-d', $from);
        $toDate   = DateTimeImmutable::createFromFormat('Y-m-d', $to);
        if (!$fromDate instanceof DateTimeImmutable || !$toDate instanceof DateTimeImmutable) {
            return new Response('Invalid from/to date format.', Response::HTTP_BAD_REQUEST);
        }
        // createFromFormat('Y-m-d', ...) garde l'heure courante : on borne explicitement aux
        // extrémités de journée, sinon les comparaisons de dates (présence d'un contrat/absence
        // le jour de `from`) échouent contre les dates BDD à minuit.
        $fromDate = $fromDate->setTime(0, 0, 0);
        $toDate   = $toDate->setTime(23, 59, 59);

        $siteIds         = $this->parseIds($request->query->get('sites'));
        $workContractIds = $this->parseIds($request->query->get('workContracts'));
        $contractNatures = $this->parseContractNatures($request->query->get('contractNatures'));

        $employees  = $this->loadEmployees($siteIds, $contractNatures, $workContractIds, $fromDate, $toDate);
        $absences   = $this->loadAbsences($fromDate, $toDate, $employees);
        $formations = $this->formationRepository->findByDateRangeAndEmployees($fromDate, $toDate, $employees);

        $days         = $this->buildDays($fromDate, $toDate);
        $absenceMap   = $this->buildAbsenceMap($absences, $fromDate, $toDate);
        $formationMap = $this->buildFormationMap($formations, $fromDate, $toDate);
        $holidayMap   = $this->buildHolidayMap($fromDate, $toDate);

        $options = new Options();
        $options->set('isRemoteEnabled', true);

        $dompdf = new Dompdf($options);
        $html   = $this->twig->render('absence/print.html.twig', [
            'from'         => $fromDate,
            'to'           => $toDate,
            'days'         => $days,
            'employees'    => $employees,
            'absenceMap'   => $absenceMap,
            'formationMap' => $formationMap,
            'holidayMap'   => $holidayMap,
        ]);

        $dompdf->loadHtml($html);
        $dompdf->setPaper('A3', 'landscape');
        $dompdf->render();

        $filename = sprintf(
            'absences_du_%s_au_%s.pdf',
            $fromDate->format('d-m-Y'),
            $toDate->format('d-m-Y')
        );

        return new Response($dompdf->output(), Response::HTTP_OK, [
            'Content-Type'        => 'application/pdf',
            'Content-Disposition' => 'inline; filename="'.$filename.'"',
        ]);
    }

    private function parseIds(?string $value): array
    {
        if (null === $value || '' === trim($value)) {
            return [];
        }

        $ids = [];
        foreach (explode(',', $value) as $part) {
            $id = (int) trim($part);
            if ($id > 0) {
                $ids[] = $id;
            }
        }

        return array_values(array_unique($ids));
    }

    private function loadEmployees(array $siteIds, array $contractNatures, array $workContractIds, DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $employees = $this->employeeRepository->findForPrintBySiteIds($siteIds);

        return array_values(array_filter($employees, function ($employee) use ($contractNatures, $workContractIds, $from, $to): bool {
            $employeeNature     = (string) $employee->getCurrentContractNature();
            $employeeContractId = $employee->getContract()?->getId();

            $natureMatches   = [] === $contractNatures || in_array($employeeNature, $contractNatures, true);
            $contractMatches = [] === $workContractIds || (null !== $employeeContractId && in_array($employeeContractId, $workContractIds, true));

            // Exclut les employés dont aucune période de contrat n'intersecte la période imprimée
            // (ex. un salarié parti en avril ne doit pas apparaître sur une impression de mai).
            return $natureMatches && $contractMatches && $this->hasContractInRange($employee, $from, $to);
        }));
    }

    /**
     * Vrai si au moins une période de contrat de l'employé intersecte [$from, $to].
     * Une période sans date de fin (contrat en cours) est considérée ouverte jusqu'à l'infini.
     * Comparaison sur la date seule (`Y-m-d`), insensible à l'heure des bornes — aligné avec
     * le filtre `hasContractInSelectedMonth` de la vue Calendrier (comparaison de chaînes).
     */
    private function hasContractInRange(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): bool
    {
        $fromDay = $from->format('Y-m-d');
        $toDay   = $to->format('Y-m-d');

        foreach ($employee->getContractPeriods() as $period) {
            $start = $period->getStartDate()->format('Y-m-d');
            $end   = $period->getEndDate()?->format('Y-m-d');
            if ($start <= $toDay && (null === $end || $end >= $fromDay)) {
                return true;
            }
        }

        return false;
    }

    private function loadAbsences(DateTimeImmutable $from, DateTimeImmutable $to, array $employees): array
    {
        return $this->absenceRepository->findForPrint($from, $to, $employees);
    }

    private function buildDays(DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $days    = [];
        $current = $from;

        while ($current <= $to) {
            $days[] = [
                'date'  => $current->format('Y-m-d'),
                'label' => $current->format('d'),
            ];

            $current = $current->add(new DateInterval('P1D'));
        }

        return $days;
    }

    private function buildAbsenceMap(array $absences, DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $map = [];

        foreach ($absences as $absence) {
            $employeeId = $absence->getEmployee()?->getId();
            $type       = $absence->getType();

            if (!$employeeId || !$type) {
                continue;
            }

            $absenceStart = DateTimeImmutable::createFromInterface($absence->getStartDate());
            $absenceEnd   = DateTimeImmutable::createFromInterface($absence->getEndDate());
            $startHalf    = $absence->getStartHalf();
            $endHalf      = $absence->getEndHalf();

            $start = max($absenceStart, $from);
            $end   = min($absenceEnd, $to);

            $current = $start;
            while ($current <= $end) {
                $dateKey    = $current->format('Y-m-d');
                $isSameDay  = $absenceStart->format('Y-m-d') === $absenceEnd->format('Y-m-d');
                $isStartDay = $current->format('Y-m-d') === $absenceStart->format('Y-m-d');
                $isEndDay   = $current->format('Y-m-d') === $absenceEnd->format('Y-m-d');
                $halfLabel  = null;

                if ($isSameDay) {
                    if ($startHalf === $endHalf) {
                        $halfLabel = $startHalf->value;
                    }
                } else {
                    if ($isStartDay && HalfDay::PM === $startHalf) {
                        $halfLabel = 'PM';
                    }
                    if ($isEndDay && HalfDay::AM === $endHalf) {
                        $halfLabel = 'AM';
                    }
                }

                $map[$employeeId][$dateKey] = [
                    'code'      => (string) $type->getCode(),
                    'color'     => (string) $type->getColor(),
                    'half'      => null !== $halfLabel,
                    'halfLabel' => $halfLabel,
                ];

                $current = $current->add(new DateInterval('P1D'));
            }
        }

        return $map;
    }

    /**
     * @param list<Formation> $formations
     *
     * @return array<int, array<string, bool>>
     */
    private function buildFormationMap(array $formations, DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $map = [];

        foreach ($formations as $formation) {
            $employeeId = $formation->getEmployee()?->getId();
            if (!$employeeId) {
                continue;
            }

            $formationStart = DateTimeImmutable::createFromInterface($formation->getStartDate());
            $formationEnd   = DateTimeImmutable::createFromInterface($formation->getEndDate());

            $start = max($formationStart, $from);
            $end   = min($formationEnd, $to);

            $current = $start;
            while ($current <= $end) {
                $map[$employeeId][$current->format('Y-m-d')] = true;
                $current                                     = $current->add(new DateInterval('P1D'));
            }
        }

        return $map;
    }

    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[$date] = (string) $label;
                }
            }
        } catch (Throwable) {
            return [];
        }

        return $map;
    }

    /**
     * @return list<string>
     */
    private function parseContractNatures(?string $value): array
    {
        if (null === $value || '' === trim($value)) {
            return [];
        }

        $values = [];
        foreach (explode(',', $value) as $part) {
            $nature = strtoupper(trim($part));
            if (null !== ContractNature::tryFrom($nature)) {
                $values[] = $nature;
            }
        }

        return array_values(array_unique($values));
    }
}
