<?php

declare(strict_types=1);

namespace NoahVet\Reef\Command\Database;

use NoahVet\Reef\Hasher\DirectoryHasherInterface;
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\Filesystem\Filesystem;
use Symfony\Component\Process\Process;

#[AsCommand(
    name: 'reef:database:reset-fast',
    description: 'Load fixtures using a SQL cache if available.',
)]
class ResetFastDataBaseCommand extends Command implements ResetFastDataBaseCommandInterface
{
    public function __construct(
        protected readonly string $databaseUrl,
        protected readonly string $environment,
        protected readonly string $kernelProjectDir,
        protected readonly DirectoryHasherInterface $directoryHasher,
    ) {
        parent::__construct();
    }

    public function __invoke(
        InputInterface $input,
        SymfonyStyle $io,
        OutputInterface $output,
    ): int {
        /** @var string[] $groups */
        $groups = $input->getOption('group') ?? [];

        // Compute all hashes in a single place (fixtures + migrations + sync + commands + global).
        $hashes = $this->directoryHasher->computeHashes($groups);

        $fixturesHash = $hashes['fixtures'];
        $fixtureCommandsHash = $hashes['fixtureCommands'];
        $migrationsHash = $hashes['migrations'];
        $syncHash = $hashes['sync'];
        $syncCommandsHash = $hashes['syncCommands'];
        $hash = $hashes['global'];

        // Shared cache for all Symfony environments.
        $cacheDir = $this->kernelProjectDir.'/var/cache/fixtures';
        $dumpPath = $cacheDir.'/'.$hash.'.sql';

        $io->writeln(\sprintf('Symfony env: <info>%s</info>', $this->environment));
        $io->writeln(\sprintf(
            'Fixture contexts (directories): <info>%s</info>',
            $groups ? \implode(', ', $groups) : '(all)',
        ));
        $io->writeln(\sprintf('Fixtures hash: <info>%s</info>', $fixturesHash));
        $io->writeln(\sprintf('Fixtures commands hash: <info>%s</info>', $fixtureCommandsHash));
        $io->writeln(\sprintf('Migrations hash: <info>%s</info>', $migrationsHash));
        $io->writeln(\sprintf('Sync data hash: <info>%s</info>', $syncHash));
        $io->writeln(\sprintf('Sync commands hash: <info>%s</info>', $syncCommandsHash));
        $io->writeln(\sprintf('Cache hash: <info>%s</info>', $hash));

        (new Filesystem())->mkdir($cacheDir);

        if (\is_file($dumpPath)) {
            $io->section('SQL dump found, importing...');

            if (Command::FAILURE === $this->importSqlDump($dumpPath, $io)) {
                return Command::FAILURE;
            }

            $io->success('Database restored from SQL cache.');

            return Command::SUCCESS;
        }

        $io->section('No SQL cache..., running migrations + sync + fixtures...');

        if (Command::FAILURE === $this->runDoctrineMigrations($io)) {
            return Command::FAILURE;
        }

        if (Command::FAILURE === $this->runAppSyncAll($io)) {
            return Command::FAILURE;
        }

        if (Command::FAILURE === $this->loadDoctrineFixtures($io, $groups)) {
            return Command::FAILURE;
        }

        $io->section('Exporting database to SQL cache...');

        if (Command::FAILURE === $this->exportDatabaseToSql($dumpPath, $io)) {
            return Command::FAILURE;
        }

        $io->success('Fixtures loaded and SQL dump generated.');

        return Command::SUCCESS;
    }

    /**
     * In symfony 6.4, to execute should be overridden.
     *
     * Should be removed in symfony 7.4, unnecessary
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        return self::__invoke($input, new SymfonyStyle($input, $output), $output);
    }

    protected function configure(): void
    {
        $this
            ->addOption(
                'group',
                'g',
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                'Fixture groups to load (and sub-directories to hash, e.g. pokemon, test)',
            )
        ;
    }

    private function runAppSyncAll(
        SymfonyStyle $io,
    ): int {
        $cmd = [
            \PHP_BINARY,
            'bin/console',
            'app:sync:all',
            '--env='.$this->environment,
        ];

        try {
            $process = new Process(
                $cmd,
                $this->kernelProjectDir,
                [
                    // Ensure APP_ENV is correctly propagated to the command.
                    'APP_ENV' => $this->environment,
                ],
            );

            $process->setTimeout(null);

            $process->run(
                static function (string $type, string $buffer) use ($io): void {
                    $io->write($buffer);
                },
            );
        } catch (\Throwable $exception) {
            $io->warning(
                \sprintf(
                    'Command "app:sync:all" could not be executed (maybe it does not exist). '
                    .'Skipping sync step. Reason: %s',
                    $exception->getMessage(),
                ),
            );

            // We silently skip sync if the command is not available.
            return Command::SUCCESS;
        }

        if (!$process->isSuccessful()) {
            $errorOutput = $process->getErrorOutput();

            // Typical Symfony message when the command is not registered.
            if (
                \str_contains($errorOutput, 'Command "app:sync:all"')
                && \str_contains($errorOutput, 'is not defined')
            ) {
                $io->warning(
                    'Command "app:sync:all" is not defined. Skipping sync step.',
                );

                return Command::SUCCESS;
            }

            $io->error("Command app:sync:all failed:\n".$errorOutput);

            return Command::FAILURE;
        }

        return Command::SUCCESS;
    }

    private function runDoctrineMigrations(SymfonyStyle $io): int
    {
        $cmd = [\PHP_BINARY, 'bin/console', 'doctrine:migrations:migrate', '-n'];

        $process = new Process($cmd, $this->kernelProjectDir);
        $process->setTimeout(null);
        $process->run(static fn (string $type, string $buffer) => $io->write($buffer));

        if (!$process->isSuccessful()) {
            $io->error("Doctrine migrations failed:\n".$process->getErrorOutput());

            return Command::FAILURE;
        }

        return Command::SUCCESS;
    }

    /**
     * @param string[] $groups
     */
    private function loadDoctrineFixtures(SymfonyStyle $io, array $groups): int
    {
        $cmd = [\PHP_BINARY, 'bin/console', 'doctrine:fixtures:load', '-n', '--append'];

        if ([] !== $groups) {
            $cmd[] = '--group';
            $cmd = [...$cmd, ...$groups];
        }

        $process = new Process($cmd, $this->kernelProjectDir);
        $process->setTimeout(null);
        $process->run(static fn (string $type, string $buffer) => $io->write($buffer));

        if (!$process->isSuccessful()) {
            $io->error("Fixtures failed:\n".$process->getErrorOutput());

            return Command::FAILURE;
        }

        return Command::SUCCESS;
    }

    private function importSqlDump(string $dumpPath, SymfonyStyle $io): int
    {
        $io->writeln(\sprintf('Importing dump <comment>%s</comment>', $dumpPath));

        $config = $this->getDatabaseConfigFromUrl();

        $process = new Process([
            'mysql',
            '--host='.$config['host'],
            '--port='.$config['port'],
            '--user='.$config['user'],
            '--password='.$config['password'],
            $config['dbname'],
        ]);

        $contents = \file_get_contents($dumpPath);
        if (false === $contents) {
            $io->error("Cannot read dump file: $dumpPath");

            return Command::FAILURE;
        }

        $process->setInput($contents);

        try {
            $process->mustRun();
        } catch (\Throwable $e) {
            $io->error('Error while importing SQL dump.');
            $io->error($e->getMessage());
            $io->error($process->getErrorOutput());

            return Command::FAILURE;
        }

        $io->writeln('Import finished.');

        return Command::SUCCESS;
    }

    private function exportDatabaseToSql(
        string $dumpPath,
        SymfonyStyle $io,
    ): int {
        $io->writeln(\sprintf('Generating SQL dump to <comment>%s</comment>', $dumpPath));

        $config = $this->getDatabaseConfigFromUrl();

        $process = new Process([
            'mariadb-dump',
            '-h',
            $config['host'],
            '-u',
            $config['user'],
            '-p'.$config['password'],
            '--single-transaction',
            '--routines',
            '--triggers',
            '--events',
            $config['dbname'],
        ]);

        $process->mustRun();

        if (!$process->isSuccessful()) {
            $io->error('Error while generating SQL dump.');
            $io->error($process->getErrorOutput());

            return Command::FAILURE;
        }

        \file_put_contents(
            $dumpPath,
            $process->getOutput(),
        );

        $io->writeln('SQL dump generated.');

        return Command::SUCCESS;
    }

    /**
     * @return array{
     *     host: string,
     *     port: string,
     *     user: string,
     *     password: string,
     *     dbname: string
     * }
     */
    private function getDatabaseConfigFromUrl(): array
    {
        // We know the DatabaseUrl pattern in .env, symfony standard
        /** @var array{
         *     fragment?: string,
         *     host?: string,
         *     pass?: string,
         *     path?: string,
         *     port?: int,
         *     query?: string,
         *     scheme?: string,
         *     user?: string
         * }|false $parts
         */
        $parts = \parse_url($this->databaseUrl);

        if (false === $parts) {
            throw new \RuntimeException('Invalid DATABASE_URL: parse_url() returned false.');
        }

        $host = ($parts['host'] ?? '127.0.0.1');
        $port = isset($parts['port']) ? (string) $parts['port'] : '3306';
        $user = ($parts['user'] ?? 'root');
        $password = ($parts['pass'] ?? 'root');
        $path = ($parts['path'] ?? '');
        $dbname = \ltrim($path, '/') ?: 'cam';

        return [
            'host' => $host,
            'port' => $port,
            'user' => $user,
            'password' => $password,
            'dbname' => $dbname,
        ];
    }
}
