<?php

declare(strict_types=1);

namespace App\Command;

use App\Entity\Employee;
use App\Entity\EmployeeLeaveBalance;
use App\Enum\ContractType;
use App\Enum\LeaveRuleCode;
use App\Repository\EmployeeLeaveBalanceRepository;
use App\Repository\EmployeeRepository;
use App\Service\Leave\LeaveBalanceComputationService;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
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 Symfony\Component\DependencyInjection\Attribute\Autowire;
use Throwable;

#[AsCommand(
    name: 'app:leave:rollover',
    description: 'Create yearly leave opening balances (idempotent).'
)]
final class LeaveRolloverCommand extends Command
{
    public function __construct(
        private readonly EmployeeRepository $employeeRepository,
        private readonly EmployeeLeaveBalanceRepository $leaveBalanceRepository,
        private readonly LeaveBalanceComputationService $leaveBalanceComputationService,
        private readonly EntityManagerInterface $entityManager,
        #[Autowire(service: 'monolog.logger.cron')]
        private readonly LoggerInterface $logger,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->addOption(
            'force',
            null,
            InputOption::VALUE_NONE,
            'Run rollover regardless of business date (manual recovery mode).'
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io    = new SymfonyStyle($input, $output);
        $today = new DateTimeImmutable('today');
        $force = (bool) $input->getOption('force');

        $this->logger->info('app:leave:rollover started.', ['date' => $today->format('Y-m-d'), 'force' => $force]);

        if (!$force && !$this->isBusinessRolloverDate($today)) {
            $message = 'No rollover today: business date is neither 01/01 nor 01/06.';
            $this->logger->info($message, ['date' => $today->format('Y-m-d')]);
            $io->success($message);

            return Command::SUCCESS;
        }

        $created = 0;
        $skipped = 0;

        foreach ($this->employeeRepository->findAll() as $employee) {
            if (!$employee instanceof Employee) {
                continue;
            }

            $ruleCode = $this->resolveRuleCode($employee);
            if (null === $ruleCode) {
                $this->logger->info('Employee skipped: no eligible rule.', ['employeeId' => $employee->getId()]);
                ++$skipped;

                continue;
            }
            if (!$force && !$this->shouldProcessRuleToday($ruleCode, $today)) {
                ++$skipped;

                continue;
            }

            $targetYear = $this->resolveTargetYear($ruleCode, $today);
            $existing   = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $targetYear);
            if (null !== $existing) {
                $this->logger->info('Employee skipped: balance already exists.', ['employeeId' => $employee->getId(), 'year' => $targetYear, 'rule' => $ruleCode->value]);
                ++$skipped;

                continue;
            }

            try {
                [$carryDays, $carrySaturdays] = $this->resolveCarry($employee, $ruleCode, $targetYear);
            } catch (Throwable $e) {
                $this->logger->error('Error computing carry for employee.', ['employeeId' => $employee->getId(), 'error' => $e->getMessage()]);
                ++$skipped;

                continue;
            }

            $balance = new EmployeeLeaveBalance()
                ->setEmployee($employee)
                ->setRuleCode($ruleCode)
                ->setYear($targetYear)
                ->setOpeningDays($carryDays)
                ->setOpeningSaturdays($carrySaturdays)
                ->setAccruedDays(0.0)
                ->setAccruedSaturdays(0.0)
                ->setTakenDays(0.0)
                ->setTakenSaturdays(0.0)
                ->setClosingDays($carryDays)
                ->setClosingSaturdays($carrySaturdays)
                ->setIsLocked(false)
            ;

            $this->entityManager->persist($balance);
            $this->logger->info('Balance created.', ['employeeId' => $employee->getId(), 'year' => $targetYear, 'rule' => $ruleCode->value, 'carryDays' => $carryDays, 'carrySaturdays' => $carrySaturdays]);
            ++$created;
        }

        try {
            $this->entityManager->flush();
        } catch (Throwable $e) {
            $this->logger->error('Error flushing leave balances.', ['error' => $e->getMessage()]);
            $io->error('Leave rollover failed: '.$e->getMessage());

            return Command::FAILURE;
        }

        $message = sprintf('Leave rollover done: %d created, %d skipped.', $created, $skipped);
        $this->logger->info($message);
        $io->success($message);

        return Command::SUCCESS;
    }

    private function resolveRuleCode(Employee $employee): ?LeaveRuleCode
    {
        $type = $employee->getContract()?->getType();
        if (null === $type || ContractType::INTERIM === $type) {
            return null;
        }

        if (ContractType::FORFAIT === $type) {
            return LeaveRuleCode::FORFAIT_218;
        }

        return LeaveRuleCode::CDI_CDD_NON_FORFAIT;
    }

    private function resolveTargetYear(LeaveRuleCode $ruleCode, DateTimeImmutable $today): int
    {
        if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
            return (int) $today->format('Y');
        }

        $year  = (int) $today->format('Y');
        $month = (int) $today->format('n');

        return $month >= 6 ? $year + 1 : $year;
    }

    private function isBusinessRolloverDate(DateTimeImmutable $today): bool
    {
        return in_array($today->format('m-d'), ['01-01', '06-01'], true);
    }

    private function shouldProcessRuleToday(LeaveRuleCode $ruleCode, DateTimeImmutable $today): bool
    {
        $day = $today->format('m-d');
        if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
            return '01-01' === $day;
        }

        return '06-01' === $day;
    }

    /**
     * @return array{float, float}
     */
    private function resolveCarry(Employee $employee, LeaveRuleCode $ruleCode, int $targetYear): array
    {
        $previousYear = $targetYear - 1;

        [$from, $to]   = $this->leaveBalanceComputationService->resolvePeriodBounds($ruleCode, $previousYear);
        $hasSettlement = $this->leaveBalanceComputationService
            ->hasPaidLeaveSettledClosureBetween($employee, $from, $to)
        ;

        if ($hasSettlement) {
            $carryDays      = 0.0;
            $carrySaturdays = 0.0;
        } else {
            // Compute the REAL closing of the ending exercise. computeDynamicClosingForYear
            // is bootstrap-aware (it anchors on the persisted opening balance of each year)
            // and already folds in accrual, taken absences and fractioned days. We must NOT
            // trust the stored closing_days: it is only ever written equal to the opening at
            // row creation (placeholder), so trusting it would propagate the opening and
            // ignore the year's accrual (cas Aurore : report 0 au lieu de 31).
            [$carryDays, $carrySaturdays] = $this->leaveBalanceComputationService
                ->computeDynamicClosingForYear($employee, $ruleCode, $previousYear)
            ;
        }

        // Freeze the computed closing on the ending exercise's row so the column finally
        // holds a real, auditable value. The cron is idempotent — it never reaches here for
        // an already-rolled target year (existing rows are skipped upstream) — so a row that
        // was corrected manually in the DB afterwards is never overwritten.
        $previous = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $previousYear);
        if (null !== $previous) {
            $previous->setClosingDays($carryDays);
            $previous->setClosingSaturdays($carrySaturdays);
        }

        return [$carryDays, $carrySaturdays];
    }
}
