<?php

declare(strict_types=1);

// ══════════════════════════════════════════════════════════════════════════════
//  Genv2 - Web Installer v2.0
//  Single-file, zero external dependencies.
//  POST ?action=<name> to JSON   |   GET (no action) to full HTML wizard
// ══════════════════════════════════════════════════════════════════════════════

const INS_VER     = '2.0';
const INS_LOCK    = __DIR__ . '/install.lock';
const INS_ENV     = __DIR__ . '/.env';
const INS_SCHEMA  = __DIR__ . '/schema.sql';
const INS_ENVLIB  = __DIR__ . '/env.php';
const INS_AUTH    = __DIR__ . '/statistics/report_auth.php';

ini_set('display_errors', '0');
ini_set('log_errors', '1');
error_reporting(E_ALL);

// ── Shared libs (best-effort; installer works without them) ───────────────────
if (is_file(INS_ENVLIB)) {
    require_once INS_ENVLIB;
}

// ── Lock check ────────────────────────────────────────────────────────────────
// Once installed, the web installer is fully locked. Reinstall must be a deliberate
// filesystem operation: remove install.lock locally, then run the installer again.
$action = trim((string) ($_GET['action'] ?? ''));
if (is_file(INS_LOCK)) {
    if ($action !== '') {
        http_response_code(403);
        header('Content-Type: application/json; charset=UTF-8');
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
        header('X-Content-Type-Options: nosniff');
        echo json_encode(['ok' => false, 'error' => 'Installer is locked. Remove install.lock locally to reinstall.']);
    } else {
        ins_locked_page();
    }

    exit;
}

// ── Session ───────────────────────────────────────────────────────────────────
if (session_status() === PHP_SESSION_NONE) {
    session_set_cookie_params(['samesite' => 'Strict', 'httponly' => true, 'secure' => ins_is_https()]);
    session_start();
}
if (empty($_SESSION['ins_csrf_token']) || !is_string($_SESSION['ins_csrf_token'])) {
    $_SESSION['ins_csrf_token'] = bin2hex(random_bytes(32));
}

// ── AJAX dispatcher ───────────────────────────────────────────────────────────
if ($action !== '') {
    header('Content-Type: application/json; charset=UTF-8');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('X-Content-Type-Options: nosniff');
    header('Referrer-Policy: same-origin');
    header('Permissions-Policy: geolocation=(), microphone=(), camera=()');

    try {
        $readOnlyActions = ['detect' => true, 'detect_ip' => true];
        if (!isset($readOnlyActions[$action])) {
            if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
                throw new RuntimeException('Invalid request method');
            }
            $csrf = (string) ($_POST['csrf_token'] ?? '');
            if (!hash_equals((string) ($_SESSION['ins_csrf_token'] ?? ''), $csrf)) {
                throw new RuntimeException('Invalid CSRF token');
            }
        }
        match ($action) {
            'detect'        => ins_act_detect(),
            'test_db'       => ins_act_test_db(),
            'import_schema' => ins_act_import_schema(),
            'detect_ip'     => ins_act_detect_ip(),
            'detect_cpanel_host' => ins_act_detect_cpanel_host(),
            'provision_wildcard'  => ins_act_provision_wildcard(),
            'add_crons'           => ins_act_add_crons(),
            'finalize'      => ins_act_finalize(),
            default         => throw new RuntimeException('Unknown action'),
        };
    } catch (Throwable $e) {
        ins_json(['ok' => false, 'error' => ins_safe_error($e)]);
    }
    exit;
}

// ══════════════════════════════════════════════════════════════════════════════
//  ACTION HANDLERS
// ══════════════════════════════════════════════════════════════════════════════

// ── Step 1: System requirements detection ────────────────────────────────────
function ins_act_detect(): void
{
    $checks = [];

    // PHP version
    $phpOk = version_compare(PHP_VERSION, '8.3.0', '>=');
    $checks[] = ['id' => 'php', 'label' => 'PHP ' . PHP_VERSION, 'ok' => $phpOk, 'req' => true,
        'note' => $phpOk ? null : 'PHP 8.3+ required - current: ' . PHP_VERSION];

    // Extensions
    foreach (
        [
            'pdo_mysql' => ['PHP PDO MySQL',          true],
            'curl'      => ['cURL',                   true],
            'json'      => ['JSON',                   true],
            'mbstring'  => ['MBString',               true],
            'openssl'   => ['OpenSSL',                true],
            'gd'        => ['GD (image resizing)',    false],
            'zip'       => ['Zip',                    false],
            'maxminddb' => ['MaxMind DB (C ext.)',     false],
        ] as $ext => [$label, $req]
    ) {
        $ok = extension_loaded($ext);
        $checks[] = ['id' => $ext, 'label' => $label, 'ok' => $ok, 'req' => $req,
            'note' => !$ok ? ($req ? 'Install php-' . $ext : 'Optional - recommended for performance') : null];
    }

    // Filesystem
    $rootW = is_writable(__DIR__);
    $checks[] = ['id' => 'root_w', 'label' => 'Root dir writable (.env)', 'ok' => $rootW, 'req' => true,
        'note' => $rootW ? null : 'chmod 755 ' . __DIR__];

    $schemaOk = is_file(INS_SCHEMA);
    $checks[] = ['id' => 'schema', 'label' => 'schema.sql present', 'ok' => $schemaOk, 'req' => true,
        'note' => $schemaOk ? null : 'schema.sql missing from project root'];

    $composerOk = is_file(__DIR__ . '/vendor/autoload.php');
    $checks[] = ['id' => 'composer', 'label' => 'vendor/ (Composer)', 'ok' => $composerOk, 'req' => true,
        'note' => $composerOk ? null : 'Run: composer install from project root'];

    $dbDir = __DIR__ . '/redirect/databases';
    $dbDirOk = is_dir($dbDir) ? is_writable($dbDir) : is_writable(dirname($dbDir));
    $checks[] = ['id' => 'db_dir', 'label' => 'redirect/databases/ writable', 'ok' => $dbDirOk, 'req' => false,
        'note' => $dbDirOk ? null : 'mkdir redirect/databases && chmod 755 redirect/databases'];

    $geoOk = is_file(__DIR__ . '/redirect/databases/GeoLite2-Country.mmdb');
    $checks[] = ['id' => 'geoip', 'label' => 'GeoLite2-Country.mmdb', 'ok' => $geoOk, 'req' => false,
        'note' => $geoOk ? null : 'Download free from maxmind.com (MAXMIND_LICENSE_KEY)'];

    $authOk = is_writable(dirname(INS_AUTH));
    $checks[] = ['id' => 'stats_auth', 'label' => 'statistics/ writable (report_auth.php)', 'ok' => $authOk, 'req' => true,
        'note' => $authOk ? null : 'chmod 755 statistics/'];

    $canProceed = true;
    foreach ($checks as $check) {
        if ($check['req'] && !$check['ok']) {
            $canProceed = false;
            break;
        }
    }
    echo json_encode(['ok' => true, 'checks' => $checks, 'can_proceed' => $canProceed]);
}

// ── Step 2a: Test DB connection ───────────────────────────────────────────────
function ins_act_test_db(): void
{
    $host = trim((string) ($_POST['db_host']    ?? 'localhost'));
    $port = (int) ($_POST['db_port']    ?? 3306);
    $user = trim((string) ($_POST['db_user']    ?? ''));
    $pass = (string) ($_POST['db_pass']    ?? '');
    $name = trim((string) ($_POST['db_name']    ?? ''));
    $sock = trim((string) ($_POST['db_socket']  ?? ''));
    $chr  = trim((string) ($_POST['db_charset'] ?? 'utf8mb4'));

    if ($user === '' || $name === '') {
        echo json_encode(['ok' => false, 'error' => 'Username and database name required']);

        return;
    }

    $dsn = $sock !== ''
        ? "mysql:unix_socket={$sock};dbname={$name};charset={$chr}"
        : "mysql:host={$host};port={$port};dbname={$name};charset={$chr}";

    try {
        $pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, PDO::MYSQL_ATTR_MULTI_STATEMENTS => false]);
        $stmt = $pdo->query('SELECT VERSION()');
        $ver = $stmt !== false ? $stmt->fetchColumn() : false;
        ins_json(['ok' => true, 'version' => (string) $ver]);
    } catch (Throwable $e) {
        ins_log_error('test_db', $e);
        ins_json(['ok' => false, 'error' => 'Database connection failed. Verify credentials and host.']);
    }
}

// ── Step 2b: Import schema.sql ────────────────────────────────────────────────
function ins_act_import_schema(): void
{
    if (!is_file(INS_SCHEMA)) {
        echo json_encode(['ok' => false, 'error' => 'schema.sql not found']);

        return;
    }
    $host = trim((string) ($_POST['db_host']    ?? 'localhost'));
    $port = (int) ($_POST['db_port']    ?? 3306);
    $user = trim((string) ($_POST['db_user']    ?? ''));
    $pass = (string) ($_POST['db_pass']    ?? '');
    $name = trim((string) ($_POST['db_name']    ?? ''));
    $sock = trim((string) ($_POST['db_socket']  ?? ''));
    $chr  = trim((string) ($_POST['db_charset'] ?? 'utf8mb4'));

    $dsn = $sock !== '' ? "mysql:unix_socket={$sock};dbname={$name};charset={$chr}"
        : "mysql:host={$host};port={$port};dbname={$name};charset={$chr}";

    try {
        $pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, PDO::MYSQL_ATTR_MULTI_STATEMENTS => false]);
        $sql = (string) file_get_contents(INS_SCHEMA);
        // Split by semicolons, skip comments and blank
        $rawStatements = preg_split('/;\s*\n/', $sql) ?: [];
        $stmts = [];
        foreach ($rawStatements as $rawStatement) {
            $statement = trim((string) $rawStatement);
            if ($statement !== '' && !str_starts_with(ltrim($statement), '--')) {
                $stmts[] = $statement;
            }
        }
        $count = 0;
        foreach ($stmts as $stmt) {
            try {
                $pdo->exec($stmt);
                $count++;
            } catch (Throwable $e) {
            }
        }
        // Run migration files too
        $migrations = glob(__DIR__ . '/migrations/*.sql') ?: [];
        sort($migrations);
        foreach ($migrations as $mig) {
            $migSql = (string) file_get_contents($mig);

            try {
                $pdo->exec($migSql);
            } catch (Throwable $e) {
            }
        }
        ins_json(['ok' => true, 'statements' => $count, 'migrations' => count($migrations)]);
    } catch (Throwable $e) {
        ins_log_error('import_schema', $e);
        ins_json(['ok' => false, 'error' => 'Schema import failed. Check database permissions and schema syntax.']);
    }
}

// ── Step 3: Auto-detect server IP ────────────────────────────────────────────
function ins_act_detect_ip(): void
{
    $ip = null;
    $srvAddr = $_SERVER['SERVER_ADDR'] ?? '';
    if (filter_var($srvAddr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_IPV4)) {
        $ip = $srvAddr;
    }
    if ($ip === null && extension_loaded('curl')) {
        foreach (['https://api4.my-ip.io/ip.txt', 'https://ipv4.icanhazip.com', 'https://checkip.amazonaws.com'] as $url) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_SSL_VERIFYPEER => true]);
            $res = curl_exec($ch);
            curl_close($ch);
            $candidate = trim((string) $res);
            if (filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_IPV4)) {
                $ip = $candidate;
                break;
            }
        }
    }
    $ip !== null
        ? print(json_encode(['ok' => true, 'ip' => $ip]))
        : print(json_encode(['ok' => false, 'error' => 'Cannot detect - enter manually']));
}

// ── Step 6: Auto-detect cPanel API hostname ──────────────────────────────────
function ins_act_detect_cpanel_host(): void
{
    $postedHost = trim((string) ($_POST['cpanel_host'] ?? ''));
    $postedPort = (int) ($_POST['cpanel_port'] ?? 2083);
    $port = $postedPort >= 1 && $postedPort <= 65535 ? $postedPort : 2083;
    $host = ins_detect_cpanel_host($postedHost, $port);
    $valid = ins_is_public_cpanel_host($host);

    ins_json([
        'ok' => $valid,
        'host' => $host,
        'port' => $port,
        'error' => $valid ? '' : 'Cannot detect a public cPanel hostname. Enter it manually.',
    ]);
}

// ── Step 3: Create *.DOMAIN wildcard in cPanel to redirect root ──────────────
/**
 * Creates *.BASE_DOMAIN as a cPanel wildcard subdomain pointing to redirect/.
 *
 * The root .htaccess already routes by subdomain prefix:
 *   gen.*  to public/
 *   r.*    to redirect/
 *   s.*    to statistics/
 *   *.*    to custom/{sub}/  or root index
 *
 * Admin and statistics subdomains need their own document roots when exposed.
 */
function ins_act_provision_wildcard(): void
{
    $domain  = strtolower(trim((string) ($_POST['domain']       ?? '')));
    $cpUser  = trim((string) ($_POST['cpanel_user']  ?? ''));
    $cpToken = trim((string) ($_POST['cpanel_token'] ?? ''));
    $cpPort  = (int) ($_POST['cpanel_port'] ?? 2083);
    $cpPort  = $cpPort >= 1 && $cpPort <= 65535 ? $cpPort : 2083;
    $cpHost  = ins_detect_cpanel_host(trim((string) ($_POST['cpanel_host'] ?? '')), $cpPort);
    $cpDir   = trim((string) ($_POST['cpanel_dir']   ?? ''));

    if ($domain === '' || $cpUser === '' || $cpToken === '') {
        echo json_encode(['ok' => false, 'error' => 'Domain, cPanel user, and API token required']);

        return;
    }

    $cpDir = ins_cpanel_redirect_dir($cpDir);

    $results = [];
    $errors  = [];

    // cPanel UAPI - SubDomain/addsubdomain
    // Creates *.DOMAIN to the redirect engine document root.
    $url    = sprintf('https://%s:%d/execute/SubDomain/addsubdomain', $cpHost, $cpPort);
    $params = http_build_query([
        'domain'     => '*',
        'rootdomain' => $domain,
        'dir'        => $cpDir,
    ]);

    try {
        if (!extension_loaded('curl')) {
            throw new RuntimeException('cURL extension not available');
        }

        $ch = curl_init($url . '?' . $params);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER     => ['Authorization: cpanel ' . $cpUser . ':' . $cpToken],
        ]);

        $resp  = curl_exec($ch);
        $errno = curl_errno($ch);
        $errst = curl_error($ch);
        curl_close($ch);

        if ($errno !== 0 || $resp === false) {
            throw new RuntimeException('cURL: ' . $errst);
        }

        $data = json_decode((string) $resp, true);

        if (($data['status'] ?? 0) === 1) {
            $results[] = '*.' . $domain . ' to /' . $cpDir . '/ created';
        } else {
            $msg = (string) ($data['errors'][0] ?? $data['messages'][0] ?? 'Unknown error');
            if (stripos($msg, 'exist') !== false || stripos($msg, 'already') !== false) {
                $results[] = '*.' . $domain . ' already exists - OK';
            } else {
                $errors[] = $msg;
            }
        }
    } catch (Throwable $e) {
        $errors[] = ins_safe_error($e, 'cPanel wildcard provisioning failed.');
    }

    echo json_encode(['ok' => $errors === [], 'results' => $results, 'errors' => $errors, 'cpanel_host' => $cpHost]);
}

// ── Step 6: Add cron jobs ─────────────────────────────────────────────────────
function ins_act_add_crons(): void
{
    $phpBin = ins_safe_cron_php_bin((string) ($_POST['php_bin'] ?? '/usr/bin/php'));
    $method = trim((string) ($_POST['method'] ?? 'manual'));
    $method = $method === 'cpanel' ? 'cpanel' : 'manual';
    $cpUser = trim((string) ($_POST['cpanel_user'] ?? ''));
    $cpToken = trim((string) ($_POST['cpanel_token'] ?? ''));
    $cpPort = (int) ($_POST['cpanel_port'] ?? 2083);
    $cpPort = $cpPort >= 1 && $cpPort <= 65535 ? $cpPort : 2083;
    $cpHost = ins_detect_cpanel_host(trim((string) ($_POST['cpanel_host'] ?? '')), $cpPort);
    $base = rtrim(__DIR__, DIRECTORY_SEPARATOR);

    $jobs = [
        [
            'label' => 'Cache cleanup (daily 2 AM)',
            'schedule' => '0 2 * * *',
            'script' => $base . '/redirect/cleanup-cache.php',
        ],
        [
            'label' => 'GeoIP update (weekly Sun 3 AM)',
            'schedule' => '0 3 * * 0',
            'script' => $base . '/redirect/update-geoip.php',
        ],
    ];

    $lines = [];
    $warnings = [];
    foreach ($jobs as $index => $job) {
        $script = (string) $job['script'];
        if (!is_file($script)) {
            $warnings[] = (string) $job['label'] . ': target script not found yet';
        }
        $jobs[$index]['cmd'] = ins_build_cron_command($phpBin, $script);
        $lines[] = (string) $job['schedule'] . ' ' . $jobs[$index]['cmd'];
    }

    if ($method !== 'cpanel') {
        ins_json(['ok' => true, 'method' => 'manual', 'lines' => $lines, 'warnings' => $warnings]);

        return;
    }

    if ($cpUser === '' || $cpToken === '') {
        ins_json([
            'ok' => false,
            'method' => 'manual',
            'error' => 'cPanel username and API token are required for automatic cron setup.',
            'lines' => $lines,
            'warnings' => $warnings,
        ]);

        return;
    }

    if (!extension_loaded('curl')) {
        ins_json([
            'ok' => false,
            'method' => 'manual',
            'error' => 'cURL extension is required for cPanel cron setup. Add the crontab lines manually.',
            'lines' => $lines,
            'warnings' => $warnings,
        ]);

        return;
    }

    $added = [];
    $errors = [];
    foreach ($jobs as $job) {
        try {
            [$min, $hour, $day, $mon, $wday] = ins_parse_cron_schedule((string) $job['schedule']);
            $url = sprintf('https://%s:%d/execute/Cron/add_line', $cpHost, $cpPort);
            $query = http_build_query([
                'command' => (string) $job['cmd'],
                'minute' => $min,
                'hour' => $hour,
                'day' => $day,
                'month' => $mon,
                'weekday' => $wday,
            ]);

            $ch = curl_init($url . '?' . $query);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 20,
                CURLOPT_CONNECTTIMEOUT => 10,
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_SSL_VERIFYHOST => 2,
                CURLOPT_HTTPHEADER => ['Authorization: cpanel ' . $cpUser . ':' . $cpToken],
            ]);

            $resp = curl_exec($ch);
            $errno = curl_errno($ch);
            $errst = curl_error($ch);
            curl_close($ch);

            if ($errno !== 0 || $resp === false) {
                throw new RuntimeException('cPanel transport error.');
            }

            $data = json_decode((string) $resp, true);
            if (!is_array($data)) {
                throw new RuntimeException('cPanel returned non-JSON response.');
            }

            if ((int) ($data['status'] ?? 0) === 1) {
                $added[] = (string) $job['label'];
                continue;
            }

            $message = (string) ($data['errors'][0] ?? $data['messages'][0] ?? 'cPanel cron add_line failed');
            if (stripos($message, 'exist') !== false || stripos($message, 'already') !== false) {
                $added[] = (string) $job['label'] . ' (already exists)';
                continue;
            }
            $errors[] = (string) $job['label'] . ': ' . $message;
        } catch (Throwable $e) {
            ins_log_error('add_crons', $e);
            $errors[] = (string) $job['label'] . ': cPanel cron setup failed.';
        }
    }

    ins_json([
        'ok' => $errors === [],
        'method' => 'cpanel',
        'added' => $added,
        'errors' => $errors,
        'warnings' => $warnings,
        'lines' => $lines,
        'cpanel_host' => $cpHost,
    ]);
}

// ── Step 7: Finalize - write .env, hash password, lock ───────────────────────
function ins_act_finalize(): void
{
    $d = $_POST;

    $domain = ins_normalize_domain((string) ($d['domain'] ?? ''));

    if (!is_writable(__DIR__)) {
        throw new RuntimeException('Project root is not writable. Cannot write .env or install.lock.');
    }
    if (is_file(INS_ENV) && !is_writable(INS_ENV)) {
        throw new RuntimeException('.env exists but is not writable.');
    }
    if (is_file(INS_LOCK) && !is_writable(INS_LOCK)) {
        throw new RuntimeException('install.lock exists but is not writable.');
    }

    // Auto-generate secrets (use posted values if re-running)
    $afSecret = ins_secret_or_generate((string) ($d['af_secret'] ?? ''));
    $ixgKey = ins_secret_or_generate((string) ($d['ixg_bypass_key'] ?? ''));
    $srpApiKey = ins_secret_or_generate((string) ($d['srp_api_key'] ?? ''));
    $pbSecret = ins_secret_or_generate((string) ($d['postback_secret'] ?? ''));

    // Hash statistics password
    $a2Pw = trim((string) ($d['a2root_password'] ?? ''));
    if ($a2Pw !== '') {
        $authDir = dirname(INS_AUTH);
        if (!is_dir($authDir) && !mkdir($authDir, 0755, true) && !is_dir($authDir)) {
            throw new RuntimeException('Cannot create statistics directory for report_auth.php.');
        }
        if (!is_writable($authDir)) {
            throw new RuntimeException('statistics directory is not writable.');
        }

        $hash = password_hash($a2Pw, PASSWORD_BCRYPT, ['cost' => 10]);
        if (!is_string($hash)) {
            throw new RuntimeException('Password hashing failed.');
        }
        $authPHP = "<?php\n\ndeclare(strict_types=1);\n\nreturn [\n    'password_hash' => '" . $hash . "',\n];\n";
        if (file_put_contents(INS_AUTH, $authPHP, LOCK_EX) === false) {
            throw new RuntimeException('Cannot write statistics/report_auth.php.');
        }
    }

    $cpanelPort = (int) ($d['cpanel_port'] ?? 2083);
    $cpanelPort = $cpanelPort >= 1 && $cpanelPort <= 65535 ? $cpanelPort : 2083;
    $cpanelHost = ins_detect_cpanel_host((string) ($d['cpanel_host'] ?? ''), $cpanelPort);
    $postbackTz = ins_valid_timezone((string) ($d['postback_tz'] ?? 'UTC'));

    // .env content
    $e = '# Generated by Genv2 Installer v' . INS_VER . ' - ' . date('Y-m-d H:i:s T') . "\n\n";
    $e .= "# ── Database ────────────────────────────────────────────────────────────────\n";
    $e .= 'DB_HOST=' . ev($d['db_host'] ?? 'localhost') . "\n";
    $e .= 'DB_PORT=' . ((int) ($d['db_port'] ?? 3306)) . "\n";
    $e .= 'DB_USER=' . ev($d['db_user'] ?? '') . "\n";
    $e .= 'DB_PASSWORD=' . ev($d['db_pass'] ?? '') . "\n";
    $e .= 'DB_NAME=' . ev($d['db_name'] ?? '') . "\n";
    $e .= 'DB_SOCKET=' . ev($d['db_socket'] ?? '') . "\n";
    $e .= 'DB_CHARSET=' . ev($d['db_charset'] ?? 'utf8mb4') . "\n";
    $e .= "DB_PERSISTENT=0\n";

    $e .= "\n# ── Admin ───────────────────────────────────────────────────────────────────\n";
    $e .= 'ADMIN_PASSWORD=' . ev($d['admin_password'] ?? '') . "\n";
    $e .= 'A2ROOT_PASSWORD=' . ev($a2Pw) . "\n";

    $e .= "\n# ── cPanel ──────────────────────────────────────────────────────────────────\n";
    $e .= 'CPANEL_USER=' . ev($d['cpanel_user'] ?? '') . "\n";
    $e .= 'CPANEL_PASSWORD=' . ev($d['cpanel_pass'] ?? '') . "\n";
    $e .= 'CPANEL_HOST=' . ev($cpanelHost) . "\n";
    $e .= 'CPANEL_PORT=' . $cpanelPort . "\n";
    $e .= 'CPANEL_SUBDOMAIN_DIR=' . ev(ins_cpanel_redirect_dir((string) ($d['cpanel_dir'] ?? ''))) . "\n";
    $e .= 'CPANEL_API_TOKEN=' . ev($d['cpanel_token'] ?? '') . "\n";

    $e .= "\n# ── Secrets (auto-generated) ────────────────────────────────────────────────\n";
    $e .= 'AF_SECRET=' . $afSecret . "\n";
    $e .= 'IXG_BYPASS_KEY=' . $ixgKey . "\n";

    $e .= "\n# ── Shortlink ───────────────────────────────────────────────────────────────\n";
    $e .= 'TRACKR_API_KEY=' . ev($d['trackr_api_key'] ?? '') . "\n";
    $e .= 'TRACKR_ENDPOINT=' . ev($d['trackr_endpoint'] ?? '') . "\n";
    $e .= 'TINYURL_API_KEY=' . ev($d['tinyurl_api_key'] ?? '') . "\n";

    $e .= "\n# ── Reporting ───────────────────────────────────────────────────────────────\n";
    $reportHost = trim((string) ($d['report_host'] ?? ''));
    if ($reportHost === '') {
        $reportHost = 's.' . $domain;
    }
    $e .= 'REPORT_HOST=' . ev($reportHost) . "\n";

    $e .= "\n# ── Geo-blocking ────────────────────────────────────────────────────────────\n";
    $e .= 'SRP_BLOCK_COUNTRIES=' . ev($d['block_countries'] ?? 'ID') . "\n";

    $e .= "\n# ── Timed Filter ────────────────────────────────────────────────────────────\n";
    $e .= 'SRP_FILTER_URL=' . ev($d['filter_url'] ?? '') . "\n";

    $e .= "\n# ── Click Log ───────────────────────────────────────────────────────────────\n";
    $reportDir = trim((string) ($d['report_dir'] ?? ''));
    if ($reportDir === '') {
        $reportDir = 'statistics';
    }
    $e .= 'SRP_REPORT_DIR=' . ev($reportDir) . "\n";

    $e .= "\n# ── API (auto-generated) ────────────────────────────────────────────────────\n";
    $e .= 'SRP_API_KEY=' . $srpApiKey . "\n";

    $e .= "\n# ── GeoIP Updater ───────────────────────────────────────────────────────────\n";
    $e .= 'MAXMIND_LICENSE_KEY=' . ev($d['maxmind_key'] ?? '') . "\n";

    $e .= "\n# ── Postback (auto-generated) ───────────────────────────────────────────────\n";
    $e .= 'POSTBACK_SECRET=' . $pbSecret . "\n";
    $e .= 'POSTBACK_TIMEZONE=' . ev($postbackTz) . "\n";

    if (file_put_contents(INS_ENV, $e, LOCK_EX) === false) {
        throw new RuntimeException('Cannot write .env. Check project root permissions.');
    }

    $lockContent = 'Installed: ' . date('Y-m-d H:i:s T') . ' - Genv2 Installer v' . INS_VER . "\n";
    if (file_put_contents(INS_LOCK, $lockContent, LOCK_EX) === false) {
        throw new RuntimeException('Cannot write install.lock. Check project root permissions.');
    }

    ins_json([
        'ok' => true,
        'secrets' => [
            'AF_SECRET' => $afSecret,
            'IXG_BYPASS_KEY' => $ixgKey,
            'SRP_API_KEY' => $srpApiKey,
            'POSTBACK_SECRET' => $pbSecret,
        ],
    ]);
}

// ══════════════════════════════════════════════════════════════════════════════
//  HELPERS
// ══════════════════════════════════════════════════════════════════════════════

function ev(string $v): string
{
    if ($v === '') {
        return '';
    }

    return preg_match('/[\s#"\'\\\\]/', $v) ? '"' . addcslashes($v, '"\\') . '"' : $v;
}

function ins_cpanel_redirect_dir(string $dir): string
{
    $normalized = trim(str_replace('\\', '/', $dir));
    $normalized = trim($normalized, "/ \t\n\r\0\x0B");
    if ($normalized === '') {
        $normalized = 'public_html';
    }

    return str_ends_with(strtolower($normalized), '/redirect')
        ? $normalized
        : $normalized . '/redirect';
}

function ins_json(array $payload): void
{
    echo json_encode($payload, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR);
}

function ins_safe_cron_php_bin(string $phpBin): string
{
    $phpBin = trim($phpBin);
    if ($phpBin === '') {
        return '/usr/bin/php';
    }

    if (preg_match('/[;&|`$<>\r\n]/', $phpBin) === 1 || preg_match('/\s/', $phpBin) === 1) {
        throw new RuntimeException('Invalid PHP binary path. Use an absolute PHP binary path without spaces or shell operators.');
    }

    return $phpBin;
}

function ins_build_cron_command(string $phpBin, string $scriptPath): string
{
    return escapeshellarg($phpBin) . ' ' . escapeshellarg($scriptPath) . ' >> /dev/null 2>&1';
}

/**
 * @return array{0:string,1:string,2:string,3:string,4:string}
 */
function ins_parse_cron_schedule(string $schedule): array
{
    $parts = preg_split('/\s+/', trim($schedule)) ?: [];
    if (count($parts) !== 5) {
        throw new RuntimeException('Invalid cron schedule.');
    }

    return [(string) $parts[0], (string) $parts[1], (string) $parts[2], (string) $parts[3], (string) $parts[4]];
}

function ins_detect_cpanel_host(string $postedHost, int $port): string
{
    if (function_exists('app_detect_cpanel_host')) {
        return app_detect_cpanel_host($postedHost, $port);
    }

    $host = strtolower(trim($postedHost));
    $host = preg_replace('/^https?:\/\//', '', $host) ?? $host;
    $host = preg_replace('/:\d+$/', '', $host) ?? $host;
    $host = trim($host, " \t\n\r\0\x0B/");

    if ($host !== '' && $host !== 'localhost') {
        return $host;
    }

    $serverName = strtolower(trim((string) ($_SERVER['SERVER_NAME'] ?? $_SERVER['HTTP_HOST'] ?? '')));
    $serverName = preg_replace('/:\d+$/', '', $serverName) ?? $serverName;
    if ($serverName !== '') {
        return $serverName;
    }

    return 'localhost';
}

function ins_is_public_cpanel_host(string $host): bool
{
    if (function_exists('app_is_public_cpanel_host')) {
        return app_is_public_cpanel_host($host);
    }

    $host = strtolower(trim($host));
    if ($host === '' || $host === 'localhost') {
        return false;
    }

    if (filter_var($host, FILTER_VALIDATE_IP)) {
        return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
    }

    return preg_match('/\A(?=.{1,253}\z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}\z/', $host) === 1;
}

function ins_normalize_domain(string $domain): string
{
    $domain = strtolower(trim($domain));
    $domain = preg_replace('/^https?:\/\//', '', $domain) ?? $domain;
    $domain = preg_replace('/\/.*$/', '', $domain) ?? $domain;
    $domain = preg_replace('/:\d+$/', '', $domain) ?? $domain;
    $domain = trim($domain, " \t\n\r\0\x0B.");

    if ($domain === '' || preg_match('/\A(?=.{1,253}\z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}\z/', $domain) !== 1) {
        throw new RuntimeException('Invalid primary domain. Use a valid domain such as example.com.');
    }

    return $domain;
}

function ins_valid_timezone(string $timezone): string
{
    $timezone = trim($timezone);
    if ($timezone === '') {
        return 'UTC';
    }

    return in_array($timezone, timezone_identifiers_list(), true) ? $timezone : 'UTC';
}

function ins_secret_or_generate(string $value): string
{
    $value = trim($value);
    if ($value !== '' && preg_match('/\A[a-f0-9]{64}\z/i', $value) === 1) {
        return strtolower($value);
    }

    return bin2hex(random_bytes(32));
}

function ins_log_error(string $scope, Throwable $e): void
{
    error_log('[installer:' . $scope . '] ' . $e->getMessage());
}

function ins_safe_error(Throwable $e, string $fallback = 'Request failed. Check installer logs for details.'): string
{
    ins_log_error('safe_error', $e);

    $message = $e->getMessage();
    if ($message === '') {
        return $fallback;
    }

    if (preg_match('/(token|secret|password|authorization|bearer|SQLSTATE|stack trace|[A-Z]:\\\\|\/home\/|dsn)/i', $message) === 1) {
        return $fallback;
    }

    return $message;
}

function ins_is_https(): bool
{
    return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (int) ($_SERVER['SERVER_PORT'] ?? 0) === 443;
}

function ins_locked_page(): void
{
    http_response_code(403);
    $when = is_file(INS_LOCK) ? trim((string) file_get_contents(INS_LOCK)) : 'unknown time';
    $h = static function (string $s): string {
        return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    };
    echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Already Installed - Genv2</title>'
        . '<style>*{box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,Arial,sans-serif;background:#f7f7f7;color:#292b2c;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}'
        . '@media(prefers-color-scheme:dark){body{background:#111315;color:#f3f4f4}.box{background:#171a1d!important;border-color:#2d3236!important}code{background:#111315!important;color:#f3f4f4!important}p{color:#6b6f72!important}}'
        . '.box{max-width:520px;padding:32px;background:#ffffff;border:1px solid #e3e5e6;border-radius:.3rem;box-shadow:none}'
        . 'h2{margin:0 0 12px;color:#6b6f72;font-size:18px;font-weight:700}p{color:#6b6f72;line-height:1.6}code{background:#f0f1f1;padding:2px 6px;border-radius:.3rem;color:#292b2c;font-size:.85em}'
        . 'a{color:#292b2c;text-decoration:underline;text-underline-offset:2px}</style></head><body><div class="box">'
        . '<h2>Already Installed</h2>'
        . '<p>' . $h($when) . '</p>'
        . '<p>Delete <code>install.lock</code> locally to reinstall.</p>'
        . '<p><a href="/">Application</a></p></div></body></html>';
}

// ══════════════════════════════════════════════════════════════════════════════
//  HTML RENDERER
// ══════════════════════════════════════════════════════════════════════════════

$nonce = base64_encode(random_bytes(12));
$preIp = filter_var($_SERVER['SERVER_ADDR'] ?? '', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_IPV4)
    ? htmlspecialchars($_SERVER['SERVER_ADDR'], ENT_QUOTES) : '';
$preDomain = strtolower(trim((string) ($_SERVER['HTTP_HOST'] ?? '')));
$preDomain = preg_replace('/^www\./', '', $preDomain) ?? '';
$preDomain = htmlspecialchars($preDomain, ENT_QUOTES);

header('Content-Type: text/html; charset=UTF-8');
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
// style-src: strict nonce (prevents injected CSS)
header('Referrer-Policy: same-origin');
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
header("Content-Security-Policy: default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; img-src 'self' data:; style-src 'nonce-{$nonce}'; script-src 'nonce-{$nonce}'; connect-src 'self' https://api4.my-ip.io https://ipv4.icanhazip.com https://checkip.amazonaws.com");

?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Genv2 Installer v<?= INS_VER ?></title>
<style nonce="<?= $nonce ?>">
:root{color-scheme:light;--inverse:#292b2c;--faded:#f7f7f7;--bg:var(--faded);--surface:rgba(41,43,44,.035);--panel:rgba(247,247,247,.96);--panel2:rgba(41,43,44,.045);--line:rgba(41,43,44,.14);--line2:rgba(41,43,44,.10);--text:var(--inverse);--muted:rgba(41,43,44,.68);--strong:var(--inverse);--primary:var(--inverse);--primary2:var(--inverse);--accent:var(--inverse);--accent-h:var(--inverse);--accent-s:rgba(41,43,44,.075);--on:var(--faded);--bad:var(--inverse);--good:var(--inverse);--warn:var(--inverse);--radius:.3rem;--shadow:none;--shadow-modal:none;--font:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;--mono:"SFMono-Regular",Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--sidebar:220px;--surface2:var(--panel2);--border:var(--line);--border2:var(--line2);--accent-hover:var(--accent-h);--accent-soft:var(--accent-s);--accent2:var(--accent-h);--success:var(--good);--danger:var(--bad)}
*{box-sizing:border-box;margin:0;padding:0}
html,body{min-height:100vh;background:var(--bg);color:var(--text);font-family:var(--font);font-size:14px;line-height:1.5}
a{color:var(--accent);text-decoration:none}
code{background:var(--surface2);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-size:12px}
pre{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:10px;font-size:12px;overflow-x:auto;white-space:pre-wrap;word-break:break-all}
.canvas-card{position:relative;overflow:hidden;border:1px solid var(--border);border-radius:var(--radius);background:var(--faded);margin:0 0 18px;color:var(--inverse)}.canvas-wrap{position:relative;height:246px;overflow:hidden;background:var(--faded)}.canvas-wrap canvas{display:block;width:100%;height:100%;background:var(--faded)}.canvas-logo-main{position:absolute;left:18px;right:18px;top:18px;bottom:86px;display:flex;flex-direction:row;align-items:center;justify-content:center;gap:20px;text-align:left;padding:12px 16px;z-index:2;pointer-events:none;border:1px solid rgba(41,43,44,.10);border-radius:var(--radius);background:rgba(247,247,247,.58)}.canvas-copy{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;min-width:0}.logo-bounce{width:88px;height:88px;margin:0;flex:0 0 auto}.logo-wrap{width:88px;height:88px;display:flex;align-items:center;justify-content:center;animation:softFadeLogo 7.5s ease-in-out infinite;will-change:opacity,transform}.logo{width:88px;height:88px;display:block;border:0;border-radius:var(--radius);user-select:none;-webkit-user-select:none;pointer-events:auto}.canvas-logo-main h1{display:flex;align-items:center;justify-content:flex-start;gap:9px;margin:0;color:var(--inverse);font-family:var(--mono);font-size:18px;font-weight:400;letter-spacing:.22em;line-height:1.15;text-transform:uppercase;white-space:nowrap}.canvas-logo-main .ngix,.canvas-logo-main .xctd{font-weight:400;opacity:.86}.canvas-logo-main .xctd{letter-spacing:.34em;opacity:.72}.canvas-logo-main .mark{margin:0 1px;font-weight:400;opacity:.52}.canvas-logo-main .details{margin:9px 0 0;color:rgba(41,43,44,.60);font-family:var(--mono);font-size:10.5px;font-weight:400;letter-spacing:.08em;white-space:nowrap}.canvas-logo-main .details span{opacity:.42}.canvas-status-grid{position:absolute;left:18px;right:18px;bottom:18px;z-index:3;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;pointer-events:none}.canvas-status-card{position:relative;min-height:52px;border:1px solid rgba(41,43,44,.14);border-radius:var(--radius);background:rgba(247,247,247,.70);padding:9px 28px 8px 14px;overflow:hidden}.canvas-status-card:after{content:"";position:absolute;right:14px;top:15px;width:5px;height:5px;border-radius:50%;background:rgba(41,43,44,.58)}.canvas-status-label{display:block;margin:0 0 4px;color:rgba(41,43,44,.46);font-family:var(--mono);font-size:8.5px;font-weight:500;letter-spacing:.05em;text-transform:uppercase;line-height:1}.canvas-status-value{display:block;color:rgba(41,43,44,.86);font-family:var(--mono);font-size:11.5px;font-weight:600;line-height:1.15;white-space:nowrap}.canvas-status-note{display:block;margin-top:3px;color:rgba(41,43,44,.48);font-family:var(--mono);font-size:8.5px;font-weight:400;line-height:1.1;white-space:nowrap}@keyframes softFadeLogo{0%,100%{opacity:.88;transform:translateY(0) scale(1)}50%{opacity:1;transform:translateY(-2px) scale(1.015)}}@media(max-width:720px){.canvas-wrap{height:300px}.canvas-logo-main{bottom:140px;gap:14px}.canvas-status-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.logo-bounce,.logo-wrap,.logo{width:76px;height:76px}.canvas-logo-main h1{font-size:15px;letter-spacing:.16em}.canvas-logo-main .xctd{letter-spacing:.22em}.canvas-logo-main .details{font-size:9.5px;letter-spacing:.04em}}@media(max-width:480px){.canvas-logo-main{left:14px;right:14px;top:14px;bottom:142px;gap:10px;padding:10px 12px}.canvas-status-grid{left:14px;right:14px;bottom:14px;gap:8px}.logo-bounce,.logo-wrap,.logo{width:64px;height:64px}.canvas-logo-main h1{font-size:13px;letter-spacing:.11em;gap:5px}.canvas-logo-main .xctd{letter-spacing:.14em}.canvas-logo-main .details{font-size:8.5px}.canvas-status-card{min-height:50px;padding:8px 22px 7px 10px}.canvas-status-value{font-size:10.5px}}
/* Layout */
.layout{display:flex;min-height:100vh}
.sidebar{width:var(--sidebar);background:var(--surface);border-right:1px solid var(--border);padding:20px 0;flex-shrink:0;position:sticky;top:0;height:100vh;overflow-y:auto}
.sidebar-brand{padding:0 16px 20px;border-bottom:1px solid var(--border);margin-bottom:12px}
.sidebar-brand h1{font-size:14px;font-weight:700;color:var(--text)}
.sidebar-brand small{color:var(--muted);font-size:11px}
.step-nav{list-style:none}
.step-nav li{display:flex;align-items:center;gap:8px;padding:7px 16px;font-size:12px;color:var(--muted);cursor:default;transition:background .1s}
.step-nav li.active{background:color-mix(in srgb,var(--accent) 10%,transparent);color:var(--text)}
.step-nav li.done{color:var(--success)}
.step-nav li.error{color:var(--danger)}
.step-dot{width:18px;height:18px;border-radius:50%;border:1.5px solid var(--border2);display:flex;align-items:center;justify-content:center;font-size:9px;flex-shrink:0;font-weight:700}
.step-nav li.active .step-dot{border-color:var(--accent);color:var(--accent)}
.step-nav li.done .step-dot{border-color:var(--success);background:var(--success);color:#000}
.step-nav li.error .step-dot{border-color:var(--danger);background:var(--danger);color:#000}
/* Main */
.main{flex:1;padding:32px;max-width:800px;overflow-y:auto}
.step-panel{display:none}
.step-panel.active{display:block}
.step-header{margin-bottom:24px;border-bottom:1px solid var(--border);padding-bottom:16px}
.step-header h2{font-size:18px;font-weight:700;margin-bottom:4px}
.step-header p{color:var(--muted);font-size:12.5px}
/* Form */
.field{margin-bottom:14px}
.field label{display:block;font-size:11.5px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin-bottom:5px}
.field input,.field select,.field textarea{width:100%;background:var(--surface2);border:1px solid var(--border2);border-radius:var(--radius);color:var(--text);padding:7px 10px;font-family:inherit;font-size:13px}
.field input:focus,.field select:focus,.field textarea:focus{outline:none;border-color:var(--accent)}
.field input[type=password]{letter-spacing:.1em}
.field .hint{font-size:11px;color:var(--muted);margin-top:3px}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.field-row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
/* Buttons */
.btn{padding:7px 16px;border-radius:var(--radius);border:1px solid var(--border2);background:var(--surface2);color:var(--text);font-family:inherit;font-size:13px;font-weight:700;cursor:pointer;transition:background .1s,border-color .1s}
.btn:hover{background:var(--surface);border-color:var(--accent)}
.btn:disabled{opacity:.5;cursor:not-allowed}
.btn-primary{background:var(--accent2);border-color:var(--accent2);color:#fff}
.btn-primary:hover{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-success{background:color-mix(in srgb,var(--success) 20%,var(--surface2));border-color:color-mix(in srgb,var(--success) 40%,var(--border2));color:var(--success)}
.btn-danger{background:color-mix(in srgb,var(--danger) 15%,var(--surface2));border-color:color-mix(in srgb,var(--danger) 35%,var(--border2));color:var(--danger)}
.btn-row{display:flex;gap:10px;margin-top:24px;align-items:center}
/* Alerts */
.alert{padding:10px 14px;border-radius:var(--radius);border:1px solid;font-size:12.5px;margin-bottom:14px}
.alert-success{background:color-mix(in srgb,var(--success) 10%,transparent);border-color:color-mix(in srgb,var(--success) 30%,var(--border));color:var(--success)}
.alert-error{background:color-mix(in srgb,var(--danger) 10%,transparent);border-color:color-mix(in srgb,var(--danger) 30%,var(--border));color:var(--danger)}
.alert-warn{background:color-mix(in srgb,var(--warn) 10%,transparent);border-color:color-mix(in srgb,var(--warn) 30%,var(--border));color:var(--warn)}
.alert-info{background:color-mix(in srgb,var(--accent) 10%,transparent);border-color:color-mix(in srgb,var(--accent) 30%,var(--border));color:var(--accent)}
/* Checks */
.check-list{list-style:none;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.check-list li{display:flex;align-items:center;gap:10px;padding:8px 12px;border-bottom:1px solid var(--border)}
.check-list li:last-child{border-bottom:0}
.chk{flex-shrink:0;font-size:13px}
.chk-ok{color:var(--success)}
.chk-fail{color:var(--danger)}
.chk-warn{color:var(--warn)}
.check-list .lbl{flex:1;font-size:12.5px}
.check-list .note{font-size:11px;color:var(--muted)}
/* NS box */
.ns-box{background:var(--surface2);border:1px solid var(--border2);border-radius:var(--radius);padding:12px 14px;margin-top:12px}
.ns-box h4{font-size:12px;font-weight:700;color:var(--warn);margin-bottom:8px}
.ns-box ol{padding-left:18px;color:var(--text);font-size:13px}
.ns-box ol li{margin-bottom:2px}
/* Log */
.log-box{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:10px;max-height:200px;overflow-y:auto;font-size:11.5px;color:var(--muted)}
.log-box .ok{color:var(--success)}
.log-box .err{color:var(--danger)}
/* Cron */
.cron-box{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:10px;font-size:12px}
/* Secret box */
.secret-box{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}
.secret-item{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:8px 10px}
.secret-item .sk{font-size:10.5px;color:var(--muted);font-weight:700;text-transform:uppercase;margin-bottom:2px}
.secret-item .sv{font-size:11px;word-break:break-all;color:var(--success)}
/* Completion */
.complete-card{text-align:center;padding:8px 0}
.complete-badge{width:56px;height:56px;margin:0 auto 14px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--success) 14%,var(--surface2));border:1px solid color-mix(in srgb,var(--success) 40%,var(--border));color:var(--success)}
.complete-badge svg{width:28px;height:28px;stroke:currentColor;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.complete-title{font-size:18px;font-weight:700;color:var(--text);margin:0 0 6px}
.complete-sub{font-size:12.5px;color:var(--muted);line-height:1.6;margin:0 auto 8px;max-width:480px}
.complete-card .section-title{text-align:left}
.complete-card .secret-box{text-align:left}
.complete-actions{justify-content:center;margin-top:20px}
/* Guide */
.guide h3{font-size:14px;font-weight:700;color:var(--accent);margin:20px 0 8px;padding-bottom:6px;border-bottom:1px solid var(--border)}
.guide h4{font-size:12.5px;font-weight:700;color:var(--text);margin:12px 0 6px}
.guide p,.guide li{font-size:12.5px;color:var(--muted);line-height:1.6;margin-bottom:6px}
.guide ul,.guide ol{padding-left:18px}
.guide .step-num{display:inline-block;background:var(--accent2);color:#fff;border-radius:50%;width:20px;height:20px;text-align:center;line-height:20px;font-size:11px;font-weight:700;margin-right:6px}
/* Token reveal */
.token-reveal{background:color-mix(in srgb,var(--warn) 8%,var(--surface2));border:1px solid color-mix(in srgb,var(--warn) 30%,var(--border));border-radius:var(--radius);padding:12px 14px;margin-top:10px}
.token-reveal .warn-label{font-size:11px;font-weight:700;color:var(--warn);margin-bottom:6px}
.token-reveal code{font-size:11.5px;word-break:break-all;background:transparent;border:0;padding:0;color:var(--text)}
/* Spinner */
.spin{display:inline-block;width:14px;height:14px;border:2px solid var(--border2);border-top-color:var(--accent);border-radius:50%;animation:sp .6s linear infinite;vertical-align:middle;margin-right:6px}
@keyframes sp{to{transform:rotate(360deg)}}
.d-none{display:none!important}.mt-8{margin-top:8px}.mt-10{margin-top:10px}.mt-20{margin-top:20px}.mb-16{margin-bottom:16px}.fw-normal,.fw-reg{font-weight:400;color:var(--muted)}.input-row{display:flex;gap:8px;align-items:center}.input-row input{flex:1}.btn-detect{width:36px;padding:0;display:inline-flex;align-items:center;justify-content:center}.ins-hint{font-size:12px;color:var(--muted);line-height:1.6;margin:6px 0 12px}.ins-muted{color:var(--muted)}.input-muted{background:var(--surface)!important;color:var(--muted)!important}.badge-opt{font-size:10px;color:var(--muted);font-weight:400}.perm-tag-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.ins-note-sm{font-size:11px;color:var(--muted);line-height:1.5}.svg-icon,.svg-slot svg{width:14px;height:14px;display:inline-block;vertical-align:-2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.svg-slot{display:inline-flex;align-items:center;justify-content:center;margin-right:6px}.btn .svg-slot:last-child{margin-right:0;margin-left:6px}.chk{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center}.chk svg{width:15px;height:15px;stroke:currentColor}.step-dot svg{width:12px;height:12px;stroke:currentColor}
/* Section divider */
.section-title{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:20px 0 10px;padding-bottom:4px;border-bottom:1px solid var(--border)}
/* Toggle */
.field-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12.5px;color:var(--muted)}
.field-toggle input[type=checkbox]{width:14px;height:14px;cursor:pointer}
</style>
</head>
<body>
<div class="layout">

<!-- ── Sidebar ── -->
<aside class="sidebar">
  <div class="sidebar-brand">
    <h1>Genv2 Installer</h1>
    <small>v<?= INS_VER ?></small>
  </div>
  <ul class="step-nav" id="stepNav">
    <li data-step="1" class="active"><span class="step-dot">1</span>System Check</li>
    <li data-step="2"><span class="step-dot">2</span>Database</li>
    <li data-step="3"><span class="step-dot">3</span>Domain, Server &amp; Services</li>
    <li data-step="4"><span class="step-dot">4</span>Admin &amp; Security</li>
    <li data-step="5"><span class="step-dot">5</span>Cron Jobs</li>
    <li data-step="6"><span class="step-dot">6</span>Finalize</li>
    <li data-step="7"><span class="step-dot">?</span>Manual Guide</li>
  </ul>
</aside>

<!-- ── Main ── -->
<div class="main" role="main">

<!-- ════ STEP 1: System Check ════ -->
<div class="step-panel active" id="step1">
  <div class="step-header">
    <h2>System Check</h2>
    <p>Auto-detecting PHP version, extensions, and file permissions.</p>
  </div>
  <section class="canvas-card" aria-label="Static Gateway Faded Inverse backend status preview">
    <div class="canvas-wrap">
      <canvas id="inverseFadedCanvas" width="900" height="260" aria-hidden="true"></canvas>
      <div class="canvas-logo-main" aria-label="Static Gateway Faded Inverse">
        <div class="logo-bounce">
          <div class="logo-wrap">
            <img src="favicon.ico" class="logo" alt="logo" draggable="false">
          </div>
        </div>
        <div class="canvas-copy">
          <h1><span class="ngix">NGIX</span><span class="mark">|</span><span class="xctd">XCTD</span></h1>
          <p class="details">Static Gateway <span>/</span> Faded Inverse</p>
        </div>
      </div>
      <div class="canvas-status-grid" aria-label="Backend capability status">
        <div class="canvas-status-card"><span class="canvas-status-label">Backend</span><span class="canvas-status-value">PHP 8.3+</span><span class="canvas-status-note">Runtime Ready</span></div>
        <div class="canvas-status-card"><span class="canvas-status-label">Security</span><span class="canvas-status-value">CSRF / CSP</span><span class="canvas-status-note">Nonce Active</span></div>
        <div class="canvas-status-card"><span class="canvas-status-label">Database</span><span class="canvas-status-value">PDO MySQL</span><span class="canvas-status-note">Prepared Mode</span></div>
        <div class="canvas-status-card"><span class="canvas-status-label">Gateway</span><span class="canvas-status-value">Wildcard DNS</span><span class="canvas-status-note">Route Layer</span></div>
      </div>
    </div>
  </section>
  <div id="s1-result"><button class="btn btn-primary" data-action="runDetect" type="button"><span class="svg-slot" data-icon="play"></span>Run System Check</button></div>
  <div class="btn-row">
    <button class="btn btn-primary" id="s1-next" disabled data-step-target="2" type="button">Continue <span class="svg-slot" data-icon="arrow-right"></span></button>
  </div>
</div>

<!-- ════ STEP 2: Database ════ -->
<div class="step-panel" id="step2">
  <div class="step-header">
    <h2>Database</h2>
    <p>Configure MySQL connection and import the schema.</p>
  </div>
  <div class="field-row">
    <div class="field"><label>Host</label><input id="db_host" value="localhost"></div>
    <div class="field"><label>Port</label><input id="db_port" value="3306" type="number"></div>
  </div>
  <div class="field-row">
    <div class="field"><label>Username</label><input id="db_user" autocomplete="off"></div>
    <div class="field"><label>Password</label><input id="db_pass" type="password" autocomplete="off"></div>
  </div>
  <div class="field"><label>Database Name</label><input id="db_name"></div>
  <div class="field"><label>Charset</label>
    <select id="db_charset"><option value="utf8mb4" selected>utf8mb4 (recommended)</option><option value="utf8">utf8</option></select>
  </div>
  <div class="field"><label>Unix Socket <span class="fw-normal">(optional - leave blank for TCP)</span></label><input id="db_socket" placeholder="/var/run/mysqld/mysqld.sock"></div>
  <div id="s2-status"></div>
  <div class="btn-row">
    <button class="btn" data-action="testDb" type="button">Test Connection</button>
    <button class="btn btn-success" id="s2-import" disabled data-action="importSchema" type="button">Import Schema</button>
    <button class="btn btn-primary" id="s2-next" disabled data-step-target="3" type="button">Continue <span class="svg-slot" data-icon="arrow-right"></span></button>
    <button class="btn" data-step-target="1" type="button"><span class="svg-slot" data-icon="arrow-left"></span>Back</button>
  </div>
</div>

<!-- ════ STEP 3: Domain, Server & Services ════ -->
<div class="step-panel" id="step3">
  <div class="step-header">
    <h2>Domain, Server &amp; Services</h2>
    <p>Domains, server IP, cPanel (wildcard subdomain), GeoIP, geo-blocking, and shortlink services. cPanel details are entered once here and reused for cron setup.</p>
  </div>

  <div class="section-title">Domain &amp; Server</div>
  <div class="alert alert-info">
    With a wildcard subdomain (<code>*</code>), all subdomains are covered by a single entry. You only need to route each subdomain to the correct directory.
  </div>
  <div class="field"><label>Primary Domain</label><input id="d_domain" value="<?= $preDomain ?>" placeholder="yourdomain.com">
    <div class="hint">Used to auto-fill reporting domains and the wildcard subdomain. Example: <code>example.com</code></div></div>
  <div class="field-row3">
    <div class="field"><label>Admin Portal</label><input id="d_gen" placeholder="gen.yourdomain.com" readonly class="input-muted"></div>
    <div class="field"><label>Redirect Engine</label><input id="d_redirect" placeholder="r.yourdomain.com" readonly class="input-muted"></div>
    <div class="field"><label>Statistics</label><input id="d_stats" placeholder="s.yourdomain.com" readonly class="input-muted"></div>
  </div>
  <div class="field"><label>Server IP</label><div class="input-row"><input id="d_server_ip" value="<?= $preIp ?>" placeholder="1.2.3.4"><button class="btn btn-detect" data-action="detectIpStep4" type="button"><span class="svg-slot" data-icon="refresh"></span></button></div></div>
  <div class="field"><label>Report Host (REPORT_HOST)</label><input id="d_report_host" placeholder="s.yourdomain.com">
    <div class="hint">Auto-filled from primary domain</div></div>
  <div class="field"><label>SRP Report Dir (click log domain)</label><input id="d_report_dir" placeholder="report.yourdomain.com"></div>

  <div class="section-title">cPanel</div>
  <p class="ins-hint">Entered once. Used to create the <code>*.DOMAIN</code> wildcard subdomain and (optionally) to add cron jobs automatically. The <code>.htaccess</code> routes <code>gen.*</code> to <code>public/</code>, <code>r.*</code> to <code>redirect/</code>, <code>s.*</code> to <code>statistics/</code>.</p>
  <div class="field-row">
    <div class="field"><label>cPanel Username</label><input id="cp_user" autocomplete="off" placeholder="cpanel_username"></div>
    <div class="field"><label>cPanel API Token <span class="fw-reg">(preferred)</span></label><input id="cp_api_token" type="password" autocomplete="off" placeholder="API token from cPanel"></div>
  </div>
  <div class="field-row3">
    <div class="field"><label>Host</label><div class="input-row"><input id="cp_host" value="localhost"><button class="btn btn-detect" data-action="detectCpanelHost" title="Auto-detect" type="button"><span class="svg-slot" data-icon="refresh"></span></button></div></div>
    <div class="field"><label>Port</label><input id="cp_port" value="2083" type="number"></div>
    <div class="field"><label>cPanel Password <span class="fw-reg">(fallback)</span></label><input id="cp_pass" type="password"></div>
  </div>
  <div class="field">
    <label>Project Root <span class="fw-normal">(relative to /home/user/ - redirect is appended)</span></label>
    <input id="cp_dir" placeholder="public_html">
    <div class="hint">Blank to <code>/home/user/public_html/redirect</code>. Parked/addon domain stays on <code>/home/user/public_html</code>; wildcard uses <code>/home/user/public_html/redirect</code>.</div>
  </div>
  <div id="cp-host-status"></div>
  <div id="s3-wc-status"></div>
  <button class="btn btn-success" data-action="provisionWildcard" type="button"><span class="svg-slot" data-icon="bolt"></span>Create *.DOMAIN in cPanel</button>

  <div class="section-title">MaxMind GeoIP</div>
  <div class="field"><label>MaxMind License Key (MAXMIND_LICENSE_KEY)</label><input id="maxmind_key" placeholder="Get free key at maxmind.com/geolite2/signup">
    <div class="hint">Required for weekly auto-update via <code>redirect/update-geoip.php</code></div></div>

  <div class="section-title">Geo-Blocking</div>
  <div class="field-row">
    <div class="field"><label>Block Countries (comma-separated ISO codes)</label><input id="block_countries" value="ID" placeholder="ID,CN,RU"></div>
    <div class="field"><label>Filter URL (SRP_FILTER_URL)</label><input id="filter_url" placeholder="https://example.com/filter"></div>
  </div>
  <div class="field"><label>Postback Timezone</label>
    <select id="pb_tz"><option value="UTC" selected>UTC</option><option value="Asia/Jakarta">Asia/Jakarta (WIB)</option><option value="Asia/Singapore">Asia/Singapore (SGT)</option><option value="America/New_York">America/New_York (ET)</option><option value="America/Los_Angeles">America/Los_Angeles (PT)</option><option value="Europe/London">Europe/London (GMT)</option></select></div>

  <div class="section-title">Shortlink Services</div>
  <div class="field-row">
    <div class="field"><label>TRACKR API Key</label><input id="trackr_key" autocomplete="off"></div>
    <div class="field"><label>TRACKR Endpoint</label><input id="trackr_ep" placeholder="https://r.yourdomain.com/api/shorten"></div>
  </div>
  <div class="field"><label>TinyURL API Key</label><input id="tinyurl_key" autocomplete="off"></div>

  <div class="btn-row">
    <button class="btn btn-primary" data-step-target="4" type="button">Continue <span class="svg-slot" data-icon="arrow-right"></span></button>
    <button class="btn" data-step-target="2" type="button"><span class="svg-slot" data-icon="arrow-left"></span>Back</button>
  </div>
</div>

<!-- ════ STEP 4: Admin & Security ════ -->
<div class="step-panel" id="step4">
  <div class="step-header">
    <h2>Admin &amp; Security</h2>
    <p>Set passwords and review auto-generated secrets.</p>
  </div>
  <div class="section-title">Passwords</div>
  <div class="field-row">
    <div class="field"><label>Admin Portal Password (ADMIN_PASSWORD)</label><input id="s_admin_pw" type="password" autocomplete="new-password">
      <div class="hint">Login password for <code>gen.yourdomain.com</code></div></div>
    <div class="field"><label>Statistics Password (A2ROOT_PASSWORD)</label><input id="s_stats_pw" type="password" autocomplete="new-password">
      <div class="hint">Login for <code>s.yourdomain.com</code> - bcrypt-hashed automatically</div></div>
  </div>
  <div class="section-title">Auto-Generated Secrets</div>
  <p class="ins-hint">These are generated fresh each install. You can regenerate them.</p>
  <div class="secret-box" id="secrets-display"></div>
  <button class="btn mt-10" data-action="genSecrets" type="button"><span class="svg-slot" data-icon="refresh"></span>Regenerate Secrets</button>
  <div class="btn-row">
    <button class="btn btn-primary" data-step-target="5" type="button">Continue <span class="svg-slot" data-icon="arrow-right"></span></button>
    <button class="btn" data-step-target="3" type="button"><span class="svg-slot" data-icon="arrow-left"></span>Back</button>
  </div>
</div>

<!-- ════ STEP 5: Cron Jobs ════ -->
<div class="step-panel" id="step5">
  <div class="step-header">
    <h2>Cron Jobs</h2>
    <p>Two cron jobs are required: daily cache cleanup and weekly GeoIP update.</p>
  </div>
  <div class="field"><label>PHP Binary Path</label><input id="cron_php" value="/usr/bin/php">
    <div class="hint">Find with: <code>which php</code> or <code>php -r "echo PHP_BINARY;"</code></div></div>
  <div class="field">
    <label class="field-toggle"><input type="checkbox" id="cron_use_cp"> Add crons via cPanel API automatically</label>
  </div>
  <div id="cron-cp-fields" class="d-none mt-8">
    <div class="alert alert-info">Uses the cPanel credentials from the Domain, Server &amp; Services step (cp_user / cp_api_token)</div>
  </div>
  <div id="s7-status"></div>
  <div id="s7-result" class="d-none"></div>
  <div class="btn-row">
    <button class="btn btn-success" data-action="addCrons" type="button"><span class="svg-slot" data-icon="settings"></span>Setup Cron Jobs</button>
    <button class="btn btn-primary" id="s7-next" data-step-target="6" type="button">Continue <span class="svg-slot" data-icon="arrow-right"></span></button>
    <button class="btn" data-step-target="4" type="button"><span class="svg-slot" data-icon="arrow-left"></span>Back</button>
  </div>
</div>

<!-- ════ STEP 6: Finalize ════ -->
<div class="step-panel" id="step6">
  <div class="step-header">
    <h2>Finalize Installation</h2>
    <p>Write <code>.env</code>, hash passwords, and lock the installer.</p>
  </div>
  <div class="alert alert-warn">After clicking <strong>Install</strong>, <code>install.lock</code> is created. Delete it if you need to reinstall.</div>
  <div id="s8-summary" class="mb-16"></div>
  <div id="s8-status"></div>
  <div id="s8-result" class="d-none"></div>
  <div class="btn-row">
    <button class="btn btn-primary" id="s8-install-btn" data-action="doFinalize" type="button"><span class="svg-slot" data-icon="rocket"></span>Install Genv2</button>
    <button class="btn" data-step-target="5" type="button"><span class="svg-slot" data-icon="arrow-left"></span>Back</button>
    <button class="btn d-none" data-step-target="7" id="s8-guide-btn" type="button">Manual Guide <span class="svg-slot" data-icon="arrow-right"></span></button>
  </div>
</div>

<!-- ════ STEP 7: Manual Guide ════ -->
<div class="step-panel" id="step7">
  <div class="step-header">
    <h2>Manual Installation Guide</h2>
    <p>Step-by-step guide for configuring web server, DNS, and cron jobs.</p>
  </div>
  <div class="guide">
    <h3>1. Prerequisites</h3>
    <ul>
      <li>PHP 8.3+ with extensions: pdo_mysql, curl, gd, json, mbstring, openssl</li>
      <li>MySQL 5.7+ or MariaDB 10.3+</li>
      <li>Apache with mod_rewrite or Nginx</li>
      <li>cPanel with wildcard subdomain support (or manual web-server vhosts)</li>
      <li>MaxMind GeoLite2 account (free)</li>
    </ul>

    <h3>2. Clone &amp; Dependencies</h3>
    <pre>git clone &lt;repo&gt; /home/user/example.com
cd /home/user/example.com
composer install --no-dev --optimize-autoloader</pre>

    <h3>3. Environment Configuration</h3>
    <pre>cp .env.example .env
# Edit .env with your credentials - or run this installer</pre>

    <h3>4. Database Setup</h3>
    <pre>mysql -u root -p -e "CREATE DATABASE genv2 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p genv2 &lt; schema.sql
# Apply migrations in order:
for f in migrations/*.sql; do mysql -u root -p genv2 &lt; "$f"; done</pre>

    <h3>5. Wildcard Subdomain (cPanel)</h3>
    <p>Create the wildcard subdomain with the redirect engine as its document root. Step 3 of this installer does it via the cPanel API, or do it manually:</p>
    <div class="cron-box">
      cPanel &rarr; Domains &rarr; Create a New Domain &rarr; <strong>*.yourdomain.com</strong> &nbsp; <strong>Document Root:</strong> <code>/home/user/public_html/redirect</code>
    </div>
    <p>Use explicit document roots for <code>gen.*</code> and <code>s.*</code> when those panels are exposed as subdomains.</p>

    <h3>6. Web Server Configuration</h3>
    <h4>cPanel: Create Addon Domains</h4>
    <ul>
      <li><code>gen.yourdomain.com</code> to Document Root: <code>/home/user/yourdomain.com/public</code></li>
      <li><code>r.yourdomain.com</code> to Document Root: <code>/home/user/yourdomain.com/redirect</code></li>
      <li><code>s.yourdomain.com</code> to Document Root: <code>/home/user/yourdomain.com/statistics</code></li>
    </ul>
    <h4>Nginx Virtual Hosts</h4>
    <pre>server {
    server_name gen.yourdomain.com;
    root /home/user/yourdomain.com/public;
    index index.php;
    location / { try_files $uri $uri/ /index.php?$query_string; }
    location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }
}
server { server_name r.yourdomain.com; root /home/user/yourdomain.com/redirect; # same php config }
server { server_name s.yourdomain.com; root /home/user/yourdomain.com/statistics; # same php config }</pre>

    <h3>7. GeoIP Database</h3>
    <pre># Add MAXMIND_LICENSE_KEY to .env first, then:
cd /home/user/yourdomain.com/redirect
php update_geoip.php
# Verify:
ls -lh databases/GeoLite2-Country.mmdb</pre>

    <h3>8. Statistics Password</h3>
    <p>The installer sets this automatically. To set manually:</p>
    <pre>php -r "echo password_hash('YOUR_PASSWORD', PASSWORD_BCRYPT, ['cost'=>10]);"
# Paste the hash into statistics/report_auth.php</pre>

    <h3>9. Cron Jobs</h3>
    <pre>crontab -e
# Add these two lines:
0 2 * * * /usr/bin/php /home/user/yourdomain.com/redirect/cleanup-cache.php >> /dev/null 2>&1
0 3 * * 0 /usr/bin/php /home/user/yourdomain.com/redirect/update-geoip.php >> /dev/null 2>&1</pre>

    <h3>10. Health Check</h3>
    <pre>curl -s https://r.yourdomain.com/health.php | python3 -m json.tool
# Should return: {"status":"ok","db":true,"geoip":true,...}</pre>

    <h3>11. Verification Checklist</h3>
    <ul>
      <li>OK - <code>gen.yourdomain.com</code> - loads admin portal</li>
      <li>OK - <code>r.yourdomain.com/health.php</code> - all checks pass</li>
      <li>OK - <code>s.yourdomain.com</code> - shows statistics login</li>
      <li>OK - Wildcard subdomain <code>*.yourdomain.com</code> created</li>
      <li>OK - GeoIP database present (<code>redirect/databases/GeoLite2-Country.mmdb</code>)</li>
      <li>OK - Cron jobs running (<code>crontab -l</code>)</li>
    </ul>
  </div>
  <div class="btn-row">
    <button class="btn" data-step-target="6" type="button"><span class="svg-slot" data-icon="arrow-left"></span>Back</button>
    <a href="/" class="btn btn-primary">Open Application</a>
  </div>
</div>

</div>
</div>

<script nonce="<?= $nonce ?>">
'use strict';
var CSRF_TOKEN = <?= json_encode((string) ($_SESSION['ins_csrf_token'] ?? ''), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;

// ── State ──────────────────────────────────────────────────────────────────────
var state = {
  step: 1,
  secrets: {},
  dbOk: false,
  schemaOk: false,
};

// ── Utilities ──────────────────────────────────────────────────────────────────
function el(id) { return document.getElementById(id); }
function val(id) { var e = el(id); return e ? e.value.trim() : ''; }
function setHtml(id, h) { var e = el(id); if (e) { e.innerHTML = h; hydrateSvgSlots(e); } }
function show(id, v) { var e = el(id); if (e) e.style.display = (v === false ? 'none' : (v || 'block')); }

function parseJsonResponse(response) {
  return response.text().then(function(text) {
    var data = null;
    try {
      data = text ? JSON.parse(text) : {};
    } catch (e) {
      return { ok: false, error: 'Server returned non-JSON response. Check install.lock, PHP error log, or server security rule.', status: response.status };
    }
    if (!response.ok && data && !data.error) {
      data.error = 'Request failed with HTTP ' + response.status;
    }
    return data;
  });
}

function post(action, data, cb) {
  var fd = new FormData();
  Object.keys(data).forEach(function(k) { fd.append(k, data[k]); });
  fd.append('csrf_token', CSRF_TOKEN);
  fetch('?action=' + encodeURIComponent(action), { method: 'POST', body: fd, credentials: 'same-origin' })
    .then(parseJsonResponse)
    .then(cb)
    .catch(function(e) { cb({ ok: false, error: e.message }); });
}

function get(action, cb) {
  fetch('?action=' + encodeURIComponent(action), { credentials: 'same-origin' })
    .then(parseJsonResponse)
    .then(cb)
    .catch(function(e) { cb({ ok: false, error: e.message }); });
}

function iconSvg(name) {
  var icons = {
    check: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>',
    x: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"></path></svg>',
    warn: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 9v4m0 4h.01"></path><path d="M10.3 4.3 2.6 18a2 2 0 0 0 1.7 3h15.4a2 2 0 0 0 1.7-3L13.7 4.3a2 2 0 0 0-3.4 0Z"></path></svg>',
    play: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7V5Z"></path></svg>',
    refresh: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12a9 9 0 0 1-15.5 6.2"></path><path d="M3 12A9 9 0 0 1 18.5 5.8"></path><path d="M18 2v4h4M6 22v-4H2"></path></svg>',
    bolt: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m13 2-10 12h8l-1 8 10-12h-8l1-8Z"></path></svg>',
    globe: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M2 12h20M12 2a15.3 15.3 0 0 1 0 20M12 2a15.3 15.3 0 0 0 0 20"></path></svg>',
    settings: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"></path><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21a2 2 0 1 1-4 0v-.09A1.7 1.7 0 0 0 8.6 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.6 8.6a1.7 1.7 0 0 0-.34-1.88l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.1V3a2 2 0 1 1 4 0v.09A1.7 1.7 0 0 0 15.4 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.24.38.6.68 1 .8.34.1.7.2 1.1.2H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.51 1Z"></path></svg>',
    rocket: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 16.5c-1 1-1.5 3-1.5 4.5 1.5 0 3.5-.5 4.5-1.5"></path><path d="M9 15 6 18l-1-4 9-9c2-2 4.5-2.8 7-2-.8 2.5 0 5-2 7l-9 9-4-1 3-3Z"></path><path d="M15 9h.01"></path></svg>',
    arrowRight: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 5l7 7-7 7"></path></svg>',
    arrowLeft: '<svg class="svg-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 19l-7-7 7-7"></path></svg>'
  };
  return icons[name] || '';
}

function hydrateSvgSlots(root) {
  var scope = root || document;
  scope.querySelectorAll('.svg-slot[data-icon]').forEach(function(slot) {
    var key = slot.getAttribute('data-icon') || '';
    key = key.replace(/-([a-z])/g, function(_, ch) { return ch.toUpperCase(); });
    slot.innerHTML = iconSvg(key);
  });
}

function alert_html(type, msg, trusted) {
  return '<div class="alert alert-' + esc(type) + '">' + (trusted ? String(msg || '') : esc(String(msg || ''))) + '</div>';
}

function spin(label) {
  return '<span class="spin"></span>' + esc(label);
}

// ── Navigation ────────────────────────────────────────────────────────────────
function goStep(n) {
  // Update panels
  document.querySelectorAll('.step-panel').forEach(function(p) { p.classList.remove('active'); });
  var panel = el('step' + n);
  if (panel) panel.classList.add('active');

  // Update sidebar
  document.querySelectorAll('#stepNav li').forEach(function(li) {
    li.classList.remove('active');
    if (parseInt(li.dataset.step) === n) li.classList.add('active');
  });

  state.step = n;

  // Step-specific init
  if (n === 3) syncDomainFields();
  if (n === 4) { if (Object.keys(state.secrets).length === 0) genSecrets(); renderSecrets(); }
  if (n === 6) renderSummary();
}

function openGuide() { goStep(7); }

function markStepDone(n, ok) {
  document.querySelectorAll('#stepNav li').forEach(function(li) {
    if (parseInt(li.dataset.step) === n) {
      li.classList.remove('done', 'error');
      li.classList.add(ok ? 'done' : 'error');
      li.querySelector('.step-dot').innerHTML = ok ? iconSvg('check') : iconSvg('x');
    }
  });
}

// ── Step 1: System Check ──────────────────────────────────────────────────────
function runDetect() {
  setHtml('s1-result', '<p>' + spin('Running system check...') + '</p>');
  el('s1-next').disabled = true;
  get('detect', function(d) {
    if (!d.ok) { setHtml('s1-result', alert_html('error', d.error || 'Detection failed')); return; }
    var html = '<ul class="check-list">';
    d.checks.forEach(function(c) {
      var icon, cls;
      if (c.ok) { icon = iconSvg('check'); cls = 'chk-ok'; }
      else if (c.req) { icon = iconSvg('x'); cls = 'chk-fail'; }
      else { icon = iconSvg('warn'); cls = 'chk-warn'; }
      html += '<li><span class="chk ' + cls + '">' + icon + '</span>'
        + '<span class="lbl">' + esc(c.label) + (c.req ? '' : ' <span class="badge-opt">[optional]</span>') + '</span>'
        + (c.note ? '<span class="note">' + esc(c.note) + '</span>' : '')
        + '</li>';
    });
    html += '</ul>';
    if (!d.can_proceed) {
      html += alert_html('error', 'Fix required items before continuing.');
    } else {
      html += alert_html('success', 'All required checks passed!');
      el('s1-next').disabled = false;
    }
    setHtml('s1-result', html);
    markStepDone(1, d.can_proceed);
  });
}

// ── Step 2: Database ──────────────────────────────────────────────────────────
function dbParams() {
  return { db_host: val('db_host'), db_port: val('db_port'), db_user: val('db_user'),
    db_pass: val('db_pass'), db_name: val('db_name'), db_socket: val('db_socket'), db_charset: val('db_charset') };
}

function testDb() {
  setHtml('s2-status', spin('Testing connection...'));
  post('test_db', dbParams(), function(d) {
    if (d.ok) {
      setHtml('s2-status', alert_html('success', 'Connected - MySQL ' + esc(d.version)));
      el('s2-import').disabled = false;
      state.dbOk = true;
    } else {
      setHtml('s2-status', alert_html('error', d.error));
      state.dbOk = false;
    }
  });
}

function importSchema() {
  setHtml('s2-status', spin('Importing schema.sql...'));
  post('import_schema', dbParams(), function(d) {
    if (d.ok) {
      setHtml('s2-status', alert_html('success', 'Schema imported - ' + d.statements + ' statements, ' + d.migrations + ' migration(s) applied'));
      el('s2-next').disabled = false;
      state.schemaOk = true;
      markStepDone(2, true);
    } else {
      setHtml('s2-status', alert_html('error', d.error));
    }
  });
}

// ── Step 3: cPanel Wildcard Subdomain ─────────────────────────────────────────
function detectIpStep4() {
  get('detect_ip', function(d) { if (d.ok) el('d_server_ip').value = d.ip; });
}

function detectCpanelHost() {
  setHtml('cp-host-status', spin('Detecting cPanel host...'));
  post('detect_cpanel_host', {
    cpanel_host: val('cp_host'),
    cpanel_port: val('cp_port') || '2083',
  }, function(d) {
    if (d.ok && d.host) {
      el('cp_host').value = d.host;
      setHtml('cp-host-status', alert_html('success', 'Detected cPanel host: <code>' + esc(d.host) + '</code>', true));
    } else {
      setHtml('cp-host-status', alert_html('warn', d.error || 'Cannot detect cPanel host'));
    }
  });
}

// ── Step 3: Wildcard cPanel domain ───────────────────────────────────────────
function provisionWildcard() {
  var domain = val('d_domain');
  if (!domain) {
    setHtml('s3-wc-status', alert_html('warn', 'Enter the primary domain first'));
    return;
  }
  var cpUser  = val('cp_user');
  var cpToken = val('cp_api_token');
  if (!cpUser || !cpToken) {
    setHtml('s3-wc-status', alert_html('warn', 'cPanel username and API token required'));
    return;
  }
  setHtml('s3-wc-status', spin('Creating *.' + domain + ' in cPanel...'));
  post('provision_wildcard', {
    domain:       domain,
    cpanel_user:  cpUser,
    cpanel_token: cpToken,
    cpanel_host:  val('cp_host') || 'localhost',
    cpanel_port:  val('cp_port') || '2083',
    cpanel_dir:   val('cp_dir'),
  }, function(d) {
    var html = '';
    if (d.results && d.results.length) {
      html += alert_html('success', d.results.map(esc).join('<br>'), true);
    }
    if (d.errors && d.errors.length) {
      html += alert_html('error', d.errors.map(esc).join('<br>'), true);
    }
    if (!html) { html = alert_html('info', 'Done'); }
    if (d.cpanel_host && el('cp_host') && (!val('cp_host') || val('cp_host') === 'localhost')) {
      el('cp_host').value = d.cpanel_host;
    }
    setHtml('s3-wc-status', html);
    markStepDone(3, !(d.errors && d.errors.length));
  });
}

// ── Step 3: Domain auto-fill ──────────────────────────────────────────────────
function syncDomainFields() {
  var domain = val('d_domain') || '';
  if (!domain) return;
  var genF = el('d_gen'); if (genF) genF.value = 'gen.' + domain;
  var redF = el('d_redirect'); if (redF) redF.value = 'r.' + domain;
  var stF  = el('d_stats');   if (stF)  stF.value  = 's.' + domain;
  var rhF  = el('d_report_host'); if (rhF && !rhF.value) rhF.value = 's.' + domain;
  var rdF  = el('d_report_dir');  if (rdF && !rdF.value) rdF.value = 'statistics';
}
el('d_domain') && el('d_domain').addEventListener('input', syncDomainFields);

// ── Step 5: Secrets ───────────────────────────────────────────────────────────
function randHex() {
  var arr = new Uint8Array(32);
  window.crypto.getRandomValues(arr);
  return Array.from(arr, function(b) { return b.toString(16).padStart(2,'0'); }).join('');
}

function genSecrets() {
  state.secrets = {
    AF_SECRET: randHex(),
    IXG_BYPASS_KEY: randHex(),
    SRP_API_KEY: randHex(),
    POSTBACK_SECRET: randHex(),
  };
  renderSecrets();
}

function renderSecrets() {
  var html = '';
  Object.keys(state.secrets).forEach(function(k) {
    html += '<div class="secret-item"><div class="sk">' + esc(k) + '</div><div class="sv">' + esc(state.secrets[k]) + '</div></div>';
  });
  setHtml('secrets-display', html);
}

// ── Step 7: Crons ─────────────────────────────────────────────────────────────
el('cron_use_cp') && el('cron_use_cp').addEventListener('change', function() {
  show('cron-cp-fields', this.checked);
});

function addCrons() {
  var useCp = el('cron_use_cp').checked;
  setHtml('s7-status', spin('Setting up cron jobs...'));
  post('add_crons', {
    php_bin: val('cron_php'),
    method: useCp ? 'cpanel' : 'manual',
    cpanel_user: val('cp_user'),
    cpanel_token: val('cp_api_token'),
    cpanel_host: val('cp_host'),
    cpanel_port: val('cp_port') || '2083',
  }, function(d) {
    setHtml('s7-status', '');
    var html = '';
    if (!d.ok && d.error) {
      html += alert_html('error', d.error);
    }
    if (d.method === 'cpanel') {
      if (d.added && d.added.length) html += alert_html('success', 'Added via cPanel: ' + esc(d.added.join(', ')), true);
      if (d.errors && d.errors.length) html += alert_html('warn', 'Partial: ' + esc(d.errors.join(' | ')), true);
    }
    if (d.warnings && d.warnings.length) {
      html += alert_html('warn', esc(d.warnings.join(' | ')), true);
    }
    html += '<div class="section-title">Crontab Lines (add manually if needed)</div>';
    html += '<pre>' + esc((d.lines || []).join('\n')) + '</pre>';
    setHtml('s7-result', html);
    show('s7-result', true);
    markStepDone(5, Boolean(d.ok));
  });
}

// ── Step 8: Finalize ──────────────────────────────────────────────────────────
function renderSummary() {
  var domain = val('d_domain') || '—';
  var rows = [
    ['Domain', domain],
    ['Server IP', val('d_server_ip') || '—'],
    ['Database', val('db_user') + '@' + val('db_host') + '/' + val('db_name')],
  ];
  var html = '<ul class="check-list">';
  rows.forEach(function(r) {
    html += '<li><span class="lbl">' + esc(r[0]) + '</span><code>' + esc(r[1]) + '</code></li>';
  });
  html += '</ul>';
  setHtml('s8-summary', html);
}

function doFinalize() {
  var domain = val('d_domain');
  if (!domain) { setHtml('s8-status', alert_html('warn', 'Domain is required')); return; }
  if (!val('db_user') || !val('db_name')) { setHtml('s8-status', alert_html('warn', 'Database username and database name are required')); return; }

  el('s8-install-btn').disabled = true;
  setHtml('s8-status', spin('Writing .env and locking installer...'));

  post('finalize', {
    // DB
    db_host: val('db_host'), db_port: val('db_port'), db_user: val('db_user'),
    db_pass: val('db_pass'), db_name: val('db_name'), db_socket: val('db_socket'), db_charset: val('db_charset'),
    // Admin
    admin_password: val('s_admin_pw'), a2root_password: val('s_stats_pw'),
    // cPanel
    cpanel_user: val('cp_user'), cpanel_pass: val('cp_pass'), cpanel_host: val('cp_host'),
    cpanel_port: val('cp_port'), cpanel_token: val('cp_api_token'), cpanel_dir: val('cp_dir'),
    // Domain & server
    domain: domain, server_ip: val('d_server_ip'),
    report_host: val('d_report_host'), report_dir: val('d_report_dir'),
    // Secrets
    af_secret: state.secrets.AF_SECRET || '', ixg_bypass_key: state.secrets.IXG_BYPASS_KEY || '',
    srp_api_key: state.secrets.SRP_API_KEY || '', postback_secret: state.secrets.POSTBACK_SECRET || '',
    // Services
    maxmind_key: val('maxmind_key'), block_countries: val('block_countries'),
    filter_url: val('filter_url'), postback_tz: val('pb_tz'),
    trackr_api_key: val('trackr_key'), trackr_endpoint: val('trackr_ep'), tinyurl_api_key: val('tinyurl_key'),
  }, function(d) {
    el('s8-install-btn').disabled = false;
    if (d.ok) {
      var html = '<div class="complete-card">';
      html += '<div class="complete-badge">' + iconSvg('check') + '</div>';
      html += '<h2 class="complete-title">Installation Complete</h2>';
      html += '<p class="complete-sub"><code>.env</code> written and <code>install.lock</code> created. The installer is now locked.</p>';
      html += '<div class="section-title">Generated Secrets (save these now - shown once)</div>';
      html += '<div class="secret-box">';
      Object.keys(d.secrets || {}).forEach(function(k) {
        html += '<div class="secret-item"><div class="sk">' + esc(k) + '</div><div class="sv">' + esc(d.secrets[k]) + '</div></div>';
      });
      html += '</div>';
      html += '<div class="btn-row complete-actions">';
      var adminHost = val('d_gen') || ('gen.' + domain);
      html += '<a href="https://' + esc(adminHost) + '/login.php" class="btn btn-primary" target="_blank" rel="noopener"><span class="svg-slot" data-icon="rocket"></span>Admin Dashboard Login</a>';
      html += '<button type="button" class="btn" data-action="openGuide"><span class="svg-slot" data-icon="arrow-right"></span>Manual Guide</button>';
      html += '</div></div>';
      setHtml('s8-result', html);
      show('s8-result', true);
      // Replace the wizard controls with the completion view.
      show('s8-summary', false);
      show('s8-install-btn', false);
      markStepDone(6, true);
      setHtml('s8-status', '');
    } else {
      setHtml('s8-status', alert_html('error', d.error));
    }
  });
}

// ── Inverse / Faded Canvas ───────────────────────────────────────────────────
function drawInverseFadedCanvas() {
  var canvas = el('inverseFadedCanvas');
  if (!canvas || !canvas.getContext) {
    return;
  }

  var wrap = canvas.parentNode;
  var rect = wrap.getBoundingClientRect();
  var ratio = window.devicePixelRatio || 1;
  var width = Math.max(320, Math.floor(rect.width));
  var height = Math.max(180, Math.floor(rect.height));

  canvas.width = Math.floor(width * ratio);
  canvas.height = Math.floor(height * ratio);

  var ctx = canvas.getContext('2d');
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
  ctx.clearRect(0, 0, width, height);

  var inverse = '#292b2c';
  var faded = '#f7f7f7';

  ctx.fillStyle = faded;
  ctx.fillRect(0, 0, width, height);

  ctx.save();
  ctx.globalAlpha = 0.055;
  ctx.strokeStyle = inverse;
  ctx.lineWidth = 1;
  var x;
  var y;
  for (x = 16; x < width; x += 24) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, height);
    ctx.stroke();
  }
  for (y = 16; y < height; y += 24) {
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(width, y);
    ctx.stroke();
  }
  ctx.restore();

  var pad = 18;
  var innerH = Math.max(110, height - 104);

  ctx.save();
  ctx.globalAlpha = 0.045;
  ctx.fillStyle = inverse;
  roundedCanvasRect(ctx, pad, pad, Math.min(220, width - (pad * 2)), 30, 5);
  ctx.fill();
  roundedCanvasRect(ctx, Math.max(pad, width - 238), pad, Math.min(220, width - (pad * 2)), 30, 5);
  ctx.fill();
  ctx.restore();

  ctx.save();
  ctx.globalAlpha = 0.075;
  ctx.strokeStyle = inverse;
  ctx.lineWidth = 1;
  roundedCanvasRect(ctx, pad, pad, width - (pad * 2), innerH, 5);
  ctx.stroke();
  ctx.restore();
}

function roundedCanvasRect(ctx, x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}

function initInverseFadedCanvas() {
  drawInverseFadedCanvas();
  window.addEventListener('resize', function() {
    drawInverseFadedCanvas();
  });
}

function initStaticGatewayLogo() {
  var logo = document.querySelector('.logo');
  if (!logo) {
    return;
  }
  logo.addEventListener('contextmenu', function(event) {
    event.preventDefault();
  });
  logo.addEventListener('dragstart', function(event) {
    event.preventDefault();
  });
}

// ── Escape helper ─────────────────────────────────────────────────────────────
function esc(s) {
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

// ── Init ──────────────────────────────────────────────────────────────────────

function bindActions() {
  hydrateSvgSlots(document);
  document.querySelectorAll('[data-step-target]').forEach(function(btn) {
    btn.addEventListener('click', function() {
      var target = parseInt(btn.getAttribute('data-step-target') || '0', 10);
      if (target > 0) {
        goStep(target);
      }
    });
  });
  var actions = {
    runDetect: runDetect,
    testDb: testDb,
    importSchema: importSchema,
    detectIpStep4: detectIpStep4,
    detectCpanelHost: detectCpanelHost,
    provisionWildcard: provisionWildcard,
    genSecrets: genSecrets,
    addCrons: addCrons,
    doFinalize: doFinalize,
    openGuide: openGuide
  };
  document.addEventListener('click', function(event) {
    var trigger = event.target.closest('[data-action]');
    if (!trigger) {
      return;
    }
    event.preventDefault();
    var action = trigger.getAttribute('data-action') || '';
    if (Object.prototype.hasOwnProperty.call(actions, action)) {
      actions[action]();
    }
  });
}

document.addEventListener('DOMContentLoaded', function() {
  bindActions();
  initInverseFadedCanvas();
  initStaticGatewayLogo();
  runDetect();
  if (el('d_domain') && '<?= $preDomain ?>') {
    el('d_domain').value = '<?= $preDomain ?>';
    syncDomainFields();
  }
});
</script>
</body>
</html>
