<?php

declare(strict_types=1);

namespace Churn\Command;

use Churn\Command\Helper\MaxScoreChecker;
use Churn\Command\Helper\ProgressBarSubscriber;
use Churn\Configuration\Config;
use Churn\Configuration\Loader;
use Churn\Event\Broker;
use Churn\Event\BrokerImpl;
use Churn\Event\Event\AfterAnalysisEvent;
use Churn\Event\Event\BeforeAnalysisEvent;
use Churn\Event\HookLoader;
use Churn\File\FileFinder;
use Churn\File\FileHelper;
use Churn\Process\CacheProcessFactory;
use Churn\Process\ConcreteProcessFactory;
use Churn\Process\ProcessFactory;
use Churn\Process\ProcessHandlerFactory;
use Churn\Result\ResultAccumulator;
use Churn\Result\ResultsRendererFactory;
use InvalidArgumentException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\StreamOutput;
use Webmozart\Assert\Assert;

/**
 * @internal
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
final class RunCommand extends Command
{
    public const LOGO = "
    ___  _   _  __  __  ____  _  _     ____  _   _  ____
   / __)( )_( )(  )(  )(  _ \( \( )___(  _ \( )_( )(  _ \
  ( (__  ) _ (  )(__)(  )   / )  ((___))___/ ) _ (  )___/
   \___)(_) (_)(______)(_)\_)(_)\_)   (__)  (_) (_)(__)
";

    /**
     * The process handler factory.
     *
     * @var ProcessHandlerFactory
     */
    private $processHandlerFactory;

    /**
     * The renderer factory.
     *
     * @var ResultsRendererFactory
     */
    private $renderFactory;

    /**
     * Class constructor.
     *
     * @param ProcessHandlerFactory $processHandlerFactory The process handler factory.
     * @param ResultsRendererFactory $renderFactory The Results Renderer Factory.
     */
    public function __construct(ProcessHandlerFactory $processHandlerFactory, ResultsRendererFactory $renderFactory)
    {
        parent::__construct();

        $this->processHandlerFactory = $processHandlerFactory;
        $this->renderFactory = $renderFactory;
    }

    /**
     * Returns a new instance of the command.
     */
    public static function newInstance(): self
    {
        return new self(new ProcessHandlerFactory(), new ResultsRendererFactory());
    }

    /**
     * Configure the command
     */
    #[\Override]
    protected function configure(): void
    {
        $this->setName('run')
            ->addArgument('paths', InputArgument::IS_ARRAY, 'Path to source to check.')
            ->addOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to the configuration file', 'churn.yml') // @codingStandardsIgnoreLine
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format to use', 'text')
            ->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'The path where to write the result')
            ->addOption('parallel', null, InputOption::VALUE_REQUIRED, 'Number of parallel jobs')
            ->addOption('progress', 'p', InputOption::VALUE_NONE, 'Show progress bar')
            ->addOption('quiet', 'q', InputOption::VALUE_NONE, 'Suppress all normal output')
            ->setDescription('Check files')
            ->setHelp('Checks the churn on the provided path argument(s).');
    }

    /**
     * Execute the command.
     *
     * @param InputInterface $input Input.
     * @param OutputInterface $output Output.
     */
    #[\Override]
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        if (true === $input->getOption('quiet')) {
            $output = new NullOutput();
        }
        $this->printLogo($input, $output);
        $config = $this->getConfiguration($input, $output);
        $broker = new BrokerImpl();
        (new HookLoader($config->getDirPath()))->attachHooks($config->getHooks(), $broker);
        if (true === $input->getOption('progress')) {
            $broker->subscribe(new ProgressBarSubscriber($output));
        }
        $report = $this->analyze($input, $config, $broker);
        $this->writeResult($input, $output, $report);

        return (int) (new MaxScoreChecker($config->getMaxScoreThreshold()))->isOverThreshold($input, $output, $report);
    }

    /**
     * @param InputInterface $input Input.
     * @param OutputInterface $output Output.
     */
    private function getConfiguration(InputInterface $input, OutputInterface $output): Config
    {
        $isDefaultValue = !$input->hasParameterOption('--configuration') && !$input->hasParameterOption('-c');
        $config = Loader::fromPath((string) $input->getOption('configuration'), $isDefaultValue);
        if ([] !== $input->getArgument('paths')) {
            $config->setDirectoriesToScan((array) $input->getArgument('paths'));
        }

        $this->checkConfiguration($config, $output);

        if (null !== $input->getOption('parallel')) {
            Assert::integerish($input->getOption('parallel'), 'Amount of parallel jobs should be an integer');
            $config->setParallelJobs((int) $input->getOption('parallel'));
        }

        return $config;
    }

    /**
     * @param Config $config The configuration object.
     * @param OutputInterface $output Output.
     * @throws InvalidArgumentException If paths argument invalid.
     */
    private function checkConfiguration(Config $config, OutputInterface $output): void
    {
        $unrecognizedKeys = $config->getUnrecognizedKeys();
        if ([] !== $unrecognizedKeys) {
            $output = $output instanceof ConsoleOutputInterface
                ? $output->getErrorOutput()
                : $output;
            $output->writeln('<error>Unrecognized configuration keys: ' . \implode(', ', $unrecognizedKeys) . "</>\n");
        }

        if ([] === $config->getDirectoriesToScan()) {
            throw new InvalidArgumentException(
                'Provide the directories you want to scan as arguments, ' .
                'or configure them under "directoriesToScan" in your churn.yml file.'
            );
        }
    }

    /**
     * Run the actual analysis.
     *
     * @param InputInterface $input Input.
     * @param Config $config The configuration object.
     * @param Broker $broker The event broker.
     */
    private function analyze(InputInterface $input, Config $config, Broker $broker): ResultAccumulator
    {
        $broker->subscribe($report = new ResultAccumulator($config->getFilesToShow(), $config->getMinScoreToShow()));
        $broker->subscribe($processFactory = $this->getProcessFactory($config));
        $broker->notify(new BeforeAnalysisEvent());
        $basePath = $this->getBasePath($input, $config);
        $filesFinder = (new FileFinder($config->getFileExtensions(), $config->getFilesToIgnore(), $basePath))
            ->getPhpFiles($config->getDirectoriesToScan());
        $this->processHandlerFactory->getProcessHandler($config, $broker)->process($filesFinder, $processFactory);
        $broker->notify(new AfterAnalysisEvent($report));

        return $report;
    }

    /**
     * @param InputInterface $input Input.
     * @param Config $config The configuration object.
     * @return string The base path.
     */
    private function getBasePath(InputInterface $input, Config $config): string
    {
        return [] === $input->getArgument('paths')
            ? $config->getDirPath()
            : (string) \getcwd();
    }

    /**
     * @param Config $config The configuration object.
     */
    private function getProcessFactory(Config $config): ProcessFactory
    {
        $factory = new ConcreteProcessFactory($config->getVCS(), $config->getCommitsSince());

        $path = $config->getCachePath();
        if (null !== $path) {
            $basePath = $config->getDirPath();
            $factory = new CacheProcessFactory(FileHelper::toAbsolutePath($path, $basePath), $factory);
        }

        return $factory;
    }

    /**
     * @param InputInterface $input Input.
     * @param OutputInterface $output Output.
     */
    private function printLogo(InputInterface $input, OutputInterface $output): void
    {
        if ('text' !== $input->getOption('format') && '' === (string) $input->getOption('output')) {
            return;
        }

        $output->writeln(self::LOGO);
    }

    /**
     * @param InputInterface $input Input.
     * @param OutputInterface $output Output.
     * @param ResultAccumulator $report The report to write.
     */
    private function writeResult(InputInterface $input, OutputInterface $output, ResultAccumulator $report): void
    {
        if (true === $input->getOption('progress')) {
            $output->writeln("\n");
        }

        $outputPath = $input->getOption('output');
        if (\is_string($outputPath) && '' !== $outputPath) {
            $handler = \fopen($outputPath, 'w+');
            Assert::notFalse($handler, 'Cannot write into: ' . $outputPath);
            $output = new StreamOutput($handler, OutputInterface::VERBOSITY_NORMAL, false);
        }

        $renderer = $this->renderFactory->getRenderer($input->getOption('format'));
        $renderer->render($output, $report->toArray());
    }
}
