<?php

declare(strict_types=1);

namespace App\Module\Absence\Infrastructure\Command;

use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Repository\AbsenceBalanceRepositoryInterface;
use App\Module\Core\Domain\Repository\UserRepositoryInterface;
use App\Shared\Domain\Contract\LeaveProfileInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

use function preg_match;
use function sprintf;

/**
 * Monthly paid-leave accrual. For each active employee it credits one twelfth
 * of their yearly entitlement (prorated by work-time ratio) to the current
 * reference-period balance. Idempotent per month thanks to lastAccruedMonth,
 * and it seeds the initial balance when the period balance is first created.
 *
 * Intended to run on the 1st of each month (cron). Notifications are out of
 * scope for now.
 */
#[AsCommand(
    name: 'app:absences:accrue-leave',
    description: 'Credit the monthly paid-leave accrual to every active employee',
)]
class AccrueLeaveCommand extends Command
{
    public function __construct(
        private readonly UserRepositoryInterface $userRepository,
        private readonly AbsenceBalanceRepositoryInterface $balanceRepository,
        private readonly AbsenceBalanceService $balanceService,
        private readonly EntityManagerInterface $entityManager,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption('month', null, InputOption::VALUE_REQUIRED, 'Target month (YYYY-MM), defaults to the current month')
            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Compute and display without persisting')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $monthOpt = $input->getOption('month');
        $dryRun   = (bool) $input->getOption('dry-run');

        try {
            $firstDay = $monthOpt
                ? new DateTimeImmutable($monthOpt.'-01')
                : new DateTimeImmutable('first day of this month');
        } catch (Exception) {
            $io->error('Invalid --month, expected format YYYY-MM.');

            return Command::FAILURE;
        }

        $firstDay = $firstDay->setTime(0, 0);
        $lastDay  = $firstDay->modify('last day of this month');
        $monthKey = $firstDay->format('Y-m');

        $io->title(sprintf('Acquisition CP — %s%s', $monthKey, $dryRun ? ' (dry-run)' : ''));

        $employees = $this->userRepository->findActiveEmployees($lastDay);
        if ([] === $employees) {
            $io->warning('Aucun salarié actif pour ce mois.');

            return Command::SUCCESS;
        }

        $rows    = [];
        $accrued = 0;
        $skipped = 0;

        foreach ($employees as $user) {
            // RH leave profile fields are read through the contract to keep the
            // Absence module decoupled from the concrete Core User entity.
            $profile = $user instanceof LeaveProfileInterface ? $user : null;
            if (null === $profile) {
                continue;
            }

            $rate   = ($profile->getAnnualLeaveDays() / 12) * $profile->getWorkTimeRatio();
            $period = $this->balanceService->periodFor($user, AbsenceType::PaidLeave, $firstDay);

            $balance = $this->balanceRepository->findOneForPeriod($user, AbsenceType::PaidLeave, $period);
            $isNew   = null === $balance;

            if ($isNew) {
                $balance = $this->balanceService->getOrCreateBalance($user, AbsenceType::PaidLeave, $period);
                // On a new period, the previous period's "en cours d'acquisition" (N)
                // becomes this period's acquired (N-1). At roll-out (no prior balance)
                // seed the configured initial balance instead.
                $previousPeriod  = self::previousPeriod($period);
                $previousBalance = null !== $previousPeriod
                    ? $this->balanceRepository->findOneForPeriod($user, AbsenceType::PaidLeave, $previousPeriod)
                    : null;

                if (null !== $previousBalance) {
                    // Only the days *not yet taken* carry over. Leave is charged
                    // oldest-first: it first consumes the previous "acquired"
                    // (N-2) bucket — which expires at roll-over anyway — so only
                    // days taken beyond that bucket eat into the carry-over.
                    $carryOver = $previousBalance->getAcquiring()
                        - max(0.0, $previousBalance->getTaken() - $previousBalance->getAcquired());
                    $balance->setAcquired(max(0.0, $carryOver));
                } else {
                    $balance->setAcquired($profile->getInitialLeaveBalance());
                }
            }

            if ($monthKey === $balance->getLastAccruedMonth()) {
                ++$skipped;
                $rows[] = [$user->getUsername(), $period, number_format($balance->getAcquired(), 2), number_format($balance->getAcquiring(), 2), 'déjà fait'];

                continue;
            }

            $balance->setAcquiring($balance->getAcquiring() + $rate);
            $balance->setLastAccruedMonth($monthKey);
            ++$accrued;

            $seeded = $isNew && (null !== self::previousPeriod($period) || $profile->getInitialLeaveBalance() > 0);
            $rows[] = [
                $user->getUsername(),
                $period,
                number_format($balance->getAcquired(), 2),
                number_format($balance->getAcquiring(), 2),
                sprintf('+%s%s', number_format($rate, 2), $seeded && $balance->getAcquired() > 0 ? ' (N-1 reporté)' : ''),
            ];
        }

        if (!$dryRun) {
            $this->entityManager->flush();
        }

        $io->table(['Salarié', 'Période', 'Acquis (N-1)', 'En cours (N)', 'Action'], $rows);
        $io->success(sprintf('%d crédité(s), %d ignoré(s)%s.', $accrued, $skipped, $dryRun ? ' (dry-run, rien enregistré)' : ''));

        return Command::SUCCESS;
    }

    /** Previous reference period for a "YYYY-YYYY" paid-leave period, or null. */
    private static function previousPeriod(string $period): ?string
    {
        if (1 !== preg_match('/^(\d{4})-(\d{4})$/', $period, $m)) {
            return null;
        }

        return sprintf('%d-%d', (int) $m[1] - 1, (int) $m[2] - 1);
    }
}
