<?php

declare(strict_types=1);

namespace Mautic\InstallBundle\Command;

use Doctrine\Persistence\ManagerRegistry;
use Mautic\InstallBundle\Configurator\Step\CheckStep;
use Mautic\InstallBundle\Configurator\Step\DoctrineStep;
use Mautic\InstallBundle\Install\InstallService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
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\Component\Console\Question\ConfirmationQuestion;

/**
 * CLI Command to install Mautic.
 */
#[AsCommand(
    name: InstallCommand::COMMAND,
    description: 'Installs Mautic'
)]
class InstallCommand extends Command
{
    public const COMMAND = 'mautic:install';

    public function __construct(
        private InstallService $installer,
        private ManagerRegistry $doctrineRegistry,
    ) {
        parent::__construct();
    }

    /**
     * Note: in every option (addOption()), please leave the default value empty to prevent problems with values from local.php being overwritten.
     */
    protected function configure()
    {
        $this
            ->setName(self::COMMAND)
            ->setHelp('This command allows you to trigger the install process. It will try to get configuration values both from the local config file and command line options/arguments, where the latter takes precedence.')
            ->addArgument(
                'site_url',
                InputArgument::REQUIRED,
                'Site URL.',
                null
            )
            ->addArgument(
                'step',
                InputArgument::OPTIONAL,
                'Install process start index. 0 for requirements check, 1 for database, 2 for admin, 3 for configuration, 4 for final step. Each successful step will trigger the next until completion.',
                '0'
            )
            ->addOption(
                '--force',
                '-f',
                InputOption::VALUE_NONE,
                'Do not ask confirmation if recommendations triggered.',
                null
            )
            ->addOption(
                '--db_driver',
                null,
                InputOption::VALUE_REQUIRED,
                'Database driver.',
                null
            )
            ->addOption(
                '--db_host',
                null,
                InputOption::VALUE_REQUIRED,
                'Database host.',
                null
            )
            ->addOption(
                '--db_port',
                null,
                InputOption::VALUE_REQUIRED,
                'Database port.',
                null
            )
            ->addOption(
                '--db_name',
                null,
                InputOption::VALUE_REQUIRED,
                'Database name.',
                null
            )
            ->addOption(
                '--db_user',
                null,
                InputOption::VALUE_REQUIRED,
                'Database user.',
                null
            )
            ->addOption(
                '--db_password',
                null,
                InputOption::VALUE_REQUIRED,
                'Database password.',
                null
            )
            ->addOption(
                '--db_table_prefix',
                null,
                InputOption::VALUE_REQUIRED,
                'Database tables prefix.',
                null
            )
            ->addOption(
                '--db_backup_tables',
                null,
                InputOption::VALUE_REQUIRED,
                'Backup database tables if they exist; otherwise drop them. (true|false)',
                null
            )
            ->addOption(
                '--db_backup_prefix',
                null,
                InputOption::VALUE_REQUIRED,
                'Database backup tables prefix.',
                null
            )
            ->addOption(
                '--admin_firstname',
                null,
                InputOption::VALUE_REQUIRED,
                'Admin first name.',
                null
            )
            ->addOption(
                '--admin_lastname',
                null,
                InputOption::VALUE_REQUIRED,
                'Admin last name.',
                null
            )
            ->addOption(
                '--admin_username',
                null,
                InputOption::VALUE_REQUIRED,
                'Admin username.',
                null
            )
            ->addOption(
                '--admin_email',
                null,
                InputOption::VALUE_REQUIRED,
                'Admin email.',
                null
            )
            ->addOption(
                '--admin_password',
                null,
                InputOption::VALUE_REQUIRED,
                'Admin user.',
                null
            );

        parent::configure();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // Check Mautic is not already installed
        if ($this->installer->checkIfInstalled()) {
            $output->writeln('Mautic already installed');

            return Command::SUCCESS;
        }

        $output->writeln([
            'Mautic Install',
            '==============',
            '',
        ]);

        if (!defined('IS_PHPUNIT')) {
            // Prevents querying of database tables that do not exist during the installation process
            define('MAUTIC_INSTALLER', 1);
        }

        // Build objects to pass to the install service from local.php or command line options
        $output->writeln('Parsing options and arguments...');
        $options = $input->getOptions();

        // Convert boolean options to actual booleans.
        $options['db_backup_tables'] = (bool) filter_var($options['db_backup_tables'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);

        /**
         * We need to have some default database parameters, as it could be the case that the
         * user didn't set them both in local.php and the command line options.
         */
        $dbParams = [
            'driver'        => 'pdo_mysql',
            'host'          => null,
            'port'          => null,
            'name'          => null,
            'user'          => null,
            'password'      => null,
            'table_prefix'  => null,
            'backup_tables' => true,
            'backup_prefix' => 'bak_',
        ];
        $adminParam = [
            'firstname' => 'Admin',
            'lastname'  => 'Mautic',
            'username'  => 'admin',
        ];
        $allParams = $this->installer->localConfigParameters();

        // Initialize DB and admin params from local.php
        foreach ((array) $allParams as $opt => $value) {
            if (str_starts_with($opt, 'db_')) {
                $dbParams[substr($opt, 3)] = $value;
            } elseif (str_starts_with($opt, 'admin_')) {
                $adminParam[substr($opt, 6)] = $value;
            }
        }

        // Initialize DB and admin params from cli options
        foreach ($options as $opt => $value) {
            if (isset($value)) {
                if (str_starts_with($opt, 'db_')) {
                    $dbParams[substr($opt, 3)] = $value;
                    $allParams[$opt]           = $value;
                } elseif (str_starts_with($opt, 'admin_')) {
                    $adminParam[substr($opt, 6)] = $value;
                }
            }
        }

        if (!empty($allParams['site_url'])) {
            $siteUrl = $allParams['site_url'];
        } else {
            $siteUrl               = $input->getArgument('site_url');
            $allParams['site_url'] = $siteUrl;
        }

        $step = (float) $input->getArgument('step');

        switch ($step) {
            default:
            case InstallService::CHECK_STEP:
                $output->writeln($step.' - Checking installation requirements...');
                $messages = $this->stepAction($this->installer, ['site_url' => $siteUrl], $step);
                if (!empty($messages)) {
                    if (isset($messages['requirements']) && !empty($messages['requirements'])) {
                        // Stop install if requirements not met
                        $output->writeln('Missing requirements:');
                        $this->handleInstallerErrors($output, $messages['requirements']);
                        $output->writeln('Install canceled');

                        return (int) -$step;
                    } elseif (isset($messages['optional']) && !empty($messages['optional'])) {
                        $output->writeln('Missing optional settings:');
                        $this->handleInstallerErrors($output, $messages['optional']);

                        if (empty($options['force'])) {
                            // Ask user to confirm install when optional settings missing

                            /** @var QuestionHelper $helper */
                            $helper   = $this->getHelper('question');
                            $question = new ConfirmationQuestion('Continue with install anyway? [yes/no]', false);

                            if (!$helper->ask($input, $output, $question)) {
                                return (int) -$step;
                            }
                        }
                    }
                }
                $output->writeln('Ready to Install!');
                // Keep on with next step
                $step = InstallService::DOCTRINE_STEP;

                // no break
            case InstallService::DOCTRINE_STEP:
                $output->writeln($step.' - Creating database...');

                /**
                 * This is needed for installations with database prefixes to work correctly.
                 */
                $connectionWrapper = $this->doctrineRegistry->getConnection();
                $connectionWrapper->initConnection($dbParams);

                $messages = $this->stepAction($this->installer, $dbParams, $step);
                if (!empty($messages)) {
                    $output->writeln('Errors in database configuration/installation:');
                    $this->handleInstallerErrors($output, $messages);

                    $output->writeln('Install canceled');

                    return (int) -$step;
                }

                $step = InstallService::DOCTRINE_STEP + .1;
                $output->writeln($step.' - Creating schema...');
                $messages = $this->stepAction($this->installer, $dbParams, $step);
                if (!empty($messages)) {
                    $output->writeln('Errors in schema configuration/installation:');
                    $this->handleInstallerErrors($output, $messages);

                    $output->writeln('Install canceled');

                    return -InstallService::DOCTRINE_STEP;
                }

                $step = InstallService::DOCTRINE_STEP + .2;
                $output->writeln($step.' - Loading fixtures...');
                $messages = $this->stepAction($this->installer, $dbParams, $step);
                if (!empty($messages)) {
                    $output->writeln('Errors in fixtures configuration/installation:');
                    $this->handleInstallerErrors($output, $messages);

                    $output->writeln('Install canceled');

                    return -InstallService::DOCTRINE_STEP;
                }

                // Keep on with next step
                $step = InstallService::USER_STEP;

                // no break
            case InstallService::USER_STEP:
                $output->writeln($step.' - Creating admin user...');
                $messages = $this->stepAction($this->installer, $adminParam, $step);
                if (!empty($messages)) {
                    $output->writeln('Errors in admin user configuration/installation:');
                    $this->handleInstallerErrors($output, $messages);

                    $output->writeln('Install canceled');

                    return (int) -$step;
                }
                // Keep on with next step
                $step = InstallService::FINAL_STEP;

                // no break
            case InstallService::FINAL_STEP:
                $output->writeln($step.' - Final steps...');
                $messages = $this->stepAction($this->installer, $allParams, $step);
                if (!empty($messages)) {
                    $output->writeln('Errors in final step:');
                    $this->handleInstallerErrors($output, $messages);

                    $output->writeln('Install canceled');

                    return (int) -$step;
                }
        }

        $output->writeln([
            '',
            '================',
            'Install complete',
            '================',
        ]);

        return Command::SUCCESS;
    }

    /**
     * Controller action for install steps.
     *
     * @param InstallService $installer The install process
     * @param array          $params    The install parameters
     * @param float          $index     The step number to process
     *
     * @throws \Exception
     */
    protected function stepAction(InstallService $installer, array $params, float $index = 0): array
    {
        if ($index - floor($index) > 0) {
            $subIndex = (int) (round($index - floor($index), 1) * 10);
            $index    = floor($index);
        }
        $index = (int) $index;

        $messages = [];

        switch ($index) {
            case InstallService::CHECK_STEP:
                // Check installation requirements
                $step = $installer->getStep($index);
                if ($step instanceof CheckStep) {
                    // Set all step fields based on parameters
                    $step->site_url = $params['site_url'];
                }

                $messages['requirements'] = $installer->checkRequirements($step);
                $messages['optional']     = $installer->checkOptionalSettings($step);
                break;

            case InstallService::DOCTRINE_STEP:
                $step = $installer->getStep($index);
                if ($step instanceof DoctrineStep) {
                    // Set all step fields based on parameters
                    foreach ($step as $key => $value) {
                        if (isset($params[$key])) {
                            $step->$key = $params[$key];
                        }
                    }
                }

                if (!isset($subIndex)) {
                    // Install database
                    $messages = $installer->createDatabaseStep($step, $params);

                    break;
                }

                switch ($subIndex) {
                    case 1:
                        // Install schema
                        $messages = $installer->createSchemaStep($params);
                        break;

                    case 2:
                        // Install fixtures
                        $messages = $installer->createFixturesStep();
                        break;
                }
                break;

            case InstallService::USER_STEP:
                // Create admin user
                $messages = $installer->createAdminUserStep($params);
                break;

            case InstallService::FINAL_STEP:
                // Save final configuration
                $siteUrl  = $params['site_url'];
                $messages = $installer->createFinalConfigStep($siteUrl);
                if (empty($messages)) {
                    $installer->finalMigrationStep();
                }
                break;
        }

        return $messages;
    }

    /**
     * Handle install command errors.
     *
     * @param array<string,string> $messages
     */
    private function handleInstallerErrors(OutputInterface $output, array $messages): void
    {
        foreach ($messages as $type => $message) {
            $output->writeln("  - [$type] $message");
        }
    }
}
