<?php

namespace App\Commands;

use App\Service\Cron\CronjobInterface;
use App\Service\Cron\UI\ConsoleUI;
use App\Service\ServiceIterator\CronjobIterator;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LockableTrait;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\Service\Attribute\Required;

/**
 * CLI Command to run the cron-jobs
 *
 * Ex:
 *
 *     bin/console ot:cron list
 *     bin/console ot:cron run clean-db --preview
 *     bin/console ot:cron run clean-db
 *
 * @see ~/src/Service/Cron/Readme.md
 */
#[AsCommand(
    name: 'ot:cron',
    description: 'Executes cron jobs'
)]
class CronCommand extends Command
{
    use LockableTrait;

    private const ACTION_LIST = 'list';
    private const ACTION_RUN = 'run';
    private const ACTION_RUN_ALL = 'all';

    private const ACTIONS = [
        self::ACTION_LIST => 'List registered jobs',
        self::ACTION_RUN => 'Run the given job',
        self::ACTION_RUN_ALL => 'Successively run all the registered cron jobs'
    ];

    private OutputInterface $output;
    private LoggerInterface $logger;
    private CronjobIterator $cronjobIterator;

    #[Required]
    /** @see https://symfony.com/doc/current/logging/channels_handlers.html#how-to-autowire-logger-channels */
    public function setLoggerInterface(LoggerInterface $cronLogger): void { $this->logger = $cronLogger; }
    #[Required]
    public function setCronjobIterator(CronjobIterator $cronjobIterator): void { $this->cronjobIterator = $cronjobIterator; }

    /**
     * Configures the command
     */
    protected function configure(): void
    {
        $this->addArgument(
            'action',
            InputArgument::REQUIRED,
            'Action to execute among : ' .
            implode(
                ', ',
                array_map(
                    static function ($v, $k) { return "'" . $k . "' (" . $v . ")"; },
                    self::ACTIONS,
                    array_keys(self::ACTIONS)
                )
            )
        );
        $this->addArgument(
            'jobs',
            InputArgument::OPTIONAL,
            "Name(s) of the cron-job(s) to execute with '" . self::ACTION_RUN . "' (comma-separated)"
        );
        $this->addOption(
            'preview',
            'p',
            InputOption::VALUE_NONE,
            'Only preview the operations instead of executing them'
        );
    }

    /**
     * Executes the command
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return int
     */
    final protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->output = $output;

        /** @var FormatterHelper $formatter */
        $formatter = $this->getHelper('formatter');

        $action = $input->getArgument('action');
        $jobNames = $input->getArgument('jobs');
        $preview = $input->getOption('preview');
        $jobs = [];

        if (!array_key_exists($action, self::ACTIONS)) {
            $this->output->writeln($formatter->formatBlock('Error: unrecognized action', 'error'));
            return Command::INVALID;
        }

        if ($action === self::ACTION_LIST) {
            $this->listJobs();
            return Command::SUCCESS;
        }

        if ($action === self::ACTION_RUN_ALL) {
            $jobs = $this->cronjobIterator->getAll();
        }

        if ($action === self::ACTION_RUN) {
            foreach (explode(',', $jobNames) as $name) {
                try {
                    $jobs[] = $this->cronjobIterator->getByName($name);
                } catch (\RuntimeException $e) {
                    $this->output->writeln($e->getMessage());
                    $this->listJobs();
                    return Command::INVALID;
                }
            }
        }

        $this->logger->info(
            'CronCommand will ' .
            ($preview ? 'preview' : 'execute') . ' ' .
            implode(', ', array_map(static function($job) { return $job->name(); }, $jobs))
        );

        $results = [];

        foreach ($jobs as $job) {
            $results[] = $this->runJob($job, $preview);
        }

        return (int) max($results); // If there is one failure result, the whole command is shown as a failure too
    }

    /**
     * List all available cron jobs
     */
    private function listJobs(): void {
        $availableJobs = $this->cronjobIterator->getAll();

        if (empty($availableJobs)) {
            $this->output->writeln('No cronjob found');
            return;
        }

        $this->output->writeln('Available cron jobs : ');
        foreach ($this->cronjobIterator->getAll() as $job) {
            $this->output->writeln('* ' . $job->name());
        }
    }

    /**
     * Run one Cronjob
     *
     * @param CronJobInterface $job
     * @param bool $preview
     * @return int
     */
    private function runJob(CronjobInterface $job, bool $preview = false): int
    {
        /** @var FormatterHelper $formatter */
        $formatter = $this->getHelper('formatter');

        if (!$this->lock($job->name())) {
            $msg = 'The command ' . $job->name() . ' is already running in another process. Abort.';
            $this->output->writeln($formatter->formatBlock($msg, 'error'));
            $this->logger->error($msg);
            return Command::FAILURE;
        }

        $t0 = microtime(true);

        $this->output->writeln(
            $formatter->formatSection($job->name(),"Start" . ($preview ? ' [PREVIEW MODE]' : ''))
        );

        // Establish communication between job and the console
        $ui = new ConsoleUI($this->output);
        $job->setUI($ui);

        try {
            if ($preview) {
                $job->preview();
            } else {
                $job->execute();
            }
        } catch (RuntimeException $e) {
            $this->logger->critical($e);
            $this->output->write("An error happened while running the process : " . $e);
            return Command::FAILURE;
        }

        $t1 = microtime(true);

        $msg = "Job has been successfully executed (" . round($t1 - $t0, 2) . " sec.)" . ($preview ? ' [PREVIEW MODE]' : '');
        $this->output->writeln($formatter->formatSection($job->name(), $msg));
        $this->logger->info($job->name() . ' - ' . $msg);

        return Command::SUCCESS;
    }
}
