<?php

declare(strict_types=1);

namespace App\State;

use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Absence;
use App\Entity\Employee;
use App\Entity\User;
use App\Enum\HalfDay;
use App\Repository\Contract\AbsenceReadRepositoryInterface;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Service\AuditLogger;
use DateInterval;
use DatePeriod;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

final readonly class AbsenceWriteProcessor implements ProcessorInterface
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private AbsenceReadRepositoryInterface $absenceRepository,
        private WorkHourReadRepositoryInterface $workHourRepository,
        private Security $security,
        private AuditLogger $auditLogger,
    ) {}

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
    {
        if (!$data instanceof Absence) {
            return $data;
        }

        $employee = $data->getEmployee();
        if (null === $employee) {
            return $data;
        }

        $user    = $this->security->getUser();
        $isAdmin = $user instanceof User && in_array('ROLE_ADMIN', $user->getRoles(), true);

        if ($operation instanceof DeleteOperationInterface) {
            if ($this->isLockedByValidation($employee, $data->getStartDate(), $data->getEndDate(), $isAdmin)) {
                throw new ConflictHttpException('Impossible de modifier une absence sur une période validée.');
            }

            $typeName  = $data->getType()?->getLabel() ?? 'inconnu';
            $startDate = $data->getStartDate()->format('d/m/Y');
            $endDate   = $data->getEndDate()->format('d/m/Y');
            $empName   = trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? ''));

            $this->auditLogger->log(
                $employee,
                'delete',
                'absence',
                $data->getId(),
                sprintf('Absence %s supprimée pour %s du %s au %s', $typeName, $empName, $startDate, $endDate),
                ['old' => ['type' => $typeName, 'start' => $startDate, 'end' => $endDate, 'startHalf' => $data->getStartHalf()->value, 'endHalf' => $data->getEndHalf()->value, 'comment' => $data->getComment()]],
                DateTimeImmutable::createFromInterface($data->getStartDate()),
            );

            $this->entityManager->remove($data);
            $this->entityManager->flush();

            return null;
        }

        $segments = $this->expandAbsenceRange($data);
        if ([] === $segments) {
            throw new UnprocessableEntityHttpException('La période de l\'absence est invalide.');
        }

        $from = DateTimeImmutable::createFromInterface($segments[0]['date']);
        $to   = DateTimeImmutable::createFromInterface($segments[count($segments) - 1]['date']);

        if ($this->isLockedByValidation($employee, $from, $to, $isAdmin)) {
            throw new ConflictHttpException('Impossible de modifier une absence sur une période validée.');
        }

        $existing = $this->absenceRepository->findByEmployeeAndDateRange($employee, $from, $to);
        foreach ($existing as $existingAbsence) {
            if ($existingAbsence->getId() === $data->getId()) {
                continue;
            }

            throw new ConflictHttpException('Cette période chevauche déjà une absence existante.');
        }

        $first = array_shift($segments);
        if (null === $first) {
            throw new UnprocessableEntityHttpException('La période de l\'absence est invalide.');
        }

        $data
            ->setStartDate($this->toMutableDate($first['date']))
            ->setEndDate($this->toMutableDate($first['date']))
            ->setStartHalf($first['startHalf'])
            ->setEndHalf($first['endHalf'])
        ;
        $this->clearWorkHoursForSegment($employee, $first);
        $this->entityManager->persist($data);

        foreach ($segments as $segment) {
            $absence = new Absence()
                ->setEmployee($employee)
                ->setType($data->getType())
                ->setComment($data->getComment())
                ->setStartDate($this->toMutableDate($segment['date']))
                ->setEndDate($this->toMutableDate($segment['date']))
                ->setStartHalf($segment['startHalf'])
                ->setEndHalf($segment['endHalf'])
            ;

            $this->clearWorkHoursForSegment($employee, $segment);
            $this->entityManager->persist($absence);
        }

        $typeName  = $data->getType()?->getLabel() ?? 'inconnu';
        $startDate = $data->getStartDate()->format('d/m/Y');
        $endDate   = $data->getEndDate()->format('d/m/Y');
        $empName   = trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? ''));

        $this->auditLogger->log(
            $employee,
            'create',
            'absence',
            null,
            sprintf('Absence %s créée pour %s du %s au %s', $typeName, $empName, $startDate, $endDate),
            ['new' => ['type' => $typeName, 'start' => $startDate, 'end' => $endDate, 'startHalf' => $data->getStartHalf()->value, 'endHalf' => $data->getEndHalf()->value, 'comment' => $data->getComment()]],
            DateTimeImmutable::createFromInterface($data->getStartDate()),
        );

        $this->entityManager->flush();

        return $data;
    }

    /**
     * @return list<array{date: DateTimeImmutable, startHalf: HalfDay, endHalf: HalfDay}>
     */
    private function expandAbsenceRange(Absence $absence): array
    {
        $start = DateTimeImmutable::createFromInterface($absence->getStartDate());
        $end   = DateTimeImmutable::createFromInterface($absence->getEndDate());

        if ($start > $end) {
            throw new UnprocessableEntityHttpException('La date de fin ne peut pas être avant la date de début.');
        }

        if (
            $start->format('Y-m-d') === $end->format('Y-m-d')
            && HalfDay::PM === $absence->getStartHalf()
            && HalfDay::AM === $absence->getEndHalf()
        ) {
            throw new UnprocessableEntityHttpException('La demi-journée de fin ne peut pas être avant la demi-journée de début.');
        }

        $days = new DatePeriod($start, new DateInterval('P1D'), $end->modify('+1 day'));

        $segments = [];
        foreach ($days as $day) {
            $isFirst = $day->format('Y-m-d') === $start->format('Y-m-d');
            $isLast  = $day->format('Y-m-d') === $end->format('Y-m-d');
            $isSame  = $isFirst && $isLast;

            if ($isSame) {
                $segments[] = [
                    'date'      => $day,
                    'startHalf' => $absence->getStartHalf(),
                    'endHalf'   => $absence->getEndHalf(),
                ];

                continue;
            }

            if ($isFirst && HalfDay::PM === $absence->getStartHalf()) {
                $segments[] = [
                    'date'      => $day,
                    'startHalf' => HalfDay::PM,
                    'endHalf'   => HalfDay::PM,
                ];

                continue;
            }

            if ($isLast && HalfDay::AM === $absence->getEndHalf()) {
                $segments[] = [
                    'date'      => $day,
                    'startHalf' => HalfDay::AM,
                    'endHalf'   => HalfDay::AM,
                ];

                continue;
            }

            $segments[] = [
                'date'      => $day,
                'startHalf' => HalfDay::AM,
                'endHalf'   => HalfDay::PM,
            ];
        }

        return $segments;
    }

    private function toMutableDate(DateTimeImmutable $date): DateTime
    {
        return DateTime::createFromImmutable($date);
    }

    private function isLockedByValidation(Employee $employee, DateTimeInterface $from, DateTimeInterface $to, bool $isAdmin): bool
    {
        if ($this->workHourRepository->hasValidatedInRange($employee, $from, $to)) {
            return true;
        }

        if ($isAdmin) {
            return false;
        }

        return $this->workHourRepository->hasSiteValidatedInRange($employee, $from, $to);
    }

    /**
     * @param array{date: DateTimeImmutable, startHalf: HalfDay, endHalf: HalfDay} $segment
     */
    private function clearWorkHoursForSegment(Employee $employee, array $segment): void
    {
        $workHour = $this->workHourRepository->findOneByEmployeeAndDate($employee, $segment['date']);
        if (null === $workHour) {
            return;
        }

        // Demi-journée matin: on efface uniquement la plage du matin.
        if (HalfDay::AM === $segment['startHalf'] && HalfDay::AM === $segment['endHalf']) {
            $workHour
                ->setMorningFrom(null)
                ->setMorningTo(null)
                ->setIsSiteValid(false)
                ->setIsValid(false)
            ;

            return;
        }

        // Demi-journée après-midi: on efface après-midi + soirée.
        if (HalfDay::PM === $segment['startHalf'] && HalfDay::PM === $segment['endHalf']) {
            $workHour
                ->setAfternoonFrom(null)
                ->setAfternoonTo(null)
                ->setEveningFrom(null)
                ->setEveningTo(null)
                ->setIsSiteValid(false)
                ->setIsValid(false)
            ;

            return;
        }

        // Journée complète: on efface toutes les plages horaires.
        $workHour
            ->setMorningFrom(null)
            ->setMorningTo(null)
            ->setAfternoonFrom(null)
            ->setAfternoonTo(null)
            ->setEveningFrom(null)
            ->setEveningTo(null)
            ->setIsSiteValid(false)
            ->setIsValid(false)
        ;
    }
}
