<?php

declare(strict_types=1);

/**
 * PPPHP standalone installer — shipped in the build root when the Output module's
 * "Include standalone installer" option is on. Self-contained: uses raw PDO, needs
 * no bootstrap and no pre-existing config/database.php. It:
 *
 *   1. tests + saves the database connection  → writes config/database.php
 *   2. creates the schema and loads seed data  → imports database.sql
 *   3. creates the first administrator account → hashed, into the auth table
 *   4. self-locks (config/installed.lock) so it can never be re-run to wipe data
 *
 * Tokens {{SLUG}} / {{APP_TITLE}} / {{GENERATED_AT}} are filled at build time.
 * No closing PHP tag anywhere on purpose.
 */

error_reporting(E_ALL & ~E_DEPRECATED);
@ini_set('display_errors', '1');
@ini_set('display_startup_errors', '1');

/* ── Never fail silently ──────────────────────────────────────────────────
 * Many shared hosts lock `display_errors` Off at the server level, so a PHP
 * fatal there produces a blank HTTP 500 with no clue. This shutdown handler
 * catches such fatals and prints a readable message instead. (A *parse* error
 * on a PHP older than this file supports is the one case it can't reach — the
 * explicit version check below guards that, and the file avoids >7.1 syntax.) */
function pp_fatal_screen($title, $detail)
{
    if (!headers_sent()) { http_response_code(500); header('Content-Type: text/html; charset=utf-8'); }
    echo '<!doctype html><meta charset="utf-8"><title>Install error</title>'
       . '<div style="max-width:640px;margin:60px auto;font:15px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:#0f172a">'
       . '<h1 style="font-size:20px;margin:0 0 8px">Installer could not start</h1>'
       . '<p style="color:#b91c1c;font-weight:600">' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</p>'
       . '<pre style="background:#f1f5f9;padding:12px 14px;border-radius:8px;white-space:pre-wrap;overflow:auto">'
       . htmlspecialchars($detail, ENT_QUOTES, 'UTF-8') . '</pre>'
       . '<p style="color:#64748b">Fix the item above and reload. If your host runs PHP as FPM/FastCGI, its own error log has the full trace.</p></div>';
}
register_shutdown_function(function () {
    $e = error_get_last();
    if ($e && in_array($e['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR), true)) {
        pp_fatal_screen('PHP fatal error', $e['message'] . "\n\nin " . $e['file'] . ':' . $e['line']);
    }
});

/* ── Preconditions: report the cause plainly instead of a blank 500. ── */
if (PHP_VERSION_ID < 70100) {
    pp_fatal_screen('PHP ' . PHP_VERSION . ' is too old',
        "This installer needs PHP 7.1 or newer (7.4+ recommended). Your server is running PHP " . PHP_VERSION . ".\n"
        . "In cPanel/Plesk open “Select PHP Version” (or ask your host) and switch this site to PHP 8.1+, then reload.");
    exit;
}
if (!extension_loaded('pdo_mysql')) {
    pp_fatal_screen('The pdo_mysql extension is not enabled',
        "This app connects to MySQL/MariaDB and needs the PDO MySQL driver.\n"
        . "Enable “pdo_mysql” (and “pdo”) in your host's PHP extensions panel, then reload.");
    exit;
}

if (session_status() === PHP_SESSION_NONE) {
    @session_start();
}

const APP_SLUG  = '{{SLUG}}';
const APP_TITLE = '{{APP_TITLE}}';

// The app's page <head> (mobile stylesheet, manifest, icons) is injected into this
// installer too and references APP_BASE — the public URL prefix of the app root. The
// installer IS at that root, so it's just the directory of this script (e.g. ""
// at the web root, or "/community" in a subfolder). Defined here so those links (and
// anything else the shared head emits) resolve instead of fatalling.
if (!defined('APP_BASE')) {
    if (PHP_SAPI === 'cli') {
        define('APP_BASE', '');
    } else {
        $__ab = str_replace('\\', '/', dirname((string) ($_SERVER['SCRIPT_NAME'] ?? '')));
        define('APP_BASE', rtrim(($__ab === '.' || $__ab === '/') ? '' : $__ab, '/'));
        unset($__ab);
    }
}

$ROOT   = __DIR__;
$CONFIG = $ROOT . '/config';
$DBCFG  = $CONFIG . '/database.php';
$LOCK   = $CONFIG . '/installed.lock';
$SQL    = $ROOT . '/database.sql';   // combined (fallback)
$SCHEMA = $ROOT . '/schema.sql';     // structure: tables + views
$SEED   = $ROOT . '/seed.sql';       // data

/* ── One-time CSRF token for the wizard's POSTs ── */
if (empty($_SESSION['pp_install_csrf'])) {
    $_SESSION['pp_install_csrf'] = bin2hex(random_bytes(16));
}
$CSRF = $_SESSION['pp_install_csrf'];
function csrf_ok(): bool
{
    return isset($_POST['csrf'], $_SESSION['pp_install_csrf'])
        && hash_equals((string) $_SESSION['pp_install_csrf'], (string) $_POST['csrf']);
}

/* ─────────────────────────  helpers  ───────────────────────── */

function pp_json($data): void
{
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data);
    exit;
}

function pp_ident(string $v, string $fallback): string
{
    $v = preg_replace('/[^A-Za-z0-9_]/', '', $v) ?? '';
    return $v !== '' ? $v : $fallback;
}

/** Build a PDO connection from a posted/loaded profile. */
function pp_pdo(array $c, bool $withDb = true): PDO
{
    $host    = (string) ($c['host'] ?? '127.0.0.1');
    $port    = (string) ($c['port'] ?? '3306');
    $db      = (string) ($c['dbname'] ?? '');
    $charset = (string) ($c['charset'] ?? 'utf8mb4');

    $dsn = "mysql:host={$host};port={$port};charset={$charset}";
    if ($withDb && $db !== '') {
        $dsn = "mysql:host={$host};port={$port};dbname={$db};charset={$charset}";
    }
    return new PDO($dsn, (string) ($c['user'] ?? ''), (string) ($c['pass'] ?? ''), [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_TIMEOUT            => 8,
    ]);
}

/** Read the posted connection fields into a normalised profile. */
function pp_profile_from_post(): array
{
    return [
        'type'    => 'mysql',
        'driver'  => 'mysql',
        'host'    => trim((string) ($_POST['host'] ?? '127.0.0.1')),
        'port'    => trim((string) ($_POST['port'] ?? '3306')) ?: '3306',
        'dbname'  => trim((string) ($_POST['dbname'] ?? '')),
        'user'    => (string) ($_POST['user'] ?? ''),
        'pass'    => (string) ($_POST['pass'] ?? ''),
        'charset' => 'utf8mb4',
    ];
}

/** Split a SQL dump into individual statements (quote/comment aware). */
function pp_split_sql(string $sql): array
{
    $stmts = [];
    $buf   = '';
    $len   = strlen($sql);
    $inStr = false;
    $q     = '';
    for ($i = 0; $i < $len; $i++) {
        $ch   = $sql[$i];
        $next = $i + 1 < $len ? $sql[$i + 1] : '';
        if ($inStr) {
            $buf .= $ch;
            if ($ch === '\\' && $next !== '') { $buf .= $next; $i++; continue; }
            if ($ch === $q) {
                if ($next === $q) { $buf .= $next; $i++; continue; } // doubled = escaped
                $inStr = false;
            }
            continue;
        }
        if ($ch === '-' && $next === '-') {                 // line comment
            $eol = strpos($sql, "\n", $i);
            if ($eol === false) break;
            $i = $eol;
            continue;
        }
        if ($ch === '/' && $next === '*') {                 // block comment
            $end = strpos($sql, '*/', $i + 2);
            if ($end === false) break;
            $i = $end + 1;
            continue;
        }
        if ($ch === "'" || $ch === '"') { $inStr = true; $q = $ch; $buf .= $ch; continue; }
        if ($ch === ';') { $t = trim($buf); if ($t !== '') $stmts[] = $t; $buf = ''; continue; }
        $buf .= $ch;
    }
    $t = trim($buf);
    if ($t !== '') $stmts[] = $t;
    return $stmts;
}

/** Auth table + column map — read what the build already emitted. */
function pp_auth_map(string $configDir): array
{
    $defaults = [
        'table'   => 'users',
        'columns' => [
            'id' => 'id', 'username' => 'username', 'email' => 'email',
            'password' => 'password', 'fullname' => 'fullname',
            'role' => 'role', 'status' => 'status',
        ],
    ];
    $f = $configDir . '/auth.php';
    if (is_file($f)) {
        $a = @require $f;
        if (is_array($a) && !empty($a['table'])) {
            return [
                'table'   => pp_ident((string) $a['table'], 'users'),
                'columns' => array_merge($defaults['columns'], (array) ($a['columns'] ?? [])),
            ];
        }
    }
    return $defaults;
}

$STATE = ['done' => is_file($LOCK)];

/* ─────────────────────────  actions  ───────────────────────── */

$action = (string) ($_POST['action'] ?? $_GET['action'] ?? '');

// Everything is blocked once installed — the lock is the guard against a
// re-run wiping a live database.
if ($STATE['done'] && $action !== 'delete-self') {
    // fall through to render the "already installed" screen
    $action = '';
}

if ($action === 'test') {
    if (!csrf_ok()) pp_json(['ok' => false, 'message' => 'Session expired — reload the page.']);
    $c      = pp_profile_from_post();
    $create = !empty($_POST['createdb']);
    if ($c['host'] === '' || $c['user'] === '') pp_json(['ok' => false, 'message' => 'Host and username are required.']);
    try {
        pp_pdo($c, true);
        pp_json(['ok' => true, 'message' => 'Connected to “' . $c['dbname'] . '”. Ready to install.']);
    } catch (Throwable $e) {
        // If the database itself is missing but the server login works, and the
        // operator opted to create it, that's still a green light.
        if ($create) {
            try {
                pp_pdo($c, false);
                pp_json(['ok' => true, 'message' => 'Server login OK — the database “' . $c['dbname'] . '” will be created during install.']);
            } catch (Throwable $e2) {
                pp_json(['ok' => false, 'message' => 'Server login failed: ' . $e2->getMessage()]);
            }
        }
        pp_json(['ok' => false, 'message' => 'Connection failed: ' . $e->getMessage()]);
    }
}

$errors = [];

if ($action === 'install' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!csrf_ok()) {
        $errors[] = 'Your session expired. Please reload and try again.';
    } elseif (!is_writable($CONFIG)) {
        $errors[] = 'The config/ folder is not writable. Give it write permission (e.g. 0755) and retry.';
    } elseif (!is_file($SCHEMA) && !is_file($SQL)) {
        $errors[] = 'schema.sql / database.sql is missing from this folder — re-upload the complete build.';
    } else {
        $c      = pp_profile_from_post();
        $create = !empty($_POST['createdb']);
        try {
            if ($create && $c['dbname'] !== '') {
                $root = pp_pdo($c, false);
                $dbn  = str_replace('`', '', $c['dbname']);
                $root->exec("CREATE DATABASE IF NOT EXISTS `{$dbn}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
            }
            $pdo = pp_pdo($c, true);

            // Import structure then data. Prefer the split schema.sql + seed.sql so
            // views are created before rows depend on them; fall back to the
            // combined database.sql. CREATE DATABASE / USE are handled by us via the
            // DSN, so they're stripped. Any DEFINER a dump might carry is stripped
            // too (belt-and-suspenders — it breaks on a server without that user).
            $files = (is_file($SCHEMA) ? [$SCHEMA] : []);
            if (is_file($SEED)) $files[] = $SEED;
            if (!$files) $files = [$SQL];

            $ran = 0; $failed = [];
            $pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
            foreach ($files as $f) {
                if (!is_file($f)) continue;
                foreach (pp_split_sql((string) file_get_contents($f)) as $stmt) {
                    if (preg_match('/^\s*CREATE\s+DATABASE/i', $stmt)) continue;
                    if (preg_match('/^\s*USE\s+/i', $stmt)) continue;
                    $stmt = (string) preg_replace('/\s*DEFINER\s*=\s*(`[^`]*`|\'[^\']*\'|[^\s]+)@(`[^`]*`|\'[^\']*\'|[^\s]+)/i', '', $stmt);
                    try { $pdo->exec($stmt); $ran++; }
                    catch (Throwable $e) {
                        // Duplicate-key on a re-run is benign; record everything else.
                        if (stripos($e->getMessage(), 'Duplicate entry') === false) {
                            $failed[] = preg_replace('/\s+/', ' ', substr($stmt, 0, 70)) . ' — ' . $e->getMessage();
                        }
                    }
                }
            }
            $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');

            // Post-import summary shown on the final screen.
            $tblN = (int) ($pdo->query("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'")->fetchColumn() ?: 0);
            $viwN = (int) ($pdo->query("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'VIEW'")->fetchColumn() ?: 0);
            $_SESSION['pp_install_summary'] = ['statements' => $ran, 'tables' => $tblN, 'views' => $viwN, 'failed' => array_slice($failed, 0, 10)];
            if ($tblN === 0) {
                $errors[] = 'No tables were created — the import failed. First error: ' . ($failed[0] ?? 'unknown');
            }

            if (!$errors) {
            // Persist the connection the app will use.
            $profile = [
                'type' => 'mysql', 'driver' => 'mysql',
                'host' => $c['host'], 'port' => $c['port'], 'dbname' => $c['dbname'],
                'path' => 'database/app.sqlite',
                'user' => $c['user'], 'pass' => $c['pass'],
                'schema' => '', 'service' => '', 'charset' => 'utf8mb4',
                'ssl' => ['enabled' => false, 'mode' => 'prefer', 'ca' => '', 'encrypt' => true, 'trust_server_certificate' => false],
            ];
            file_put_contents($DBCFG, "<?php\n\ndeclare(strict_types=1);\n\nreturn " . var_export($profile, true) . ";\n");

            header('Location: ' . basename(__FILE__) . '?step=admin');
            exit;
            }
        } catch (Throwable $e) {
            $errors[] = 'Install failed: ' . $e->getMessage();
        }
    }
}

if ($action === 'admin' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!csrf_ok()) {
        $errors[] = 'Your session expired. Please reload and try again.';
    } elseif (!is_file($DBCFG)) {
        $errors[] = 'Database is not configured yet — complete step 1 first.';
    } else {
        $username = trim((string) ($_POST['admin_user'] ?? ''));
        $email    = trim((string) ($_POST['admin_email'] ?? ''));
        $pass     = (string) ($_POST['admin_pass'] ?? '');
        if ($username === '' || $email === '' || strlen($pass) < 8) {
            $errors[] = 'Enter a username, a valid email, and a password of at least 8 characters.';
        } else {
            try {
                $cfg  = require $DBCFG;
                $pdo  = pp_pdo($cfg, true);
                $map  = pp_auth_map($CONFIG);
                $T    = pp_ident($map['table'], 'users');
                $col  = function (string $k, string $d) use ($map): string { return pp_ident((string) ($map['columns'][$k] ?? $d), $d); };
                $cU = $col('username', 'username'); $cE = $col('email', 'email');
                $cP = $col('password', 'password'); $cR = $col('role', 'role');
                $cS = $col('status', 'status');     $cI = $col('id', 'id');
                $hash = password_hash($pass, PASSWORD_BCRYPT, ['cost' => 12]);

                $find = $pdo->prepare("SELECT `{$cI}` FROM `{$T}` WHERE `{$cU}` = ? OR `{$cE}` = ? LIMIT 1");
                $find->execute([$username, $email]);
                $id = $find->fetchColumn();

                if ($id !== false) {
                    $pdo->prepare("UPDATE `{$T}` SET `{$cU}` = ?, `{$cE}` = ?, `{$cP}` = ?, `{$cR}` = 'superadmin', `{$cS}` = 'active' WHERE `{$cI}` = ?")
                        ->execute([$username, $email, $hash, $id]);
                } else {
                    $pdo->prepare("INSERT INTO `{$T}` (`{$cU}`, `{$cE}`, `{$cP}`, `{$cR}`, `{$cS}`) VALUES (?, ?, ?, 'superadmin', 'active')")
                        ->execute([$username, $email, $hash]);
                }

                file_put_contents($LOCK, date('c') . " installed \"" . APP_SLUG . "\"\n");
                unset($_SESSION['pp_install_csrf']);
                header('Location: ' . basename(__FILE__) . '?step=done');
                exit;
            } catch (Throwable $e) {
                $errors[] = 'Could not create the administrator: ' . $e->getMessage();
            }
        }
    }
}

if ($action === 'delete-self' && $_SERVER['REQUEST_METHOD'] === 'POST' && csrf_ok()) {
    @unlink(__FILE__);
    header('Location: ./');
    exit;
}

/* Decide which screen to show. */
$step = 'db';
if (is_file($LOCK)) {
    $step = 'locked';
} elseif (($_GET['step'] ?? '') === 'done') {
    $step = 'done';
} elseif (($_GET['step'] ?? '') === 'admin' || is_file($DBCFG)) {
    $step = 'admin';
}

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

/* ─────────────────────────  view  ───────────────────────── */
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>Install · <?= $h(APP_TITLE) ?></title>
<style>
  :root{
    --bg:#0f172a; --card:#ffffff; --ink:#0f172a; --muted:#64748b;
    --line:#e2e8f0; --brand:#4f46e5; --brand-d:#4338ca;
    --ok:#059669; --err:#dc2626; --soft:#f8fafc;
  }
  *{box-sizing:border-box}
  body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
    color:var(--ink);
    background:linear-gradient(135deg,#1e1b4b 0%,#0f172a 60%,#020617 100%);
    min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
  .wrap{width:100%;max-width:560px}
  .brand{display:flex;align-items:center;gap:10px;justify-content:center;margin-bottom:18px}
  .brand .mark{width:34px;height:34px;border-radius:9px;background:var(--brand);color:#fff;
    display:grid;place-items:center;font-weight:800;font-size:15px;letter-spacing:.3px;
    box-shadow:0 6px 18px rgba(79,70,229,.45)}
  .brand .name{color:#e2e8f0;font-weight:700;letter-spacing:.4px}
  .brand .name small{display:block;color:#94a3b8;font-weight:500;font-size:11px;letter-spacing:.6px;text-transform:uppercase}
  .card{background:var(--card);border-radius:16px;padding:28px;
    box-shadow:0 24px 60px rgba(2,6,23,.5);border:1px solid rgba(255,255,255,.06)}
  h1{font-size:20px;margin:0 0 4px}
  .sub{color:var(--muted);font-size:13.5px;margin:0 0 20px}
  .steps{display:flex;gap:8px;margin:0 0 22px;font-size:12px}
  .steps div{flex:1;text-align:center;padding:8px 4px;border-radius:9px;background:var(--soft);
    color:var(--muted);border:1px solid var(--line);font-weight:600}
  .steps div.on{background:var(--brand);color:#fff;border-color:var(--brand)}
  .steps div.ok{background:#ecfdf5;color:var(--ok);border-color:#a7f3d0}
  label{display:block;font-size:12.5px;font-weight:600;margin:14px 0 6px}
  .row{display:flex;gap:12px}
  .row > div{flex:1}
  input[type=text],input[type=password],input[type=email],input[type=number]{
    width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:10px;font-size:14px;
    background:#fff;color:var(--ink);transition:border-color .15s,box-shadow .15s}
  input:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px rgba(79,70,229,.15)}
  .check{display:flex;align-items:center;gap:8px;margin-top:14px;font-size:13px;color:var(--muted)}
  .check input{width:16px;height:16px}
  .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;cursor:pointer;
    border:0;border-radius:10px;font-size:14px;font-weight:600;padding:12px 18px}
  .btn-primary{background:var(--brand);color:#fff;width:100%}
  .btn-primary:hover{background:var(--brand-d)}
  .btn-ghost{background:var(--soft);color:var(--ink);border:1px solid var(--line)}
  .actions{display:flex;gap:10px;margin-top:22px}
  .actions .btn{flex:1}
  .hint{font-size:12px;color:var(--muted);margin-top:6px}
  .msg{margin-top:14px;padding:11px 13px;border-radius:10px;font-size:13px;display:none}
  .msg.ok{display:block;background:#ecfdf5;color:var(--ok);border:1px solid #a7f3d0}
  .msg.err{display:block;background:#fef2f2;color:var(--err);border:1px solid #fecaca}
  .alert{background:#fef2f2;color:var(--err);border:1px solid #fecaca;border-radius:10px;
    padding:11px 13px;font-size:13px;margin-bottom:16px}
  .done-ico{width:56px;height:56px;border-radius:50%;background:#ecfdf5;color:var(--ok);
    display:grid;place-items:center;margin:4px auto 14px;font-size:28px;border:1px solid #a7f3d0}
  .center{text-align:center}
  code{background:var(--soft);border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-size:12.5px}
  a.link{color:var(--brand);text-decoration:none;font-weight:600}
</style>
</head>
<body>
<div class="wrap">
  <div class="brand">
    <span class="mark">PP</span>
    <span class="name">PPPHP<small>Installer</small></span>
  </div>
  <div class="card">
<?php if ($step === 'locked'): ?>
    <div class="center">
      <div class="done-ico">&#128274;</div>
      <h1>Already installed</h1>
      <p class="sub"><?= $h(APP_TITLE) ?> is set up on this server. The installer is locked so it can't overwrite your data.</p>
      <a class="btn btn-primary" href="./" style="text-decoration:none">Open the app &rarr;</a>
      <p class="hint" style="margin-top:16px">To reinstall from scratch, delete <code>config/installed.lock</code> and <code>config/database.php</code> first.</p>
    </div>

<?php elseif ($step === 'done'): ?>
    <div class="steps">
      <div class="ok">1 &middot; Database</div><div class="ok">2 &middot; Admin</div><div class="on">3 &middot; Done</div>
    </div>
    <div class="center">
      <div class="done-ico">&#10003;</div>
      <h1>You're all set</h1>
      <p class="sub"><?= $h(APP_TITLE) ?> is installed and ready. For security, remove the installer now.</p>
    </div>
    <?php $sum = $_SESSION['pp_install_summary'] ?? null; if ($sum): ?>
      <div class="alert" style="background:#f0fdf4;border-color:#bbf7d0;color:#166534">
        Imported <b><?= (int) $sum['tables'] ?></b> table(s) and <b><?= (int) $sum['views'] ?></b> view(s)
        (<?= (int) $sum['statements'] ?> statements) + created your administrator.
        <?php if (!empty($sum['failed'])): ?>
          <br><span style="color:#b45309">Note — <?= count($sum['failed']) ?> statement(s) reported a warning:</span>
          <ul style="margin:6px 0 0 16px;font-size:12px"><?php foreach ($sum['failed'] as $fm): ?><li><?= $h($fm) ?></li><?php endforeach; ?></ul>
        <?php endif; ?>
      </div>
    <?php endif; unset($_SESSION['pp_install_summary']); ?>
    <form method="post" onsubmit="return confirm('Delete install.php from the server?')">
      <input type="hidden" name="csrf" value="<?= $h($CSRF) ?>">
      <input type="hidden" name="action" value="delete-self">
      <div class="actions">
        <a class="btn btn-primary" href="./" style="text-decoration:none">Open the app &rarr;</a>
        <button class="btn btn-ghost" type="submit">Delete installer</button>
      </div>
    </form>
    <p class="hint" style="margin-top:16px">If the button can't delete it (permissions), remove <code>install.php</code> manually.</p>

<?php elseif ($step === 'admin'): ?>
    <div class="steps">
      <div class="ok">1 &middot; Database</div><div class="on">2 &middot; Admin</div><div>3 &middot; Done</div>
    </div>
    <h1>Create your administrator</h1>
    <p class="sub">This is the first account you'll sign in with. Choose a strong password.</p>
    <?php foreach ($errors as $e): ?><div class="alert"><?= $h($e) ?></div><?php endforeach; ?>
    <form method="post">
      <input type="hidden" name="csrf" value="<?= $h($CSRF) ?>">
      <input type="hidden" name="action" value="admin">
      <label>Username</label>
      <input type="text" name="admin_user" value="<?= $h($_POST['admin_user'] ?? 'admin') ?>" autocomplete="username" required>
      <label>Email</label>
      <input type="email" name="admin_email" value="<?= $h($_POST['admin_email'] ?? '') ?>" autocomplete="email" required>
      <label>Password</label>
      <input type="password" name="admin_pass" autocomplete="new-password" minlength="8" required>
      <p class="hint">At least 8 characters. You can change it later from the app.</p>
      <div class="actions">
        <button class="btn btn-primary" type="submit">Finish install &rarr;</button>
      </div>
    </form>

<?php else: /* db */ ?>
    <div class="steps">
      <div class="on">1 &middot; Database</div><div>2 &middot; Admin</div><div>3 &middot; Done</div>
    </div>
    <h1>Connect your database</h1>
    <p class="sub">Enter your MySQL / MariaDB connection details. The installer creates the tables and loads the starter data.</p>
    <?php foreach ($errors as $e): ?><div class="alert"><?= $h($e) ?></div><?php endforeach; ?>
    <form method="post" id="dbform">
      <input type="hidden" name="csrf" value="<?= $h($CSRF) ?>">
      <input type="hidden" name="action" value="install">
      <div class="row">
        <div>
          <label>Host</label>
          <input type="text" name="host" id="f_host" value="<?= $h($_POST['host'] ?? 'localhost') ?>" required>
        </div>
        <div style="max-width:120px">
          <label>Port</label>
          <input type="number" name="port" id="f_port" value="<?= $h($_POST['port'] ?? '3306') ?>">
        </div>
      </div>
      <label>Database name</label>
      <input type="text" name="dbname" id="f_dbname" value="<?= $h($_POST['dbname'] ?? APP_SLUG) ?>" required>
      <div class="row">
        <div>
          <label>Username</label>
          <input type="text" name="user" id="f_user" value="<?= $h($_POST['user'] ?? '') ?>" autocomplete="off" required>
        </div>
        <div>
          <label>Password</label>
          <input type="password" name="pass" id="f_pass" autocomplete="off">
        </div>
      </div>
      <label class="check"><input type="checkbox" name="createdb" id="f_createdb"> Create the database if it doesn't exist (needs a privileged DB user)</label>
      <div id="msg" class="msg"></div>
      <div class="actions">
        <button class="btn btn-ghost" type="button" id="btnTest">Test connection</button>
        <button class="btn btn-primary" type="submit" id="btnInstall">Install &rarr;</button>
      </div>
      <p class="hint">On shared hosting, create the database in your control panel first, then leave the box unchecked.</p>
    </form>
<?php endif; ?>
  </div>
</div>
<script>
(function(){
  var btn = document.getElementById('btnTest');
  if(!btn) return;
  var msg = document.getElementById('msg');
  btn.addEventListener('click', function(){
    var f = document.getElementById('dbform');
    var body = new URLSearchParams();
    body.set('action','test');
    body.set('csrf', f.querySelector('[name=csrf]').value);
    ['host','port','dbname','user','pass'].forEach(function(k){
      body.set(k, (f.querySelector('[name="'+k+'"]')||{}).value || '');
    });
    body.set('createdb', document.getElementById('f_createdb').checked ? '1' : '');
    msg.className='msg'; msg.textContent=''; btn.disabled=true; var old=btn.textContent; btn.textContent='Testing…';
    fetch(location.pathname, {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:body.toString()})
      .then(function(r){return r.json();})
      .then(function(j){ msg.className='msg '+(j.ok?'ok':'err'); msg.textContent=j.message||''; })
      .catch(function(){ msg.className='msg err'; msg.textContent='Test request failed.'; })
      .finally(function(){ btn.disabled=false; btn.textContent=old; });
  });
})();
</script>
</body>
</html>
