<?php

declare(strict_types=1);

/**
 * ATDT installer (SPEC §6). Single-file wizard, self-locks after success.
 *
 * Steps: 1 environment check → 2 database → 3 board identity → install → done.
 * Wizard state travels in hidden fields; the final POST (step=install) carries
 * everything, so scripted installs can POST it directly. Re-running before the
 * lock is written starts a clean install (§6: idempotent-safe).
 *
 * The block below runs BEFORE anything modern loads, and this file keeps to
 * PHP 7-parseable syntax on purpose: on a host whose PHP is too old, the
 * caller must see a plain explanation, never a blank 500.
 */

if (PHP_VERSION_ID < 80100) {
    header('Content-Type: text/html; charset=utf-8');
    echo '<!doctype html><html><head><meta charset="utf-8"><title>ATDT needs a newer PHP</title></head>'
        . '<body style="font-family:system-ui,sans-serif;max-width:640px;margin:60px auto;line-height:1.6">'
        . '<h1>This server is running PHP ' . htmlspecialchars(PHP_VERSION, ENT_QUOTES) . '</h1>'
        . '<p><strong>ATDT needs PHP 8.1 or newer.</strong> Nothing was installed.</p>'
        . '<p>On cPanel hosting: <em>MultiPHP Manager</em> &rarr; select this domain or '
        . 'subdomain &rarr; choose PHP 8.1+ &rarr; Apply, then reload this page. '
        . 'Each subdomain can have its own PHP version, so a fresh subdomain often '
        . 'starts on the host default even when your other sites are newer.</p>'
        . '</body></html>';
    exit;
}

// Pre-install only: show real errors instead of a blank page. The installer
// self-locks after success, so this debugging aid retires with it.
error_reporting(E_ALL);
ini_set('display_errors', '1');

require __DIR__ . '/lib/bootstrap.php';

use Atdt\Db;
use Atdt\Schema;

function h(?string $s): string
{
    return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}

// ---------------------------------------------------------------------------
// Already installed? Refuse to run (§6).
// ---------------------------------------------------------------------------
if (atdt_installed()) {
    page('Already installed', '
      <h1>ATDT is already installed</h1>
      <p class="ok">✅ This board is up and running; the installer has locked itself.</p>
      <p><a class="btn" href="index.php">Go to the board</a>
         <a class="btn secondary" href="admin/">Sysop Console</a></p>
      <p class="hint">You may delete <code>install.php</code> from the server now.
         To reinstall from scratch, delete <code>data/config.php</code> first.</p>');
}

// ---------------------------------------------------------------------------
// AJAX: MySQL "test connection" (§6 step 2)
// ---------------------------------------------------------------------------
if (($_GET['ajax'] ?? '') === 'testdb') {
    header('Content-Type: application/json; charset=utf-8');
    try {
        $db = mysqlFromPost();
        $version = $db->val('SELECT VERSION()');
        echo json_encode(['ok' => true, 'msg' => 'Connected: server version ' . $version]);
    } catch (\Throwable $e) {
        echo json_encode(['ok' => false, 'msg' => 'Connection failed: ' . sanitizeDbError($e->getMessage())]);
    }
    exit;
}

$step = $_POST['step'] ?? '1';
if ($step === 'install') {
    runInstall();
} elseif ($step === '3') {
    renderStep3([]);
} elseif ($step === '2') {
    renderStep2();
} else {
    renderStep1();
}
exit;

// ---------------------------------------------------------------------------
// Steps
// ---------------------------------------------------------------------------

function renderStep1(): never
{
    [$checks, $hardFail] = envChecks();
    $rows = '';
    foreach ($checks as [$label, $state, $hint]) {
        $mark = $state === true ? '<span class="ok">✅</span>'
              : ($state === null ? '<span class="warn">⚠️</span>' : '<span class="bad">❌</span>');
        $rows .= '<tr><td>' . $mark . '</td><td>' . h($label) . '</td><td class="hint">' . h($hint) . '</td></tr>';
    }
    $button = $hardFail
        ? '<p class="bad">Fix the items marked ❌ above, then reload this page.</p>'
        : '<form method="post"><input type="hidden" name="step" value="2">
           <button class="btn" type="submit">Continue →</button></form>';
    page('Environment check', '
      <h1>Install ATDT <span class="step">step 1 of 4: requirements</span></h1>
      <p>Checking that this hosting account can run ATDT: the PHP version,
      the required PHP extensions, a database driver, and a writable
      <code>data/</code> directory for the board\'s files. Nothing is
      installed or changed in this step.</p>
      <table class="checks">' . $rows . '</table>
      <p class="hint">✅ ready &nbsp; ⚠️ optional, works without it &nbsp; ❌ must be fixed before installing</p>'
      . $button);
}

function renderStep2(string $error = ''): never
{
    $err = $error !== '' ? '<p class="bad">' . h($error) . '</p>' : '';
    $v = fn(string $k, string $d = '') => h((string)($_POST[$k] ?? $d));
    page('Database', '
      <h1>Install ATDT <span class="step">step 2 of 4: database</span></h1>' . $err . '
      <form method="post">
        <input type="hidden" name="step" value="3">
        <label class="radio"><input type="radio" name="db_driver" value="sqlite" checked
          onclick="document.getElementById(\'my\').style.display=\'none\'">
          <strong>SQLite</strong>: recommended. Zero configuration; the database lives in <code>data/</code>.</label>
        <label class="radio"><input type="radio" name="db_driver" value="mysql"
          onclick="document.getElementById(\'my\').style.display=\'block\'">
          <strong>MySQL / MariaDB</strong>: use a database your host provides.</label>
        <fieldset id="my" style="display:none">
          <legend>MySQL connection</legend>
          <label>Host <input name="my_host" value="' . $v('my_host', 'localhost') . '"></label>
          <label>Port <input name="my_port" value="' . $v('my_port', '3306') . '" size="6"></label>
          <label>Database name <input name="my_name" value="' . $v('my_name') . '"></label>
          <label>User <input name="my_user" value="' . $v('my_user') . '"></label>
          <label>Password <input type="password" name="my_pass" value="' . $v('my_pass') . '"></label>
          <p><button class="btn secondary" type="button" onclick="testDb(this)">Test connection</button>
             <span id="testout" class="hint"></span></p>
        </fieldset>
        <button class="btn" type="submit">Continue →</button>
      </form>
      <script>
      function testDb(btn){
        const f = btn.form, out = document.getElementById("testout");
        out.textContent = "Testing…";
        fetch("install.php?ajax=testdb", {method:"POST", body:new FormData(f)})
          .then(r => r.json())
          .then(j => { out.textContent = j.msg; out.className = j.ok ? "ok" : "bad"; })
          .catch(() => { out.textContent = "Request failed."; out.className = "bad"; });
      }
      </script>');
}

/** @param list<string> $errors */
function renderStep3(array $errors): never
{
    $err = $errors ? '<ul class="bad">' . implode('', array_map(fn($e) => '<li>' . h($e) . '</li>', $errors)) . '</ul>' : '';
    $v = fn(string $k, string $d = '') => h((string)($_POST[$k] ?? $d));
    $tzOptions = atdt_timezone_options((string)($_POST['timezone'] ?? 'UTC'));
    page('Board identity', '
      <h1>Install ATDT <span class="step">step 3 of 4: board identity</span></h1>' . $err . '
      <form method="post">
        <input type="hidden" name="step" value="install">' . carryDbFields() . '
        <label>Board name <input name="board_name" value="' . $v('board_name', 'ATDT') . '" maxlength="60"></label>
        <label>Sysop handle <input name="sysop_handle" value="' . $v('sysop_handle', 'Sysop') . '" maxlength="20">
          <span class="hint">3-20 characters: letters, digits, space, . _ -</span></label>
        <label>Sysop password <input type="password" name="sysop_pass">
          <span class="hint">minimum 8 characters</span></label>
        <label>Repeat password <input type="password" name="sysop_pass2"></label>
        <label>Board timezone <select name="timezone">' . $tzOptions . '</select></label>
        <label>Base URL <input name="base_url" value="' . $v('base_url', detectBaseUrl()) . '">
          <span class="hint">auto-detected; fix it if your board lives elsewhere</span></label>
        <button class="btn" type="submit">Install →</button>
      </form>');
}

function runInstall(): never
{
    // Server-side re-validation of everything (§6 step 4).
    [, $hardFail] = envChecks();
    if ($hardFail) {
        renderStep1();
    }
    $driver = ($_POST['db_driver'] ?? 'sqlite') === 'mysql' ? 'mysql' : 'sqlite';

    $errors = [];
    $boardName = trim((string)($_POST['board_name'] ?? ''));
    $handle = trim((string)($_POST['sysop_handle'] ?? ''));
    $pass = (string)($_POST['sysop_pass'] ?? '');
    $pass2 = (string)($_POST['sysop_pass2'] ?? '');
    $timezone = (string)($_POST['timezone'] ?? 'UTC');
    $baseUrl = trim((string)($_POST['base_url'] ?? ''));

    if ($boardName === '' || mb_strlen($boardName) > 60) {
        $errors[] = 'Board name must be 1-60 characters.';
    }
    if (!preg_match('/^[A-Za-z0-9 ._\[\]-]{3,20}$/', $handle)) {
        $errors[] = 'Sysop handle must be 3-20 characters of letters, digits, space, . _ - [ ]';
    }
    if (strlen($pass) < 8) {
        $errors[] = 'Sysop password must be at least 8 characters.';
    }
    if ($pass !== $pass2) {
        $errors[] = 'The two passwords do not match.';
    }
    if (!in_array($timezone, \DateTimeZone::listIdentifiers(), true)) {
        $errors[] = 'Pick a valid timezone.';
    }
    if (!preg_match('#^https?://[^\s]+$#', $baseUrl)) {
        $errors[] = 'Base URL must start with http:// or https://';
    } else {
        $baseUrl = rtrim($baseUrl, '/') . '/';
    }
    if ($errors) {
        renderStep3($errors);
    }

    $t0 = microtime(true);
    $dataDir = ATDT_ROOT . '/data';
    foreach (['', '/logs', '/locks', '/doorstore', '/themes', '/art'] as $sub) {
        if (!is_dir($dataDir . $sub) && !@mkdir($dataDir . $sub, 0755, true)) {
            page('Install failed', '<h1>Install failed</h1><p class="bad">Could not create <code>data' . h($sub) . '/</code>.
                  Check directory permissions and reload.</p>');
        }
    }
    @file_put_contents($dataDir . '/.htaccess',
        "# ATDT BBS System data directory - no direct web access.\n" .
        "<IfModule mod_authz_core.c>\n  Require all denied\n</IfModule>\n" .
        "<IfModule !mod_authz_core.c>\n  Deny from all\n</IfModule>\n");

    // Open the database; wipe any half-finished previous attempt (unlocked ⇒ never completed).
    $dbCfg = ['driver' => $driver];
    try {
        if ($driver === 'mysql') {
            $db = mysqlFromPost();
            Schema::dropAll($db);
            $dbCfg['mysql'] = [
                'host' => trim((string)($_POST['my_host'] ?? 'localhost')),
                'port' => (int)($_POST['my_port'] ?? 3306),
                'name' => trim((string)($_POST['my_name'] ?? '')),
                'user' => (string)($_POST['my_user'] ?? ''),
                'pass' => (string)($_POST['my_pass'] ?? ''),
            ];
        } else {
            foreach (glob($dataDir . '/atdt_*.sqlite*') ?: [] as $stale) {
                @unlink($stale);
            }
            // §19.6 belt-and-suspenders: randomized filename.
            $dbCfg['sqlite_file'] = 'atdt_' . bin2hex(random_bytes(4)) . '.sqlite';
            $db = Db::sqlite($dataDir . '/' . $dbCfg['sqlite_file']);
        }

        $cronKey = bin2hex(random_bytes(16)); // §6: random 32-hex
        Schema::create($db);
        Schema::seed($db, [
            'board_name' => $boardName,
            'sysop_handle' => $handle,
            'sysop_pass' => $pass,
            'timezone' => $timezone,
            'base_url' => $baseUrl,
            'cron_key' => $cronKey,
        ]);
    } catch (\Throwable $e) {
        error_log('[atdt:install] ' . $e->getMessage());
        $msg = 'Database step failed: ' . sanitizeDbError($e->getMessage());
        if ($driver === 'mysql') {
            renderStep2($msg);
        }
        page('Install failed', '<h1>Install failed</h1><p class="bad">' . h($msg) . '</p>
              <p class="hint">Fix the issue and reload; the installer restarts cleanly.</p>');
    }

    // Config last: its install_lock is what marks the install complete (§6).
    $config = [
        'db' => $dbCfg,
        'base_url' => $baseUrl,
        'cron_key' => $cronKey,
        'install_lock' => bin2hex(random_bytes(16)),
        'installed_at' => now_utc(),
    ];
    $php = "<?php\n\n// ATDT configuration - generated by install.php. Keep this file private.\n\nreturn " .
        var_export($config, true) . ";\n";
    if (@file_put_contents($dataDir . '/config.php', $php) === false) {
        page('Install failed', '<h1>Install failed</h1><p class="bad">Could not write <code>data/config.php</code>.</p>');
    }
    @chmod($dataDir . '/config.php', 0600);

    $elapsed = microtime(true) - $t0;
    renderDone($baseUrl, $cronKey, $elapsed);
}

function renderDone(string $baseUrl, string $cronKey, float $elapsed): never
{
    $cronLine = '*/5 * * * * php ' . (realpath(__DIR__ . '/cron.php') ?: __DIR__ . '/cron.php') . ' key=' . $cronKey;
    page('Installed', '
      <h1 id="install-done">ATDT is installed <span class="step">done in ' . h(sprintf('%.1f', $elapsed)) . 's</span></h1>
      <p class="ok">✅ Schema created, defaults seeded, sysop account ready.</p>
      <table class="kv">
        <tr><th>Caller URL</th><td><a href="' . h($baseUrl) . '">' . h($baseUrl) . '</a></td></tr>
        <tr><th>Sysop Console</th><td><a href="' . h($baseUrl) . 'admin/">' . h($baseUrl) . 'admin/</a>
            <span class="hint">log in with the sysop handle and password you just chose</span></td></tr>
      </table>
      <h2>Scheduling</h2>
      <p>Paste this line into cPanel → Cron Jobs:</p>
      <pre class="cronline">' . h($cronLine) . '</pre>
      <p class="hint">No cron access? Skip it; ATDT\'s pseudo-cron runs housekeeping
         piggybacked on caller traffic. Real cron is only about promptness.</p>
      <p><a class="btn" href="index.php">Dial the board →</a></p>
      <p class="hint">This installer has locked itself; you may delete <code>install.php</code>.</p>');
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** @return array{0: list<array{0:string,1:?bool,2:string}>, 1: bool} */
function envChecks(): array
{
    $checks = [];
    $hardFail = false;
    $add = function (string $label, ?bool $state, string $hint = '') use (&$checks, &$hardFail): void {
        $checks[] = [$label, $state, $hint];
        if ($state === false) {
            $hardFail = true;
        }
    };

    $add('PHP 8.1 or newer (running ' . PHP_VERSION . ')', PHP_VERSION_ID >= 80100,
        PHP_VERSION_ID >= 80100 ? '' : 'Ask your host to switch this site to PHP 8.1+ (cPanel → MultiPHP Manager).');
    foreach (['pdo', 'mbstring', 'json', 'openssl', 'session'] as $ext) {
        $add("PHP extension: $ext", extension_loaded($ext),
            extension_loaded($ext) ? '' : "Ask your host to enable the '$ext' PHP extension.");
    }
    $sqlite = extension_loaded('pdo_sqlite');
    $mysql = extension_loaded('pdo_mysql');
    $add('PHP extension: pdo_sqlite', $sqlite ? true : ($mysql ? null : false),
        $sqlite ? 'Default database engine.' : 'Missing; you must use MySQL instead.');
    $add('PHP extension: pdo_mysql', $mysql ? true : ($sqlite ? null : false),
        $mysql ? 'Available if you prefer MySQL.' : 'Missing; only needed if you pick MySQL.');
    if (!$sqlite && !$mysql) {
        $add('At least one database driver', false, 'Neither pdo_sqlite nor pdo_mysql is loaded.');
    }

    $dataDir = ATDT_ROOT . '/data';
    $writable = is_dir($dataDir) ? is_writable($dataDir) : @mkdir($dataDir, 0755, true);
    if ($writable && is_dir($dataDir)) {
        $probe = $dataDir . '/.write_probe';
        $writable = @file_put_contents($probe, 'ok') !== false;
        @unlink($probe);
    }
    $add('data/ directory is creatable and writable', (bool)$writable,
        $writable ? 'All board data lives here.' : 'Create a data/ directory next to install.php and make it writable (755/775).');

    $add('allow_url_fopen', ini_get('allow_url_fopen') ? true : null,
        ini_get('allow_url_fopen') ? '' : 'Off; fine for v1, nothing uses it yet.');

    return [$checks, $hardFail];
}

function detectBaseUrl(): string
{
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
    $host = preg_replace('/[^A-Za-z0-9.\-:\[\]]/', '', (string)($_SERVER['HTTP_HOST'] ?? 'localhost'));
    $dir = rtrim(str_replace('\\', '/', dirname((string)($_SERVER['SCRIPT_NAME'] ?? '/'))), '/');
    return ($https ? 'https' : 'http') . '://' . $host . $dir . '/';
}

function mysqlFromPost(): Db
{
    if (!extension_loaded('pdo_mysql')) {
        throw new \RuntimeException('The pdo_mysql extension is not loaded.');
    }
    return Db::mysql(
        trim((string)($_POST['my_host'] ?? 'localhost')),
        (int)($_POST['my_port'] ?? 3306),
        trim((string)($_POST['my_name'] ?? '')),
        (string)($_POST['my_user'] ?? ''),
        (string)($_POST['my_pass'] ?? '')
    );
}

/** Keep DB error text useful but free of credentials. */
function sanitizeDbError(string $msg): string
{
    return mb_substr(preg_replace('/using password:\s*\S+/i', '', $msg) ?? $msg, 0, 300);
}

function carryDbFields(): string
{
    $out = '';
    foreach (['db_driver', 'my_host', 'my_port', 'my_name', 'my_user', 'my_pass'] as $k) {
        $out .= '<input type="hidden" name="' . h($k) . '" value="' . h((string)($_POST[$k] ?? '')) . '">';
    }
    return $out;
}

function page(string $title, string $body): never
{
    header('Content-Type: text/html; charset=utf-8');
    echo '<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>' . h($title) . ' - ATDT installer</title>
<link rel="stylesheet" href="assets/admin.css"></head>
<body class="installer"><main>' . $body . '</main>
<footer>ATDT ' . h(ATDT_VERSION) . ' installer</footer></body></html>';
    exit;
}
