<?php
declare(strict_types=1);
const GRPG_INC     = true;
const NO_SESSION   = true;
const NO_CLASSES   = true;
const NO_FUNCTIONS = true;
const NO_CSRF      = true;
const INSTALLER    = true;
require_once __DIR__ . '/header.php';
if (!defined('PHP_VERSION_ID')) {
    $version = array_map('intval', explode('.', PHP_VERSION));
    define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2]));
}

/**
 *
 */
class gRpgInstall
{
    private static ?self $instance = null;
    private ?database $db = null;
    private string $configFile {
        set {
            $this->configFile = $value;
        }
    }
    private string $mainPath {
        set {
            $this->mainPath = $value;
        }
    }
    private string $path {
        set {
            $this->path = $value;
        }
    }
    private string $sqlPathMain {
        set {
            $this->sqlPathMain = $value;
        }
    }
    private array $steps;

    public function __construct()
    {
        $this->mainPath = dirname(__DIR__);
        $paths          = explode(DIRECTORY_SEPARATOR, $this->mainPath);
        $this->path     = end($paths);
        $this->steps    = range(1, 6);

        $this->sqlPathMain = __DIR__ . DIRECTORY_SEPARATOR . 'sqls' . DIRECTORY_SEPARATOR . 'grpg-pdo.sql';
        $this->configFile  = dirname(__DIR__) . DIRECTORY_SEPARATOR . '.env';
        $_GET['step']      = isset($_GET['step']) && is_numeric($_GET['step']) && in_array($_GET['step'], $this->steps) ? $_GET['step'] : 1; ?>
        <div class="header">
            <h1>gRPG: PDO</h1>
            <h2>Installation</h2>
            <h3>Progress: <span class="pure-u-<?php echo $_GET['step']; ?>-<?php echo count($this->steps); ?>"></span></h3>
            <p>Step <?php echo $_GET['step']; ?> of <?php echo count($this->steps); ?></p>
        </div>
        <div class="content">
            <?php
            match ($_GET['step']) {
                default => $this->preChecks(),
                2 => $this->dbDetails(),
                3 => $this->generateConfigFile(),
                4 => $this->tryDbConn(),
                5 => $this->createAccountForm(),
                6 => $this->doCreateAccount(),
            }; ?>
        </div>
        <?php
    }

    /**
     * @return void
     */
    private function preChecks(): void
    {
        $this->checkInstallation(); ?>
        <h2 class="content-subhead">Let's do some checks first...</h2>
        <form action="install.php?step=2" method="post" class="pure-form pure-form-aligned">
            <div class="pure-control-group">
                <label for="version">PHP Version</label>
                <span id="version"
                      class="<?php echo PHP_VERSION_ID >= 70400 ? 'green' : 'red'; ?>"><?php echo PHP_VERSION; ?></span>
            </div>
            <div class="pure-control-group">
                <label for="sql-file">SQL File</label>
                <span id="sql-file"><?php echo is_file($this->sqlPathMain) ? '<span class="green">Exists</span>' : '<span class="red">Doesn\'t exist!</span>'; ?></span>
            </div>
            <div class="pure-control-group">
                <label for="game-dir">Game Directory</label>
                <input type="text" name="gamedir" id="game-dir" value="<?php echo DIRECTORY_SEPARATOR; ?>"/>
            </div>
            <div class="pure-controls">
                <button type="submit" class="pure-button pure-button-primary">Check</button>
            </div>
        </form>
        <p>
            *<strong>Game Directory:</strong> This is simply where you've uploaded the game - this is usually
            <code>/</code>.<br/>
            Make sure that <code>/.env</code> is writable.<br/>
            If you're not sure, just leave it blank.
        </p>
        <?php
    }

    /**
     * @return void
     */
    private function checkInstallation(): void
    {
        $installed = getenv('INSTALLER_RAN');
        if ($installed === 'true') {
            $this->info('It looks like your game may have already been installed... Please check that <code>/.env</code> contains the correct information');
            exit;
        }
    }

    /**
     * @param string $msg
     * @return void
     */
    private function info(string $msg): void
    {
        ?>
        <div class="notification notification-info">
            <span class="fas fa-info-circle"></span>
            <p><?php echo $msg; ?></p>
        </div>
        <?php
    }

    /**
     * @return void
     */
    private function dbDetails(): void
    {
        $this->checkInstallation();
        $_POST['gamedir'] = isset($_POST['gamedir']) && is_string($_POST['gamedir']) ? $_POST['gamedir'] : null;
        $this->path       = ltrim(DIRECTORY_SEPARATOR . (!empty($_POST['gamedir']) ? $_POST['gamedir'] : DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR);
        $this->path       = str_replace([DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, '\\' . DIRECTORY_SEPARATOR, '/'], DIRECTORY_SEPARATOR, $this->mainPath . $this->path);
        if (!is_dir($this->path)) {
            $this->error('That\'s not a valid directory path: ' . $this->path);
        }
        if (!is_dir($this->path)) {
            $this->error('I couldn\'t find that directory. Are you sure you\'ve entered the correct game path?');
        }
        $timezones = $this->formatTimeZones(timezone_identifiers_list()); ?>
        <h2 class="content-subhead">That checks out fine!</h2>
        <p>
        <form action="install.php?step=3" method="post" class="pure-form pure-form-aligned">
            <input type="hidden" name="gamedir" value="<?php echo $this->path; ?>"/>
            <fieldset>
                <legend>Database Configuration</legend>
                <div class="pure-control-group">
                    <label for="host">Host</label>
                    <input type="text" name="host" id="host" value="localhost"/>
                </div>
                <div class="pure-control-group">
                    <label for="user">User</label>
                    <input type="text" name="user" id="user" placeholder="root"/>
                </div>
                <div class="pure-control-group">
                    <label for="pass">Password</label>
                    <input type="password" name="pass" id="pass"/>
                </div>
                <div class="pure-control-group">
                    <label for="name">Database</label>
                    <input type="text" name="name" id="name"/>
                </div>
                <div class="pure-control-group">
                    <label for="offset">Time Offset</label>
                    <?php echo $this->listTimeZones($timezones); ?>
                </div>
            </fieldset>
            <div class="pure-controls">
                <button type="submit" class="pure-button pure-button-primary">Connect</button>
            </div>
        </form>
        <p>
            *<strong>Host:</strong> This speaks for itself. You need to enter the URL to your MySQL database.<br/>
            &nbsp;&nbsp;&nbsp;&nbsp;- For most people, it's normally
            <code>localhost</code>, which is filled in by default.<br/>
            *<strong>User:</strong> The name of the user you created when creating the database.<br/>
            *<strong>Pass:</strong> This is the password you entered when creating the user.<br/>
            *<strong>Database:</strong> And finally, the name of the database itself!
        </p>
        <?php
    }

    /**
     * @param $msg
     * @return void
     */
    private function error($msg): void
    {
        ?>
        <div class="notification notification-error">
            <span class="fa fa-times-circle"></span>
            <p><?php echo $msg; ?></p>
        </div>
        <?php
        exit;
    }

    /**
     * @param array $zones
     * @return array|string
     */
    private function formatTimeZones(array $zones): array|string
    {
        if (!count($zones)) {
            return 'Something screwed up..';
        }
        $locations  = [];
        $continents = [
            'Africa',
            'America',
            'Antarctica',
            'Arctic',
            'Asia',
            'Atlantic',
            'Australia',
            'Europe',
            'Indian',
            'Pacific',
        ];
        foreach ($zones as $zone) {
            $zone = explode('/', $zone);
            // Only use "friendly" continent names
            if (isset($zone[1]) && $zone[1] !== '' && in_array($zone[0], $continents, true)) {
                $locations[$zone[0]][$zone[0] . '/' . $zone[1]] = str_replace('_', ' ', $zone[1]);
            }
        }

        return $locations;
    }

    /**
     * @param array $list
     * @return string
     */
    private function listTimeZones(array $list): string
    {
        if (!count($list)) {
            return 'Something screwed up...';
        }
        $ret = '<select name="timezone">';
        $cnt = 0;
        foreach ($list as $key => $val) {
            ++$cnt;
            $ret .= "\n" . '<option value="0" disabled ' . ($cnt === 1 ? 'selected' : '') . '>------ ' . $key . ' -------</option>';
            foreach ($val as $zone => $show) {
                $ret .= "\n" . '<option value="' . $zone . '">' . $show . '</option>';
            }
        }
        $ret .= '</select>';

        return $ret;
    }

    /**
     * @return void
     */
    private function generateConfigFile(): void
    {
        $this->checkInstallation();
        $_POST['host'] = $_POST['host'] ?? null;
        $_POST['user']     = array_key_exists('user', $_POST) && !empty($_POST['user']) ? $_POST['user'] : 'root';
        $_POST['timezone'] = array_key_exists('timezone', $_POST) && is_string($_POST['timezone']) ? $_POST['timezone'] : null;
        $_POST['gamedir'] = isset($_POST['gamedir']) && is_string($_POST['gamedir']) ? $_POST['gamedir'] : null;
        if (empty($_POST['host'])) {
            $this->error('You didn\'t enter a valid hostname');
        }
        if (!in_array($_POST['host'], ['localhost', '127.0.0.1'], true) && !@checkdnsrr($_POST['host'])) {
            $this->warning('I couldn\'t verify that host. I\'ll continue attempting to install this for you anyway');
        }
        if (empty($_POST['timezone'])) {
            $this->error('You didn\'t select a valid timezone');
        }
        $path             = !empty($_POST['gamedir']) ? $_POST['gamedir'] : '';
        if (!is_dir($path)) {
            $this->error('That\'s not a valid directory path');
        }
        $include_dir = rtrim($path, '/') . '/inc';
        if (!is_dir($path)) {
            $this->error('I couldn\'t find that directory. Are you sure you\'ve entered the correct game path?');
        }
        $site_url      = 'http' . (isset($_SERVER['HTTPS']) ? 's' : '') . '://' . $_SERVER['HTTP_HOST'] . '/';
        $configuration = 'DB_HOST="' . $_POST['host'] . '"
DB_USER="' . $_POST['user'] . '"
DB_PASS="' . $_POST['pass'] . '"
DB_NAME="' . $_POST['name'] . '"
DEFAULT_TIMEZONE="' . $_POST['timezone'] . '"
GAME_PATH="' . addslashes($_POST['gamedir']) . '"
SITE_URL="' . $site_url . ltrim(str_replace([dirname(__DIR__, 2), DIRECTORY_SEPARATOR], ['', '/'], $_POST['gamedir']), '/') . '"
';
        if (!file_exists($this->configFile)) {
            $this->info('The configuration file (<code>' . $this->configFile . '</code>) couldn\'t be found. Trying to create it now...');
            $creation = @fopen($this->configFile, 'wb');
            if (!$creation) {
                $this->error('I couldn\'t open .env to edit! Please manually create it in the <code>/inc</code> directory');
            }
            fwrite($creation, $configuration);
            fclose($creation);
            if (!$creation || !file_exists($this->configFile)) {
                $this->error('The configuration file couldn\'t be created');
            } else {
                $this->success('The configuration file has been created');
            }
        } elseif (file_exists($this->configFile) && !is_writable($this->configFile)) {
            ?>
            <label for="code">Code required:</label><br/><textarea class="pure-input-1-2" rows="10" cols="70" id="code"><?php echo $configuration; ?></textarea><br/>
            <?php
            $this->error('.env exists, but unfortunately couldn\'t be modified. Please make sure your <code>/.env</code> is writable - or edit the file manually');
        } else {
            $creation = fopen($this->configFile, 'wb');
            if (!$creation) {
                $this->error('I couldn\'t edit .env');
            }
            fwrite($creation, $configuration);
            fclose($creation);
            if (!$creation || !file_exists($this->configFile)) {
                $this->error('The configuration file couldn\'t be created');
            } else {
                $this->success('The configuration file has been created');
            }
        }
        $this->info('Attempting connection to the database..');
        require_once $this->mainPath . '/inc/dbcon.php';
        $this->success('We\'ve connected! Moving on...<meta http-equiv="refresh" content="2; url=install.php?step=4" />');
    }

    /**
     * @param string $msg
     * @return void
     */
    private function warning(string $msg): void
    {
        ?>
        <div class="notification notification-secondary">
            <span class="fa fa-secondary-circle"></span>
            <p><?php echo $msg; ?></p>
        </div>
        <?php
    }

    /**
     * @param $msg
     * @return void
     */
    private function success($msg): void
    {
        ?>
        <div class="notification notification-success">
            <span class="fa fa-check-circle"></span>
            <p><?php echo $msg; ?></p>
        </div>
        <?php
    }

    /**
     * @return void
     */
    private function tryDbConn(): void
    {
        require_once $this->mainPath . '/inc/dbcon.php';
        $this->db = database::getInstance(); ?>
        <h2 class="content-subhead">We're connected! Let's install the database</h2>
        <?php
        $templine_main = '';
        if ($this->isSysFuncAvailable()) {
            system('mysql --user=' . getenv('MYSQL_USER') . ' --password=' . getenv('MYSQL_PASS') . ' ' . getenv('MYSQL_BASE') . ' < ' . __DIR__ . DIRECTORY_SEPARATOR . 'sqls' . DIRECTORY_SEPARATOR . 'grpg-pdo.sql');
        } else {
            $lines = file($this->sqlPathMain);
            foreach ($lines as $line) {
                if (strncmp($line, '--', 2) === 0 || !$line) {
                    continue;
                }
                $templine_main .= $line;
                if (str_ends_with(trim($line), ';')) {
                    $this->db->query($templine_main);
                    $this->db->execute();
                    $templine_main = '';
                }
            }
        }
        if ($this->db->tableExists('users')) {
            $this->success('Database installed, let\'s move on.<meta http-equiv="refresh" content="2; url=install.php?step=5" />');
        } else {
            $this->error('The database didn\'t install.. Try importing it manually');
        }
    }

    /**
     * @return self|null
     */
    public static function getInstance(): ?self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * @return bool
     */
    private function isSysFuncAvailable(): bool
    {
        /** @noinspection DeprecatedIniOptionsInspection */
        if (ini_get('safe_mode')) {
            return false;
        }
        $disabled_functions = array_map('trim', explode(',', ini_get('disable_functions')));
        if (!empty($disabled_functions)) {
            return !in_array('system', $disabled_functions, true);
        }
        return true;
    }

    /**
     * @return void
     */
    private function createAccountForm(): void
    {
        ?>
        <h2 class="content-subhead">Database installed, let's configure the game</h2>
        <form action="install.php?step=6" method="post" class="pure-form pure-form-aligned">
            <fieldset>
                <legend>Your Account</legend>
                <div class="pure-control-group">
                    <label for="username">Username</label>
                    <input type="text" name="username" id="username" class="pure-u-1-3" required/>
                </div>
                <div class="pure-control-group">
                    <label for="pass">Password</label>
                    <input type="password" name="pass" id="pass" class="pure-u-1-3" required/>
                </div>
                <div class="pure-control-group">
                    <label for="cpass">Confirm Password</label>
                    <input type="password" name="cpass" id="cpass" class="pure-u-1-3" required/>
                </div>
                <div class="pure-control-group">
                    <label for="email">Email</label>
                    <input type="email" name="email" id="email" class="pure-u-1-3" required/>
                </div>
            </fieldset>
            <div class="pure-controls">
                <button type="submit" name="submit" class="pure-button pure-button-primary">Create Account</button>
                <button type="reset" class="pure-button pure-button-secondary"><i class="fa fa-recycle"></i> Reset</button>
            </div>
        </form>
        <?php
    }

    /**
     * @return void
     */
    private function doCreateAccount(): void
    {
        if (!array_key_exists('submit', $_POST)) {
            $this->error('You didn\'t come from step 5..');
        }
        require_once $this->mainPath . '/inc/dbcon.php';
        if (empty($_POST['username'])) {
            $this->error('You didn\'t enter a valid username');
        }
        if (empty($_POST['pass'])) {
            $this->error('You didn\'t enter a valid password');
        }
        if (empty($_POST['cpass'])) {
            $this->error('You didn\'t enter a valid password confirmation');
        }
        if ($_POST['pass'] != $_POST['cpass']) {
            $this->error('Your passwords didn\'t match');
        }
        $pass    = password_hash($_POST['pass'], PASSWORD_BCRYPT);
        $success = $this->db->exists('users', 'id', 1) ? 'updated' : 'created';
        $this->db->query(
            'INSERT INTO users (id, ip, username, loginame, password, email, admin, activate, signature, notepad) VALUES (1, ?, ?, ?, ?, ?, 1, 1, \'\', \'\') 
                ON DUPLICATE KEY UPDATE username = ?, loginame = ?, password = ?, email = ?, admin = 1, activate = 1',
            [
                $_SERVER['REMOTE_ADDR'], $_POST['username'], $_POST['username'], $pass, $_POST['email'],
                $_POST['username'], $_POST['username'], $pass, $_POST['email'],
            ]
        );
        $this->success('Your account has been ' . $success . '!'); ?>
        <a href="/">Home</a>
        <?php
    }
}

$module            = gRpgInstall::getInstance();
