<?php

declare(strict_types=1);

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao\ManagerBundle\Command;

use Contao\ManagerBundle\Dotenv\DotenvDumper;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;

#[AsCommand(
    name: 'contao:setup',
    description: 'Sets up a Contao Managed Edition. This command will be run when executing the "contao-setup" binary.',
)]
class ContaoSetupCommand extends Command
{
    private readonly string $webDir;

    private readonly string $consolePath;

    private readonly string|false $phpPath;

    /**
     * @var \Closure(array<string>):Process
     */
    private readonly \Closure $createProcessHandler;

    /**
     * @param (\Closure(array<string>): Process)|null $createProcessHandler
     */
    public function __construct(
        private readonly string $projectDir,
        string $webDir,
        #[\SensitiveParameter] private readonly string|null $kernelSecret,
        \Closure|null $createProcessHandler = null,
    ) {
        $this->webDir = Path::makeRelative($webDir, $projectDir);
        $this->phpPath = (new PhpExecutableFinder())->find();
        $this->consolePath = Path::canonicalize(__DIR__.'/../../bin/contao-console');

        $this->createProcessHandler = $createProcessHandler ?? static fn (array $command) => new Process($command);

        parent::__construct();
    }

    protected function configure(): void
    {
        $this->setHidden(true);
    }

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

        // Generate a default .env file
        if (!$filesystem->exists($envPath = Path::join($this->projectDir, '.env'))) {
            $filesystem->dumpFile($envPath, "#COOKIE_ALLOW_LIST=PHPSESSID,csrf_https-contao_csrf_token,csrf_contao_csrf_token,trusted_device,REMEMBERME\n");

            $io->writeln('An empty <comment>.env</comment> file was created.');
        }

        // Generate a default .env.local file
        if (!$filesystem->exists($envLocalPath = Path::join($this->projectDir, '.env.local'))) {
            $filesystem->dumpFile(
                $envLocalPath,
                <<<'EOT'
                    #DATABASE_URL=mysql://username:password@localhost/database_name
                    #MAILER_DSN=smtp://username:password@smtp.example.com:587
                    EOT,
            );

            $io->writeln('An empty <comment>.env.local</comment> file was created.');
        }

        // Auto-generate a kernel secret if none was set
        if (!$this->kernelSecret || 'ThisTokenIsNotSoSecretChangeIt' === $this->kernelSecret) {
            $dotenv = new DotenvDumper($envLocalPath, $filesystem);
            $dotenv->setParameter('APP_SECRET', bin2hex(random_bytes(32)));
            $dotenv->dump();

            $io->info('An APP_SECRET was generated and written to your .env.local file.');
        }

        if (false === $this->phpPath) {
            throw new \RuntimeException('The php executable could not be found.');
        }

        $php = [
            $this->phpPath,
            '-dmemory_limit='.\ini_get('memory_limit'),
        ];

        if (OutputInterface::VERBOSITY_DEBUG === $output->getVerbosity()) {
            $php[] = '-ddisplay_errors=-1';
            $php[] = '-ddisplay_startup_errors=-1';
        }

        $commands = [
            ['skeleton:install', $this->webDir, '--env=prod'],
            ['assets:install', $this->webDir, '--symlink', '--relative', '--env=prod'],
            ['contao:install', $this->webDir, '--env=prod'],
            ['contao:symlinks', $this->webDir, '--env=prod'],
            ['cache:clear', '--no-warmup', '--env=prod'],
            ['cache:clear', '--no-warmup', '--env=dev'],
            ['cache:warmup', '--env=prod'],
            ['cmsig:seal:index-create', '--env=prod'],
        ];

        $commandFlags = array_filter([
            $output->isDecorated() ? '--ansi' : '--no-ansi',
            $this->getVerbosityFlag($output),
        ]);

        foreach ($commands as $command) {
            $this->executeCommand([...$php, $this->consolePath, ...$command, ...$commandFlags], $output);
        }

        $io->info('Done! Please run the contao:migrate command to make sure the database is up-to-date.');

        return Command::SUCCESS;
    }

    /**
     * Executes a console command in its own process and streams the output.
     */
    private function executeCommand(array $command, OutputInterface $output): void
    {
        $process = ($this->createProcessHandler)($command);

        // Increase the timeout according to contao/manager-bundle (see #54)
        $process->setTimeout(500);

        $process->run(
            static function (string $type, string $buffer) use ($output): void {
                $output->write($buffer);
            },
        );

        if (!$process->isSuccessful()) {
            throw new \RuntimeException(\sprintf('An error occurred while executing the "%s" command: %s', implode(' ', $command), $process->getErrorOutput()));
        }
    }

    private function getVerbosityFlag(OutputInterface $output): string
    {
        return match ($output->getVerbosity()) {
            OutputInterface::VERBOSITY_DEBUG => '-vvv',
            OutputInterface::VERBOSITY_VERY_VERBOSE => '-vv',
            OutputInterface::VERBOSITY_VERBOSE => '-v',
            default => '',
        };
    }
}
