<?php

declare(strict_types=1);

namespace App\Module\Absence\Infrastructure\ApiPlatform\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceStatus;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

/**
 * Admin approval / rejection of a pending absence request. The target status
 * is derived from the operation URI (.../approve or .../reject).
 *
 * @implements ProcessorInterface<AbsenceRequest, AbsenceRequest>
 */
final readonly class AbsenceReviewProcessor implements ProcessorInterface
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private Security $security,
        private AbsenceBalanceService $balanceService,
    ) {}

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): AbsenceRequest
    {
        assert($data instanceof AbsenceRequest);

        $isApprove          = str_contains((string) $operation->getUriTemplate(), 'approve');
        $newRejectionReason = $data->getRejectionReason();

        // Reviewing must never alter the request content: restore everything
        // from the persisted state, only status/review fields may change.
        $previous = $context['previous_data'] ?? null;
        if ($previous instanceof AbsenceRequest) {
            $data->setType($previous->getType());
            $data->setStartDate($previous->getStartDate());
            $data->setEndDate($previous->getEndDate());
            $data->setStartHalfDay($previous->getStartHalfDay());
            $data->setEndHalfDay($previous->getEndHalfDay());
            $data->setReason($previous->getReason());
            $data->setCountedDays($previous->getCountedDays());
        }

        if (AbsenceStatus::Pending !== $data->getStatus()) {
            throw new ConflictHttpException('Only a pending request can be reviewed.');
        }

        $admin = $this->security->getUser();
        assert($admin instanceof UserInterface);

        if ($isApprove) {
            // Never let an approval push the balance below zero (CP only): the
            // days being accrued (N) are posable, but not beyond the entitlement.
            $available = $this->balanceService->availableForRequest($data);
            if (null !== $available && $data->getCountedDays() > $available + 1e-9) {
                throw new UnprocessableEntityHttpException(sprintf(
                    'Approving this request would put the balance below zero: %g day(s) requested but only %g available.',
                    $data->getCountedDays(),
                    $available,
                ));
            }

            $data->setStatus(AbsenceStatus::Approved);
            $data->setRejectionReason(null);
            $this->balanceService->applyApproval($data);
        } else {
            if (null === $newRejectionReason || '' === trim($newRejectionReason)) {
                throw new UnprocessableEntityHttpException('A reason is required when rejecting a request.');
            }
            $data->setStatus(AbsenceStatus::Rejected);
            $data->setRejectionReason($newRejectionReason);
            $this->balanceService->release($data, false);
        }

        $data->setReviewedAt(new DateTimeImmutable());
        $data->setReviewedBy($admin);

        $this->entityManager->flush();

        return $data;
    }
}
