<?php

declare(strict_types=1);

namespace App\Module\Absence\Application\Service;

use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Entity\AbsenceRequest;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
use App\Shared\Domain\Contract\LeaveProfileInterface;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeInterface;
use Doctrine\ORM\EntityManagerInterface;

/**
 * Maintains per-employee leave balances as absence requests move through their
 * lifecycle: a PENDING request reserves days in `pending`, an APPROVED one
 * moves them to `taken`, and a cancellation gives them back.
 */
final readonly class AbsenceBalanceService
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private AbsenceBalanceRepositoryInterface $balanceRepository,
    ) {}

    /**
     * Reference period string for a request: paid leave follows the employee's
     * reference period (e.g. "2025-2026"), other types are tracked yearly.
     */
    public function periodFor(UserInterface $user, AbsenceType $type, DateTimeInterface $date): string
    {
        if (AbsenceType::PaidLeave !== $type) {
            return $date->format('Y');
        }

        $year = (int) $date->format('Y');
        // The reference-period start (e.g. "06-01") is an HR profile field,
        // accessed through the LeaveProfileInterface contract to keep the
        // Absence module decoupled from the concrete Core User entity.
        $startMonthDay   = $user instanceof LeaveProfileInterface ? $user->getReferencePeriodStart() : '01-01';
        $currentMonthDay = $date->format('m-d');

        $startYear = $currentMonthDay >= $startMonthDay ? $year : $year - 1;

        return sprintf('%d-%d', $startYear, $startYear + 1);
    }

    public function getOrCreateBalance(UserInterface $user, AbsenceType $type, string $period): AbsenceBalance
    {
        $balance = $this->balanceRepository->findOneForPeriod($user, $type, $period);

        if (null === $balance) {
            $balance = new AbsenceBalance()
                ->setUser($user)
                ->setType($type)
                ->setPeriod($period)
            ;
            $this->entityManager->persist($balance);
        }

        return $balance;
    }

    /** Reserve the requested days in the PENDING bucket. */
    public function reservePending(AbsenceRequest $request): void
    {
        if (!$this->shouldTrack($request)) {
            return;
        }

        $balance = $this->balanceForRequest($request);
        $balance->setPending($balance->getPending() + $request->getCountedDays());
    }

    /**
     * Days still available to take in the request's balance period
     * (acquired N-1 + acquiring N − already taken), or null when the type is
     * not balance-tracked (per-event leaves such as bereavement or marriage).
     *
     * Days currently reserved in PENDING are intentionally not subtracted: the
     * request being reviewed already sits in that pending bucket, and approval
     * only moves it to TAKEN.
     */
    public function availableForRequest(AbsenceRequest $request): ?float
    {
        if (!$this->shouldTrack($request)) {
            return null;
        }

        /** @var UserInterface $user */
        $user    = $request->getUser();
        $period  = $this->periodFor($user, $request->getType(), $request->getStartDate());
        $balance = $this->balanceRepository->findOneForPeriod($user, $request->getType(), $period);

        return $balance?->getAvailable() ?? 0.0;
    }

    /** Move reserved days from PENDING to TAKEN on approval. */
    public function applyApproval(AbsenceRequest $request): void
    {
        if (!$this->shouldTrack($request)) {
            return;
        }

        $balance = $this->balanceForRequest($request);
        $balance->setPending(max(0.0, $balance->getPending() - $request->getCountedDays()));
        $balance->setTaken($balance->getTaken() + $request->getCountedDays());
    }

    /**
     * Give days back when a request is cancelled or rejected.
     *
     * @param bool $wasApproved true if the request had already been approved
     *                          (days were in TAKEN), false if still PENDING
     */
    public function release(AbsenceRequest $request, bool $wasApproved): void
    {
        if (!$this->shouldTrack($request)) {
            return;
        }

        $balance = $this->balanceForRequest($request);

        if ($wasApproved) {
            $balance->setTaken(max(0.0, $balance->getTaken() - $request->getCountedDays()));
        } else {
            $balance->setPending(max(0.0, $balance->getPending() - $request->getCountedDays()));
        }
    }

    private function balanceForRequest(AbsenceRequest $request): AbsenceBalance
    {
        /** @var UserInterface $user */
        $user   = $request->getUser();
        $type   = $request->getType();
        $period = $this->periodFor($user, $type, $request->getStartDate());

        return $this->getOrCreateBalance($user, $type, $period);
    }

    private function shouldTrack(AbsenceRequest $request): bool
    {
        $type = $request->getType();

        return null !== $type && $type->decrementsBalance() && null !== $request->getUser();
    }
}
