<?php
/**
 * @package     Dotclear
 *
 * @copyright   Olivier Meunier & Association Dotclear
 * @copyright   AGPL-3.0
 */

namespace Dotclear\Process\Install;

use Dotclear\App;
use Dotclear\Exception\ConfigException;
use Dotclear\Exception\NotFoundException;
use Dotclear\Helper\File\Files;
use Dotclear\Helper\File\Path;
use Dotclear\Helper\Html\Form\Div;
use Dotclear\Helper\Html\Form\Email;
use Dotclear\Helper\Html\Form\Form;
use Dotclear\Helper\Html\Form\Input;
use Dotclear\Helper\Html\Form\Label;
use Dotclear\Helper\Html\Form\Li;
use Dotclear\Helper\Html\Form\Note;
use Dotclear\Helper\Html\Form\Option;
use Dotclear\Helper\Html\Form\Para;
use Dotclear\Helper\Html\Form\Password;
use Dotclear\Helper\Html\Form\Select;
use Dotclear\Helper\Html\Form\Set;
use Dotclear\Helper\Html\Form\Span;
use Dotclear\Helper\Html\Form\Strong;
use Dotclear\Helper\Html\Form\Submit;
use Dotclear\Helper\Html\Form\Text;
use Dotclear\Helper\Html\Form\Ul;
use Dotclear\Helper\Html\Html;
use Dotclear\Helper\Network\Http;
use Dotclear\Helper\Process\TraitProcess;
use Dotclear\Helper\Text as Htext;
use Exception;

/**
 * @brief   Installation wizard process.
 */
class Wizard
{
    use TraitProcess;

    /**
     * Error description
     */
    private static string $err = '';

    /**
     * Installation checking flag
     */
    private static bool $can_install = true;

    /**
     * Available database drivers.
     *
     * @var     Option[]
     */
    private static array $drivers = [];

    /**
     * DB driver name
     */
    private static string $DBDRIVER = 'mysqli';

    /**
     * DB host
     */
    private static string $DBHOST = '';

    /**
     * DB name
     */
    private static string $DBNAME = '';

    /**
     * DB credentials username
     */
    private static string $DBUSER = '';

    /**
     * DB credentials password
     */
    private static string $DBPASSWORD = '';

    /**
     * DB tables prefix
     */
    private static string $DBPREFIX = 'dc_';

    /**
     * Admin email
     */
    private static string $ADMINMAILFROM = '';

    public static function init(): bool
    {
        if (!self::status(App::task()->checkContext('INSTALL') && !App::config()->hasConfig())) {
            throw new NotFoundException();
        }

        // Loading locales for detected language
        $dlang = Http::getAcceptLanguage();
        if ($dlang !== 'en') {
            App::lang()->init($dlang);
            App::lang()->set(App::config()->dotclearRoot() . '/locales/' . $dlang . '/main');
        }

        if (!is_writable(dirname(App::config()->configPath()))) {
            self::$can_install = false;
            self::$err .= (new Set())
                ->items([
                    (new Text('p', sprintf(__('Path <strong>%s</strong> is not writable.'), Path::real(dirname(App::config()->configPath()))))),
                    (new Text('p', sprintf(
                        __('Dotclear installation wizard could not create configuration file for you. You must change folder right or create the <strong>config.php</strong> file manually, please refer to <a href="%s">the documentation</a> to learn how to do this.'),
                        'https://dotclear.org/category/Documentation/Installer-et-g%C3%A9rer'
                    ))),
                ])
                ->render();
        }

        foreach (App::db()->combo() as $key => $value) {
            self::$drivers[] = new Option($key, $value);
        }
        if (self::$drivers === []) {
            self::$can_install = false;
            self::$err .= (new Set())
                ->items([
                    new Text('p', __('There are no supported database driver.')),
                    (new Text('p', sprintf(
                        __('Dotclear installation wizard could not find database driver. You must change your PHP configuration to add a supported database handler please refer to <a href="%s">the documentation</a> to learn how to do this.'),
                        'https://dotclear.org/category/Documentation/Installer-et-g%C3%A9rer'
                    ))),
                ])
                ->render();
        }

        return self::status();
    }

    public static function process(): bool
    {
        if (!self::status()) {
            throw new NotFoundException();
        }

        if (!self::$can_install) {
            return true;
        }

        # Uses HTML form or server variables (ie docker)
        $_Str = fn (string $name, string $default = ''): string => isset($_POST[$name]) && is_string($value = $_POST[$name])
                ? $value
                : (isset($_SERVER['DC_' . $name]) && is_string($value = $_SERVER['DC_' . $name]) ? $value : $default);

        self::$DBDRIVER      = $_Str('DBDRIVER', 'mysqli');
        self::$DBHOST        = $_Str('DBHOST');
        self::$DBNAME        = $_Str('DBNAME');
        self::$DBUSER        = $_Str('DBUSER');
        self::$DBPASSWORD    = $_Str('DBPASSWORD');
        self::$DBPREFIX      = $_Str('DBPREFIX', 'dc_');
        self::$ADMINMAILFROM = $_Str('ADMINMAILFROM');

        if ($_POST !== [] || !empty($_SERVER['DC_DBDRIVER'])) {
            try {
                if (str_contains(self::$DBDRIVER, 'sqlite') && !str_contains(self::$DBNAME, '/')) {
                    if (self::$DBNAME === '') {
                        // create sqlite db name if not set
                        self::$DBNAME = 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)) {
                        self::throwString(sprintf(__('Cannot write "%s" directory.'), Path::real($sqlite_db_directory, false)));
                    }
                    self::$DBNAME = (string) Path::real($sqlite_db_directory . self::$DBNAME, false);
                    if (!file_exists(self::$DBNAME)) {
                        touch(self::$DBNAME);
                    }
                }

                # Tries to connect to database
                $con = App::db()->newCon(self::$DBDRIVER, self::$DBHOST, self::$DBNAME, self::$DBUSER, self::$DBPASSWORD);

                # Checks system capabilites
                $_e = [];
                if (!App::install()->utils()->check($con, $_e)) {
                    throw new ConfigException(
                        (new Set())
                        ->items([
                            new Text('p', __('Dotclear cannot be installed.')),
                            (new Ul())
                                ->items(array_map(fn (string $e): Li => (new Li())->text($e), $_e)),
                        ])
                        ->render()
                    );
                }

                # Check if dotclear is already installed
                if (in_array(self::$DBPREFIX . 'version', $con->schema()->getTables())) {
                    self::throwString(__('Dotclear is already installed.'));
                }
                # Check master email
                if (!HText::isEmail(self::$ADMINMAILFROM)) {
                    self::throwString(__('Master email is not valid.'));
                }

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

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

                # Creates config.php file
                $request_uri = isset($_SERVER['REQUEST_URI']) && is_string($request_uri = $_SERVER['REQUEST_URI']) ? $request_uri : '';
                $admin_url   = preg_replace('%install/index.php$%', '', $request_uri);

                $http_host   = isset($_SERVER['HTTP_HOST']) && is_string($http_host = $_SERVER['HTTP_HOST']) ? $http_host : '';
                $admin_email = self::$ADMINMAILFROM ?: 'dotclear@' . $http_host;

                $full_conf = (string) file_get_contents($config_in);

                $full_conf = self::writeConfigValue('DC_DBDRIVER', self::$DBDRIVER, $full_conf);
                $full_conf = self::writeConfigValue('DC_DBHOST', self::$DBHOST, $full_conf);
                $full_conf = self::writeConfigValue('DC_DBUSER', self::$DBUSER, $full_conf);
                $full_conf = self::writeConfigValue('DC_DBPASSWORD', self::$DBPASSWORD, $full_conf);
                $full_conf = self::writeConfigValue('DC_DBNAME', self::$DBNAME, $full_conf);
                $full_conf = self::writeConfigValue('DC_DBPREFIX', self::$DBPREFIX, $full_conf);
                $full_conf = self::writeConfigValue('DC_ADMIN_URL', Http::getHost() . $admin_url, $full_conf);
                $full_conf = self::writeConfigValue('DC_ADMIN_MAILFROM', $admin_email, $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) {
                    self::throwString(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();
                Http::redirect('index.php?wiz=1');
            } catch (ConfigException $e) {
                self::$err .= $e->getMessage();
            } catch (Exception $e) {
                self::$err .= (new Text('p', $e->getMessage()))->render();
            }
        }

        return true;
    }

    public static function render(): void
    {
        if (!self::status()) {
            throw new NotFoundException();
        }

        header('Content-Type: text/html; charset=UTF-8');

        // Prevents Clickjacking as far as possible
        header('X-Frame-Options: SAMEORIGIN'); // FF 3.6.9+ Chrome 4.1+ IE 8+ Safari 4+ Opera 10.5+

        ?>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Script-Type" content="text/javascript">
  <meta http-equiv="Content-Style-Type" content="text/css">
  <meta http-equiv="Content-Language" content="en">
  <meta name="ROBOTS" content="NOARCHIVE,NOINDEX,NOFOLLOW">
  <meta name="GOOGLEBOT" content="NOSNIPPET">
  <title><?= __('Dotclear installation wizard') ?></title>
    <link rel="stylesheet" href="../style/install.css" type="text/css" media="screen">
</head>

<body id="dotclear-admin" class="install">
        <?php self::renderContent(); ?>
</body>
</html>
        <?php
    }

    /**
     * Render form content.
     */
    private static function renderContent(): void
    {
        $required = (new Span())->text('*')->render();

        if (!self::$can_install) {
            echo (new Div('content'))
                ->items([
                    (new Text('h1', __('Dotclear installation wizard'))),
                    (new Div('main'))
                        ->items([
                            (new Div())
                                ->class('error')
                                ->role('main')
                                ->items([
                                    (new Para())
                                        ->items([
                                            (new Text('p'))
                                                ->items([
                                                    new Strong(__('Errors:')),
                                                ]),
                                            new Text('', self::$err),
                                        ]),
                                ]),
                            new Text('p', __('Dotclear can not be installed.')),
                        ]),
                ])
                ->render();

            return;
        }

        echo (new Div('content'))
            ->items([
                (new Text('h1', __('Dotclear installation wizard'))),
                (new Div('main'))
                    ->items([...
                        (self::$err !== '' ? [
                            (new Div())
                                ->class('error')
                                ->role('main')
                                ->items([
                                    (new Para())
                                        ->items([
                                            (new Text('p'))
                                                ->items([
                                                    new Strong(__('Errors:')),
                                                ]),
                                            new Text('', self::$err),
                                        ]),
                                ]),
                        ] : [
                            (new Text('h2', __('Welcome'))),
                            (new Text('p', __('To complete your Dotclear installation and start writing on your blog, we just need to know how to access your database and who you are. Just fill this two steps wizard with this information and we will be done.'))),
                            (new Set())
                                ->items([
                                    (new Text('p'))
                                        ->class('message')
                                        ->items([
                                            new Strong(__('Attention:')),
                                            (new Text('', sprintf(
                                                __('this wizard may not function on every host. If it does not work for you, please refer to <a href="%s">the documentation</a> to learn how to create the <strong>config.php</strong> file manually.'),
                                                'https://dotclear.org/category/Documentation/Installer-et-g%C3%A9rer'
                                            ))),
                                        ]),
                                ]),
                        ]),
                        (new Text('h2', __('System information'))),
                        (new Text('p', __('Please provide the following information needed to create your configuration file.'))),
                        (new Form('install-form'))
                            ->method('post')
                            ->action('index.php')
                            ->fields([
                                (new Para())
                                    ->class('form-note')
                                    ->items([
                                        (new Note())
                                            ->text(sprintf(__('Fields preceded by %s are mandatory.'), '<span class="required">*</span>'))
                                            ->class('form-note'),
                                        (new Label($required . __('Database type:')))
                                            ->class('required')
                                            ->for('DBDRIVER'),
                                        (new Select('DBDRIVER'))
                                            ->items(self::$drivers)
                                            ->default(self::$DBDRIVER)
                                            ->extra('required placeholder="' . __('Driver') . '"'),
                                        (new Para())
                                            ->items([
                                                (new Input('DBHOST'))
                                                    ->size(30)
                                                    ->maxlength(255)
                                                    ->value(Html::escapeHTML(self::$DBHOST))
                                                    ->label(new Label(__('Database Host Name:'), Label::OUTSIDE_LABEL_BEFORE)),
                                            ]),
                                        (new Para())
                                            ->items([
                                                (new Input('DBNAME'))
                                                    ->size(30)
                                                    ->maxlength(255)
                                                    ->value(Html::escapeHTML(self::$DBNAME))
                                                    ->label(new Label(__('Database Name:'), Label::OUTSIDE_LABEL_BEFORE)),
                                            ]),
                                        (new Para())
                                            ->items([
                                                (new Input('DBUSER'))
                                                    ->size(30)
                                                    ->maxlength(255)
                                                    ->value(Html::escapeHTML(self::$DBUSER))
                                                    ->label(new Label(__('Database User Name:'), Label::OUTSIDE_LABEL_BEFORE)),
                                            ]),
                                        (new Para())
                                            ->items([
                                                (new Password('DBPASSWORD'))
                                                    ->size(30)
                                                    ->maxlength(255)
                                                    ->value('')
                                                    ->label(new Label(__('Database Password:'), Label::OUTSIDE_LABEL_BEFORE)),
                                            ]),
                                        (new Para())
                                            ->items([
                                                (new Input('DBPREFIX'))
                                                    ->size(30)
                                                    ->maxlength(255)
                                                    ->value(Html::escapeHTML(self::$DBPREFIX))
                                                    ->extra('required placeholder="' . __('Prefix') . '"')
                                                    ->label((new Label($required . __('Database Tables Prefix:'), Label::OUTSIDE_LABEL_BEFORE))->class('required')),
                                            ]),
                                        (new Para())
                                            ->items([
                                                (new Email('ADMINMAILFROM'))
                                                    ->size(30)
                                                    ->autocomplete('email')
                                                    ->value(Html::escapeHTML(self::$ADMINMAILFROM))
                                                    ->extra('required placeholder="' . __('Email') . '"')
                                                    ->label(new Label($required . __('Master Email: (used as sender for password recovery)'), Label::OUTSIDE_LABEL_BEFORE)),
                                            ]),
                                        (new Submit('submit', __('Continue'))),
                                    ]),
                            ]),
                    ]),
            ])
            ->render();
    }

    /**
     * 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);
    }

    /**
     * Parse error message.
     *
     * @param   string          $message    The message
     * @param   int             $code       The code
     * @param   null|Exception  $error      The error
     *
     * @throws  ConfigException
     *
     * @return  never
     */
    private static function throwString(string $message, int $code = 0, ?Exception $error = null): void
    {
        throw new ConfigException((new Text('p', $message))->render(), $code, $error);
    }
}
