<?php

/**
 * @package     Dotclear
 *
 * @copyright   Olivier Meunier & Association Dotclear
 * @copyright   AGPL-3.0
 */
declare(strict_types=1);

namespace Dotclear\Process\Install;

use Dotclear\App;
use Dotclear\Exception\ProcessException;
use Dotclear\Helper\File\Files;
use Dotclear\Helper\File\Path;
use Dotclear\Helper\Process\TraitProcess;
use Dotclear\Helper\Text;
use Dotclear\Schema\Schema;
use Exception;

/**
 * @brief   CLI install process.
 *
 * There are additonnals queries
 * as some server http var are not availables in CLI mode.
 *
 * Usage from Dotclear root, use shell: php admin/install/index.php --options...
 * * To list available options, use -h option
 * * To use non-interactive script, use -n option
 *
 * @since   2.36
 */
class Cli
{
    use TraitProcess;

    /**
     * @var     array<string, string>   $options
     */
    private static array $options = [];

    private static bool $interactive = false;

    public static function init(): bool
    {
        if (!self::status(App::task()->checkContext('INSTALL') && App::config()->cliMode())) {
            throw new ProcessException('Application is not in CLI mode', 550);
        }

        return self::status();
    }

    /**
     * @return  array<string, string>
     */
    public static function getArguments(): array
    {
        return [
            'dbdriver'   => __('The database driver'),
            'dbhost'     => __('The database host'),
            'dbname'     => __('The database name'),
            'dbuser'     => __('The database user'),
            'dbpassword' => __('The database password'),
            'dbprefix'   => __('The database table prefix, can be empty for deault to _dc'),
            'adminemail' => __('The administration mail from'),
            'ufirstname' => __('The super administrator first name, can be empty'),
            'ulastname'  => __('The super administrator last name, can be empty'),
            'uemail'     => __('The super administrator email'),
            'ulogin'     => __('The super administrator login'),
            'upassword'  => __('The super administrator password'),
            'adminurl'   => __('The admin dashboard URL'),
            'blogurl'    => __('The default blog URL'),
        ];
    }

    public static function render(): void
    {
        if (!self::status()) {
            return;
        }

        // Need to have output buffer for interactive commands
        while (ob_get_level() > 0) {
            ob_end_clean();
        }

        // Get options from arguments
        $options = getopt('h::n::', array_map(fn (string $v): string => $v . '::', array_keys(self::getArguments())));
        if (is_array($options)) {
            self::$options = array_map(fn ($v): string => is_string($v) ? $v : '', $options);
        }

        // Set (non)intercative mode
        self::$interactive = !isset($options['n']) || $options['n'] !== false;

        // Show help
        if (isset($options['h']) && $options['h'] === false) {
            echo __('Command line options are:') . "\n" .
                '-h => ' . __('This help') . "\n" .
                '-n => ' . __('Disable interactive mode') . "\n" .
                implode('', array_map(fn ($k, $v): string => '--' . $k . ' => ' . $v . "\n", array_keys(self::getArguments()), array_values(self::getArguments()))) . "\n";
        }

        if (!App::config()->hasConfig()) {
            // First step
            self::msgLine(__('Starting first step of Dotclear installation process.'));

            // Parse configuration
            $dbdriver = self::parseDbDriver();
            if (str_contains($dbdriver, 'sqlite')) {
                $dbhost     = '';
                $dbuser     = '';
                $dbpassword = '';
                $dbname     = self::parseDbPath();
            } else {
                $dbhost     = self::parseDbHost();
                $dbname     = self::parseDbName();
                $dbuser     = self::parseDbUser();
                $dbpassword = self::parseDbPassword();
            }

            $dbprefix   = self::parseDbPrefix();
            $adminemail = self::parseAdminEmail();
            $adminurl   = self::parseAdminUrl();

            // Check configuration

            // Try to connect to database
            $con = App::db()->newCon($dbdriver, $dbhost, $dbname, $dbuser, $dbpassword);

            // Check system capabilites
            $_e = [];
            if (!App::install()->utils()->check($con, $_e)) {
                throw new Exception(implode(', ', $_e));
            }

            // Check if dotclear is already installed
            if (in_array($dbprefix . 'version', $con->schema()->getTables())) {
                throw new Exception(__('Dotclear is already installed.'));
            }

            // Does config.php.in exist?
            $config_in = App::config()->dotclearRoot() . '/inc/config.php.in';
            if (!is_file($config_in)) {
                throw new Exception(sprintf(__('File %s does not exist.'), $config_in));
            }

            // Can we write config.php
            if (!is_writable(dirname(App::config()->configPath()))) {
                throw new Exception(sprintf(__('Cannot write %s file.'), App::config()->configPath()));
            }

            // Write config file
            $full_conf = (string) file_get_contents($config_in);

            $full_conf = self::writeConfigValue('DC_DBDRIVER', $dbdriver, $full_conf);
            $full_conf = self::writeConfigValue('DC_DBHOST', $dbhost, $full_conf);
            $full_conf = self::writeConfigValue('DC_DBUSER', $dbuser, $full_conf);
            $full_conf = self::writeConfigValue('DC_DBPASSWORD', $dbpassword, $full_conf);
            $full_conf = self::writeConfigValue('DC_DBNAME', $dbname, $full_conf);
            $full_conf = self::writeConfigValue('DC_DBPREFIX', $dbprefix, $full_conf);
            $full_conf = self::writeConfigValue('DC_ADMIN_URL', $adminurl, $full_conf);
            $full_conf = self::writeConfigValue('DC_ADMIN_MAILFROM', $adminemail, $full_conf);
            $full_conf = self::writeConfigValue('DC_MASTER_KEY', md5(uniqid()), $full_conf);

            # Fix path if config file has moved elsewhere and allow environment variables
            $full_conf    = self::writeConfigValue('DC_PLUGINS_ROOT', App::config()->dotclearRoot() . '/plugins', $full_conf);
            $plugins_root = isset($_SERVER['DC_PLUGINS_ROOT']) && is_string($plugins_root = $_SERVER['DC_PLUGINS_ROOT']) ? $plugins_root : '';
            if ($plugins_root !== '' && is_writable(dirname($plugins_root))) {
                $full_conf = self::writeConfigValue('DC_PLUGINS_ROOT', App::config()->dotclearRoot() . '/plugins' . PATH_SEPARATOR . $plugins_root, $full_conf);
            }

            $full_conf = self::writeConfigValue('DC_TPL_CACHE', App::config()->dotclearRoot() . '/cache', $full_conf);
            $tpl_cache = isset($_SERVER['DC_TPL_CACHE']) && is_string($tpl_cache = $_SERVER['DC_TPL_CACHE']) ? $tpl_cache : '';
            if ($tpl_cache !== '' && is_writable(dirname($tpl_cache))) {
                $full_conf = self::writeConfigValue('DC_TPL_CACHE', $tpl_cache, $full_conf);
            }

            $full_conf = self::writeConfigValue('DC_VAR', App::config()->dotclearRoot() . '/var', $full_conf);
            $dc_var    = isset($_SERVER['DC_VAR']) && is_string($dc_var = $_SERVER['DC_VAR']) ? $dc_var : '';
            if ($dc_var !== '' && is_writable(dirname($dc_var))) {
                $full_conf = self::writeConfigValue('DC_VAR', $dc_var, $full_conf);
            }

            $fp = @fopen(App::config()->configPath(), 'wb');
            if ($fp === false) {
                throw new Exception(sprintf(__('Cannot write %s file.'), App::config()->configPath()));
            }

            fwrite($fp, $full_conf);
            fclose($fp);

            if (function_exists('chmod')) {
                try {
                    @chmod(App::config()->configPath(), 0o666);
                } catch (Exception) {
                }
            }

            $con->close();

            // Success message
            self::okLine(__('First step of Dotclear installation succeed.'));
            self::msgLine(__('Re-run same script to process second step of Dotclear installation.'));
        } else {
            // Second step
            self::msgLine(__('Starting second step of Dotclear installation process.'));

            // Check current installation state
            if (App::config()->masterKey() === '') {
                throw new Exception(__('Please set a master key (DC_MASTER_KEY) in configuration file.'));
            }

            // Check if dotclear is already installed
            if (in_array(App::db()->con()->prefix() . App::blog()::POST_TABLE_NAME, App::db()->con()->schema()->getTables())) {
                throw new Exception(__('Dotclear is already installed.'));
            }

            // Check system capabilites
            $_e = [];
            if (!App::install()->utils()->check(App::db()->con(), $_e)) {
                throw new Exception(implode(', ', $_e));
            }

            // Parse configuration
            $ufirstname = self::parseUserFirstname();
            $ulastname  = self::parseUserLastname();
            $uemail     = self::parseUserEmail();
            $ulogin     = self::parseUserLogin();
            $upassword  = self::parseUserPassword();
            $ulang      = 'en';
            $utz        = 'Europe/London';
            $blogurl    = self::parseBlogUrl();

            self::dot(__('Processing installation'));

            // Create schema
            $_s = App::db()->structure();
            self::dot();

            // Fill database structure
            Schema::fillStructure($_s);
            self::dot();

            // Update database
            App::db()->structure()->synchronize($_s);
            self::dot();

            // Create user
            $cur = App::db()->con()->openCursor(App::db()->con()->prefix() . App::auth()::USER_TABLE_NAME);
            $cur->setStrField('user_id', $ulogin);
            $cur->setBoolField('user_super', 1);
            $cur->setStrField('user_pwd', App::auth()->crypt($upassword));
            $cur->setStrField('user_name', $ulastname);
            $cur->setStrField('user_firstname', $ufirstname);
            $cur->setStrField('user_email', $uemail);
            $cur->setStrField('user_lang', $ulang);
            $cur->setStrField('user_tz', $utz);
            $cur->setStrField('user_creadt', date('Y-m-d H:i:s'));
            $cur->setStrField('user_upddt', date('Y-m-d H:i:s'));
            $cur->setStrField('user_options', serialize(App::users()->userDefaults()));
            $cur->insert();
            self::dot();

            App::auth()->checkUser($ulogin);
            self::dot();

            // Create blog
            $cur = App::blog()->openBlogCursor();
            $cur->setStrField('blog_id', 'default');
            $cur->setStrField('blog_url', $blogurl . '/index.php?');
            $cur->setStrField('blog_name', __('My first blog'));
            App::blogs()->addBlog($cur);
            self::dot();

            // Create global blog settings
            App::blogs()->blogDefaults();
            self::dot();

            $blog_settings = App::blogSettings()->createFromBlog('default');
            $blog_settings->get('system')->put('blog_timezone', $utz);
            self::dot();
            $blog_settings->get('system')->put('lang', $ulang);
            self::dot();
            $blog_settings->get('system')->put('public_url', $blogurl . '/public');
            self::dot();
            $blog_settings->get('system')->put('themes_url', $blogurl . '/themes');
            self::dot();

            // date and time formats
            $formatDate   = __('%A, %B %e %Y');
            $date_formats = ['%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y', '%Y/%m/%d', '%d.%m.%Y', '%b %e %Y', '%e %b %Y', '%Y %b %e',
                '%a, %Y-%m-%d', '%a, %m/%d/%Y', '%a, %d/%m/%Y', '%a, %Y/%m/%d', '%B %e, %Y', '%e %B, %Y', '%Y, %B %e', '%e. %B %Y',
                '%A, %B %e, %Y', '%A, %e %B, %Y', '%A, %Y, %B %e', '%A, %Y, %B %e', '%A, %e. %B %Y', ];
            $time_formats = ['%H:%M', '%I:%M', '%l:%M', '%Hh%M', '%Ih%M', '%lh%M'];
            if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
                $formatDate   = preg_replace('#(?<!%)((?:%%)*)%e#', '\1%#d', $formatDate);
                $date_formats = array_map(
                    fn (string $f): string => str_replace('%e', '%#d', $f),
                    $date_formats
                );
            }

            $blog_settings->get('system')->put('date_format', $formatDate);
            self::dot();
            $blog_settings->get('system')->put('date_formats', $date_formats, App::blogWorkspace()::NS_ARRAY, 'Date formats examples', true, true);
            self::dot();
            $blog_settings->get('system')->put('time_formats', $time_formats, App::blogWorkspace()::NS_ARRAY, 'Time formats examples', true, true);
            self::dot();

            # Add repository URL for themes and plugins
            $blog_settings->get('system')->put('store_plugin_url', App::config()->release('store_plugin_url'), App::blogWorkspace()::NS_STRING, 'Plugins XML feed location', true, true);
            self::dot();
            $blog_settings->get('system')->put('store_theme_url', App::config()->release('store_theme_url'), App::blogWorkspace()::NS_STRING, 'Themes XML feed location', true, true);
            self::dot();

            // CSP directive (admin part)

            /* SQlite driver does not allow using single quote at beginning or end of a field value
            so we have to use neutral values (localhost and 127.0.0.1) for some CSP directives
             */
            $csp_prefix = str_contains(App::db()->con()->driver(), 'sqlite') ? 'localhost ' : ''; // Hack for SQlite syntax
            $csp_suffix = str_contains(App::db()->con()->driver(), 'sqlite') ? ' 127.0.0.1' : ''; // Hack for SQlite syntax

            $blog_settings->get('system')->put('csp_admin_on', true, App::blogWorkspace()::NS_BOOL, 'Send CSP header (admin)', true, true);
            self::dot();
            $blog_settings->get('system')->put('csp_admin_report_only', false, App::blogWorkspace()::NS_BOOL, 'CSP Report only violations (admin)', true, true);
            self::dot();
            $blog_settings->get('system')->put(
                'csp_admin_default',
                $csp_prefix . "'self'" . $csp_suffix,
                App::blogWorkspace()::NS_STRING,
                'CSP default-src directive',
                true,
                true
            );
            self::dot();
            $blog_settings->get('system')->put(
                'csp_admin_script',
                $csp_prefix . "'self' 'unsafe-eval'" . $csp_suffix,
                App::blogWorkspace()::NS_STRING,
                'CSP script-src directive',
                true,
                true
            );
            self::dot();
            $blog_settings->get('system')->put(
                'csp_admin_style',
                $csp_prefix . "'self' 'unsafe-inline'" . $csp_suffix,
                App::blogWorkspace()::NS_STRING,
                'CSP style-src directive',
                true,
                true
            );
            self::dot();
            $blog_settings->get('system')->put(
                'csp_admin_img',
                $csp_prefix . "'self' data: https://dotclear.org blob:",
                App::blogWorkspace()::NS_STRING,
                'CSP img-src directive',
                true,
                true
            );
            self::dot();

            // Add Dotclear version
            $cur = App::version()->openVersionCursor();
            $cur->setStrField('module', 'core');
            $cur->setStrField('version', App::config()->dotclearVersion());
            $cur->insert();
            self::dot();

            // Create first post
            App::blog()->loadFromBlog('default');
            self::dot();

            $cur = App::blog()->openPostCursor();
            $cur->setStrField('user_id', $ulogin);
            $cur->setStrField('post_format', 'xhtml');
            $cur->setStrField('post_lang', $ulang);
            $cur->setStrField('post_title', __('Welcome to Dotclear!'));
            $cur->setStrField('post_content', '<p>' . __('This is your first entry. When you\'re ready to blog, log in to edit or delete it.') . '</p>');
            $cur->setStrField('post_content_xhtml', $cur->strField('post_content'));
            $cur->setIntField('post_status', App::status()->post()::PUBLISHED);
            $cur->setBoolField('post_open_comment', true);
            $cur->setBoolField('post_open_tb', false);
            $post_id = App::blog()->addPost($cur);
            self::dot();

            // Add a comment to it
            $cur = App::blog()->openCommentCursor();
            $cur->setIntField('post_id', $post_id);
            $cur->setStrField('comment_tz', $utz);
            $cur->setStrField('comment_author', __('Dotclear Team'));
            $cur->setStrField('comment_email', 'contact@dotclear.org');
            $cur->setStrField('comment_site', 'https://dotclear.org/');
            $cur->setStrField('comment_content', __("<p>This is a comment.</p>\n<p>To delete it, log in and view your blog's comments. Then you might remove or edit it.</p>"));
            App::blog()->addComment($cur);
            self::dot();

            // Plugins initialization
            App::task()->addContext('BACKEND');
            App::plugins()->loadModules(App::config()->pluginsRoot());
            $plugins_install = App::plugins()->installModules();
            self::dot();

            // Add dashboard module options
            App::auth()->prefs()->get('dashboard')->put('doclinks', true, App::userWorkspace()::WS_BOOL, '', false, true);
            self::dot();
            App::auth()->prefs()->get('dashboard')->put('donate', true, App::userWorkspace()::WS_BOOL, '', false, true);
            self::dot();
            App::auth()->prefs()->get('dashboard')->put('dcnews', true, App::userWorkspace()::WS_BOOL, '', false, true);
            self::dot();
            App::auth()->prefs()->get('dashboard')->put('quickentry', true, App::userWorkspace()::WS_BOOL, '', false, true);
            self::dot();
            App::auth()->prefs()->get('dashboard')->put('nodcupdate', false, App::userWorkspace()::WS_BOOL, '', false, true);
            self::dot();

            // Add accessibility options
            App::auth()->prefs()->get('accessibility')->put('nodragdrop', false, App::userWorkspace()::WS_BOOL, '', false, true);
            self::dot();

            // Add user interface options
            App::auth()->prefs()->get('interface')->put('enhanceduploader', true, App::userWorkspace()::WS_BOOL, '', false, true);
            self::dot();

            // Add default favorites
            $init_favs = ['posts', 'new_post', 'newpage', 'comments', 'categories', 'media', 'blog_theme', 'widgets', 'simpleMenu', 'prefs', 'help'];
            App::install()->favorites()->setFavoriteIDs($init_favs, true);
            self::dot();

            // Success message
            self::msgLine('');
            self::okLine(__('Second step of Dotclear installation succeed.'));
            self::msgLine(sprintf(__('Go to visit "%s" to manage your blog.'), App::config()->adminUrl()));

            if (!empty($plugins_install['failure'])) {
                self::koLine(__('Following plugins have not been installed:') . ' ' . implode(', ', array_keys($plugins_install['failure'])));
            }
        }

        if (!self::$interactive) {
            echo '1';
        }
    }

    private static function parseDbDriver(): string
    {
        $in = self::cleanString(
            empty(self::$options['dbdriver']) ?
            self::inLine(sprintf(__('Configure the database driver (%s):'), implode(',', App::db()->combo()))) :
            self::$options['dbdriver']
        );

        if ($in === '') {
            self::koLine(__('No database driver.'));
        } elseif (!in_array($in, App::db()->combo())) {
            self::koLine(__('Invalid database driver.'));
        } else {
            self::okLine(sprintf(__('Database driver is set to "%s".'), $in));
            self::$options['dbdriver'] = '';

            return $in;
        }

        return self::parseDbDriver();
    }

    private static function parseDbPath(): string
    {
        $in = self::cleanString(
            self::$options['dbname'] ?? self::inLine(__('Configure the database path:'))
        );

        if (!str_contains($in, '/')) {
            if ($in === '') {
                // create sqlite db name if not set
                $in = date('YmdHi') . '.sqlite';
            }

            // Create sqlite db dir if not set
            $sqlite_db_directory = dirname(App::config()->configPath()) . '/../db/';
            Files::makeDir($sqlite_db_directory, true);

            // Can we write sqlite_db_directory ?
            if (!is_writable($sqlite_db_directory)) {
                throw new Exception(sprintf(__('Cannot write "%s" directory.'), Path::real($sqlite_db_directory, false)));
            }

            $in = (string) Path::real($sqlite_db_directory . $in, false);
            if (!file_exists($in)) {
                touch($in);
            }

            // Try to fix file right
            if (function_exists('chmod')) {
                try {
                    @chmod($in, 0o666);
                } catch (Exception) {
                }
            }
        }

        self::okLine(sprintf(__('Database path is set to "%s".'), $in));
        self::$options['dbname'] = '';

        return $in;
    }

    private static function parseDbHost(bool $loop = false): string
    {
        $in = self::cleanString(
            $loop || empty(self::$options['dbhost']) ?
            self::inLine(__('Configure the database host:')) :
            self::$options['dbhost']
        );

        if ($in === '') {
            self::koLine(__('No database host.'));
        } else {
            self::okLine(sprintf(__('Database host is set to "%s".'), $in));

            return $in;
        }

        return self::parseDbHost(true);
    }

    private static function parseDbName(bool $loop = false): string
    {
        $in = self::cleanString(
            $loop || empty(self::$options['dbname']) ?
            self::inLine(__('Configure the database name:')) :
            self::$options['dbname']
        );

        if ($in === '') {
            self::koLine(__('No database name.'));
        } else {
            self::okLine(sprintf(__('Database name is set to "%s".'), $in));

            return $in;
        }

        return self::parseDbName(true);
    }

    private static function parseDbUser(bool $loop = false): string
    {
        $in = self::cleanString(
            $loop || empty(self::$options['dbuser']) ?
            self::inLine(__('Configure the database user: ')) :
            self::$options['dbuser']
        );

        if ($in === '') {
            self::koLine(__('No database user.'));
        } else {
            self::okLine(sprintf(__('Database user is set to "%s"'), $in));

            return $in;
        }

        return self::parseDbUser(true);
    }

    private static function parseDbPassword(bool $loop = false): string
    {
        $in = self::cleanString(
            $loop || empty(self::$options['dbpassword']) ?
            self::inLine(__('Configure the database password:')) :
            self::$options['dbpassword']
        );

        if ($in === '') {
            self::koLine(__('No database password.'));
        } else {
            self::okLine(__('Database password is set.'));

            return $in;
        }

        return self::parseDbPassword(true);
    }

    private static function parseDbPrefix(bool $loop = false): string
    {
        $in = self::cleanString(
            $loop || !isset(self::$options['dbprefix']) ?
            self::inLine(__('Configure the database table prefix:')) :
            self::$options['dbprefix']
        );

        if ($in === '') {
            $in = 'dc_';
        }

        if (!preg_match('/^[A-Za-z0-9@._-]{2,}$/', $in)) {
            self::koLine(__('Invalid database prefix.'));
        } else {
            self::okLine(sprintf(__('Database table prefix is set to "%s".'), $in));

            return $in;
        }

        return self::parseDbPrefix(true);
    }

    private static function parseAdminEmail(bool $loop = false): string
    {
        $in = self::cleanString(
            $loop || empty(self::$options['adminemail']) ?
            self::inLine(__('Configure the administration mail from:')) :
            self::$options['adminemail']
        );

        if ($in === '') {
            self::koLine(__('No administration mail from.'));
        } elseif (!Text::isEmail($in)) {
            self::koLine(__('Invalid administration mail from.'));
        } else {
            self::okLine(sprintf(__('Administration mail from is set to "%s".'), $in));

            return $in;
        }

        return self::parseAdminEmail(true);
    }

    private static function parseAdminUrl(): string
    {
        $in = self::cleanString(
            empty(self::$options['adminurl']) ?
            self::inLine(__('Configure the administration full URL:')) :
            self::$options['adminurl']
        );

        if ($in === '') {
            self::koLine(__('No administration URL.'));
        } elseif (!preg_match('#^http(s)?://#', $in)) {
            self::koLine(__('Invalid administration URL.'));
        } else {
            $in = (string) preg_replace('%/(index.php)?$%', '', $in);
            self::okLine(sprintf(__('Administration URL is set to "%s".'), $in));
            self::$options['adminurl'] = '';

            return $in;
        }

        return self::parseAdminUrl();
    }

    private static function parseUserFirstname(): string
    {
        $in = self::cleanString(
            self::$options['ufirstname'] ?? self::inLine(__('Super administrator first name (optionnal):'))
        );

        self::okLine(sprintf(__('Super administrator first name is set to "%s".'), $in));

        return $in;
    }

    private static function parseUserLastname(): string
    {
        $in = self::cleanString(
            self::$options['ulastname'] ?? self::inLine(__('Super administrator last name (optionnal):'))
        );

        self::okLine(sprintf(__('Super administrator last name is set to "%s".'), $in));

        return $in;
    }

    private static function parseUserEmail(bool $loop = false): string
    {
        $in = self::cleanString(
            $loop || empty(self::$options['uemail']) ?
            self::inLine(__('Configure the super administrator mail:')) :
            self::$options['uemail']
        );

        if ($in === '') {
            self::koLine(__('No super administrator mail.'));
        } elseif (!Text::isEmail($in)) {
            self::koLine(__('Invalid super administrator mail.'));
        } else {
            self::okLine(sprintf(__('Super administrator mail is set to "%s".'), $in));

            return $in;
        }

        return self::parseUserEmail(true);
    }

    private static function parseUserLogin(bool $loop = false): string
    {
        $in = self::cleanString(
            $loop || !isset(self::$options['ulogin']) ?
            self::inLine(__('Configure the super administrator login:')) :
            self::$options['ulogin']
        );

        if ($in === '') {
            self::koLine(__('No super administrator login.'));
        } elseif (!preg_match('/^[A-Za-z0-9@._-]{2,}$/', $in)) {
            self::koLine(__('Super administrator login must contain at least 2 characters using letters, numbers or symbols.'));
        } else {
            self::okLine(sprintf(__('Super administrator login is set to "%s".'), $in));

            return $in;
        }

        return self::parseUserLogin(true);
    }

    private static function parseUserPassword(bool $loop = false): string
    {
        $in = self::cleanString(
            $loop || empty(self::$options['upassword']) ?
            self::inLine(__('Configure the super administrator password: ')) :
            self::$options['upassword']
        );

        if ($in === '') {
            self::koLine(__('No super administrator password.'));
        } elseif (strlen($in) < 6) {
            self::koLine(__('Password must contain at least 6 characters.'));
        } else {
            $retype = $in;
            if (empty(self::$options['upassword'])) { // Do not confirm password form command arguments
                $retype = self::inLine(__('Confirm the super administrator password:'));
                $retype = self::cleanString($retype);
            }

            if ($retype !== $in) {
                self::koLine(__("Passwords don't match"));
            } else {
                self::okLine(__('Super administrator password is set.'));

                return $in;
            }
        }

        return self::parseUserPassword(true);
    }

    private static function parseBlogUrl(): string
    {
        $in = self::cleanString(
            empty(self::$options['blogurl']) ?
            self::inLine(__('Configure the blog full URL:')) :
            self::$options['blogurl']
        );

        if ($in === '') {
            self::koLine(__('No blog URL.'));
        } elseif (!preg_match('#^http(s)?://#', $in)) {
            self::koLine(__('Invalid blog URL.'));
        } else {
            $in = (string) preg_replace('%/(index.php)?$%', '', $in);
            self::okLine(sprintf(__('Blog URL is set to "%s".'), $in));
            self::$options['blogurl'] = '';

            return $in;
        }

        return self::parseBlogUrl();
    }

    /**
     * Clean CLI response.
     */
    private static function cleanString(mixed $in): string
    {
        return trim(is_scalar($in) ? (string) $in : '');
    }

    /**
     * CLI error line.
     *
     * @param   string  $text   The error text
     */
    private static function koLine(string $text): void
    {
        if (self::$interactive) {
            echo "[\033[31mKO\033[0m] " . $text . "\n";

            return;
        }

        throw new Exception($text);
    }

    /**
     * Dot.
     */
    private static function dot(string $text = ''): void
    {
        if (self::$interactive) {
            echo $text !== '' ? $text : '.';
        }
    }

    /**
     * CLI message line.
     *
     * @param   string  $text   The success text
     */
    private static function msgLine(string $text): void
    {
        if (self::$interactive) {
            echo $text . "\n";
        }
    }

    /**
     * CLI success line.
     *
     * @param   string  $text   The success text
     */
    private static function okLine(string $text): void
    {
        if (self::$interactive) {
            echo "[\033[32mOK\033[0m] " . $text . "\n";
        }
    }

    /**
     * CLI query line.
     *
     * @param   string  $text   The query
     *
     * @return  mixed   The user response
     */
    private static function inLine(string $text): mixed
    {
        if (self::$interactive) {
            echo "[\033[33mIN\033[0m] " . $text . ' ';

            return  fgets(STDIN);
        }

        return '';
    }

    /**
     * Writes a configuration value in configuration file content.
     *
     * @param      string  $name                The name
     * @param      string  $value               The value
     * @param      string  $config_content      The configuration file content
     *
     * @return     string  The new configuration file content
     */
    private static function writeConfigValue(string $name, string $value, string $config_content): string
    {
        $value = str_replace("'", "\'", $value);

        return (string) preg_replace('/(\'' . $name . '\')(.*?)$/ms', '$1,\'' . $value . '\');', $config_content);
    }
}
