<?php

declare(strict_types=1);

/**
 * Installation wizard (M1).
 */

$root = dirname(__DIR__);
// Packaging-safe: the installer may be executed from /public/install/.
// In that case dirname(__DIR__) points to /public and app/bootstrap.php is one level above.
if (!is_readable($root . '/app/bootstrap.php')) {
    $root = dirname(__DIR__, 2);
}
require $root . '/app/bootstrap.php';

use SeoScope\Database\MigrationRunner;
use SeoScope\Http\Request;
use SeoScope\Install\EnvironmentChecker;
use SeoScope\Install\LocalConfigWriter;
use SeoScope\i18n\TranslatorBootstrap;
use SeoScope\Security\CsrfToken;

session_name('seoscope_install');
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'secure' => Request::isHttps(),
    'httponly' => true,
    'samesite' => 'Lax',
]);
session_start();

TranslatorBootstrap::registerDefault($root);

$lockFile = $root . '/storage/app/install.lock';
if (is_readable($lockFile)) {
    respondAlreadyInstalled();
}

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';

if ($method === 'POST') {
    handleInstallPost($root);
    exit;
}

$step = preg_replace('/[^a-z]/', '', strtolower((string) ($_GET['step'] ?? 'a')));
if ($step !== 'a' && $step !== 'b' && $step !== 'c') {
    $step = 'a';
}

if ($step === 'b' && empty($_SESSION['install_env_ok'])) {
    header('Location: index.php?step=a', true, 302);
    exit;
}
if ($step === 'c' && empty($_SESSION['install_db'])) {
    header('Location: index.php?step=b', true, 302);
    exit;
}

if ($step === 'a') {
    renderStepA($root);
} elseif ($step === 'b') {
    renderStepB($root);
} else {
    renderStepC($root);
}

// --- handlers ----------------------------------------------------------------

function respondAlreadyInstalled(): void
{
    header('Content-Type: text/html; charset=utf-8');
    http_response_code(403);
    echo installLayout(
        __('install.already.title'),
        '<div class="alert alert-warning mb-0">' . h(__('install.already.body')) . '</div>
         <div class="mt-3"><a class="btn btn-upgrade" href="/">' . h(__('app.name')) . '</a></div>'
    );
    exit;
}

/**
 * @throws JsonException
 */
function handleInstallPost(string $root): void
{
    $step = preg_replace('/[^a-z]/', '', strtolower((string) ($_POST['step'] ?? '')));
    $token = $_POST['_csrf'] ?? null;

    if (!CsrfToken::validate(is_string($token) ? $token : null)) {
        renderError(__('install.csrf_invalid'));
        return;
    }

    if ($step === 'a') {
        $checker = new EnvironmentChecker();
        if (!$checker->run($root)) {
            header('Location: index.php?step=a', true, 302);
            exit;
        }
        $_SESSION['install_env_ok'] = true;
        header('Location: index.php?step=b', true, 302);
        exit;
    }

    if ($step === 'b') {
        if (empty($_SESSION['install_env_ok'])) {
            header('Location: index.php?step=a', true, 302);
            exit;
        }
        $host = trim((string) ($_POST['db_host'] ?? ''));
        $port = (int) ($_POST['db_port'] ?? 3306);
        $name = trim((string) ($_POST['db_name'] ?? ''));
        $user = trim((string) ($_POST['db_user'] ?? ''));
        $pass = (string) ($_POST['db_password'] ?? '');
        if ($host === '' || $name === '' || $user === '') {
            $_SESSION['install_error'] = __('install.error.db_missing_fields');
            header('Location: index.php?step=b', true, 302);
            exit;
        }
        if ($port < 1 || $port > 65535) {
            $_SESSION['install_error'] = __('install.error.db_port_invalid');
            header('Location: index.php?step=b', true, 302);
            exit;
        }
        try {
            $pdo = createPdoConnection($host, $port, $name, $user, $pass);
            $pdo->query('SELECT 1');
        } catch (PDOException $e) {
            $_SESSION['install_error'] = mapDbConnectError($e);
            header('Location: index.php?step=b', true, 302);
            exit;
        } catch (Throwable $e) {
            $_SESSION['install_error'] = __('install.error.db_connection');
            header('Location: index.php?step=b', true, 302);
            exit;
        }
        $_SESSION['install_db'] = [
            'host' => $host,
            'port' => $port,
            'database' => $name,
            'username' => $user,
            'password' => $pass,
        ];
        unset($_SESSION['install_error']);
        header('Location: index.php?step=c', true, 302);
        exit;
    }

    if ($step === 'c') {
        if (empty($_SESSION['install_db'])) {
            header('Location: index.php?step=b', true, 302);
            exit;
        }
        $email = trim((string) ($_POST['admin_email'] ?? ''));
        $displayName = trim((string) ($_POST['admin_name'] ?? ''));
        $password = (string) ($_POST['admin_password'] ?? '');
        $password2 = (string) ($_POST['admin_password2'] ?? '');

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $_SESSION['install_error'] = __('install.error.invalid_email');
            header('Location: index.php?step=c', true, 302);
            exit;
        }
        if (strlen($password) < 12) {
            $_SESSION['install_error'] = __('install.error.password_short');
            header('Location: index.php?step=c', true, 302);
            exit;
        }
        if (!hash_equals($password, $password2)) {
            $_SESSION['install_error'] = __('install.error.password_mismatch');
            header('Location: index.php?step=c', true, 302);
            exit;
        }

        $example = require $root . '/config/config.example.php';
        /** @var array<string, mixed> $example */
        $tablePrefix = (string) ($example['db']['table_prefix'] ?? 'sscope_');
        if (!preg_match('/^[a-zA-Z0-9_]+$/', $tablePrefix)) {
            $tablePrefix = 'sscope_';
        }

        $db = $_SESSION['install_db'];
        $config = [
            'app' => [
                'env' => 'production',
                'debug' => false,
                'url' => Request::guessedBaseUrl(),
                'base_path' => '',
                'installed_at' => date('c'),
                'key' => bin2hex(random_bytes(32)),
            ],
            'db' => [
                'host' => $db['host'],
                'port' => $db['port'],
                'database' => $db['database'],
                'username' => $db['username'],
                'password' => $db['password'],
                'charset' => 'utf8mb4',
                'table_prefix' => $tablePrefix,
            ],
        ];

        $localPath = $root . '/config/local.php';

        try {
            if (is_readable($localPath)) {
                renderError(__('install.error.config_exists'));
                return;
            }
            LocalConfigWriter::write($localPath, $config);
        } catch (Throwable $e) {
            renderError(__('install.error.config_write'));
            return;
        }

        try {
            $written = require $localPath;
            $pdo = createPdoFromConfig($written);
            $runner = new MigrationRunner($pdo, $tablePrefix);
            $runner->runAll($root . '/database/migrations');

            $now = date('Y-m-d H:i:s');
            $hash = password_hash($password, PASSWORD_DEFAULT);
            $userTable = '`' . str_replace('`', '``', $tablePrefix . 'users') . '`';
            $stmt = $pdo->prepare(
                "INSERT INTO {$userTable} (email, password_hash, name, role, created_at, updated_at)
                 VALUES (?, ?, ?, 'admin', ?, ?)"
            );
            $stmt->execute([
                $email,
                $hash,
                $displayName !== '' ? $displayName : null,
                $now,
                $now,
            ]);
        } catch (Throwable $e) {
            @unlink($localPath);
            renderError(__('install.error.generic', ['message' => $e->getMessage()]));
            return;
        }

        $lockPayload = json_encode(
            [
                'installed_at' => date('c'),
                'version' => SEOSCOPE_VERSION,
            ],
            JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
        );
        $lockPath = $root . '/storage/app/install.lock';
        $lockDir = dirname($lockPath);
        if (!is_dir($lockDir) || !is_writable($lockDir)) {
            renderError(__('install.error.generic', ['message' => 'storage/app not writable']));
            return;
        }
        if (@file_put_contents($lockPath, $lockPayload, LOCK_EX) === false) {
            renderError(__('install.error.generic', ['message' => 'install.lock']));
            return;
        }

        $installationKey = sprintf(
            "%s\n",
            bin2hex(random_bytes(16)) . bin2hex(random_bytes(16))
        );
        file_put_contents($root . '/storage/app/installation.key', $installationKey);

        unset($_SESSION['install_db'], $_SESSION['install_env_ok'], $_SESSION['install_error']);
        CsrfToken::clear();
        session_destroy();

        header('Location: ' . Request::guessedBaseUrl() . '/?installed=1', true, 302);
        exit;
    }

    header('Location: index.php?step=a', true, 302);
}

function createPdoConnection(string $host, int $port, string $database, string $username, string $password): PDO
{
    $dsn = sprintf(
        'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
        $host,
        $port,
        $database
    );
    return new PDO($dsn, $username, $password, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]);
}

/**
 * @param array<string, mixed> $config
 */
function createPdoFromConfig(array $config): PDO
{
    $db = $config['db'];
    return createPdoConnection(
        (string) $db['host'],
        (int) $db['port'],
        (string) $db['database'],
        (string) $db['username'],
        (string) $db['password']
    );
}

// --- views -------------------------------------------------------------------

function renderError(string $message): void
{
    header('Content-Type: text/html; charset=utf-8');
    http_response_code(500);
    $extra = '<div class="dashboard-card section-card mt-3"><div class="section-title mb-2">' . h(__('install.rollback.title')) . '</div><div class="section-subtitle mb-0">' . h(__('install.rollback.body')) . '</div></div>';
    echo installLayout(
        __('install.required.title'),
        '<div class="alert alert-danger mb-0">' . h($message) . '</div>' . $extra . '<div class="mt-3"><a class="btn btn-upgrade" href="index.php?step=a">' . h(__('install.step_b.back')) . '</a></div>'
    );
}

function renderStepA(string $root): void
{
    $checker = new EnvironmentChecker();
    $ok = $checker->run($root);
    $items = $checker->getItems();
    $csrf = CsrfToken::generate();

    ob_start();
    echo '<p class="section-subtitle">' . h(__('install.step_a.intro')) . '</p>';
    echo '<div class="mt-3">';
    if (count($items) > 0) {
        echo '<ul class="list-group">';
        foreach ($items as $item) {
            $isErr = $item['level'] === 'error';
            $msg = __($item['key'], $item['params'] ?? []);
            $icon = $isErr ? 'fa-solid fa-triangle-exclamation' : 'fa-solid fa-circle-info';
            $textClass = $isErr ? 'text-danger' : 'text-warning';
            echo '<li class="list-group-item d-flex gap-2 align-items-start">';
            echo '<i class="' . h($icon) . ' ' . h($textClass) . '" style="margin-top:2px;"></i>';
            echo '<div>' . h($msg) . '</div>';
            echo '</li>';
        }
        echo '</ul>';
    }
    echo '</div>';

    if ($ok) {
        echo '<form method="post" action="index.php" class="mt-4">';
        echo '<input type="hidden" name="step" value="a">';
        echo '<input type="hidden" name="_csrf" value="' . h($csrf) . '">';
        echo '<button class="btn-login" type="submit">' . h(__('install.step_a.continue')) . '</button>';
        echo '</form>';
    } else {
        echo '<div class="alert alert-danger mt-3 mb-0">' . h(__('install.step_a.refresh')) . '</div>';
    }

    $body = ob_get_clean();
    header('Content-Type: text/html; charset=utf-8');
    echo installLayout(__('install.step_a.title'), $body);
}

function renderStepB(string $root): void
{
    $csrf = CsrfToken::generate();
    $err = $_SESSION['install_error'] ?? '';
    if (is_string($err) && $err !== '') {
        unset($_SESSION['install_error']);
    } else {
        $err = '';
    }

    $db = $_SESSION['install_db'] ?? [];

    ob_start();
    if ($err !== '') {
        echo '<div class="alert alert-danger">' . h($err) . '</div>';
    }
    echo '<p class="section-subtitle">' . h(__('install.step_b.intro')) . '</p>';
    echo '<form method="post" action="index.php" class="mt-4">';
    echo '<input type="hidden" name="step" value="b">';
    echo '<input type="hidden" name="_csrf" value="' . h($csrf) . '">';
    echo '<div class="mb-4"><label for="db_host" class="form-label">' . h(__('install.db.host')) . '</label><input id="db_host" class="form-control" name="db_host" required value="' . h((string) ($db['host'] ?? '127.0.0.1')) . '"></div>';
    echo '<div class="mb-4"><label for="db_port" class="form-label">' . h(__('install.db.port')) . '</label><input id="db_port" class="form-control" name="db_port" type="number" required value="' . h((string) ($db['port'] ?? '3306')) . '"></div>';
    echo '<div class="mb-4"><label for="db_name" class="form-label">' . h(__('install.db.name')) . '</label><input id="db_name" class="form-control" name="db_name" required value="' . h((string) ($db['database'] ?? '')) . '"></div>';
    echo '<div class="mb-4"><label for="db_user" class="form-label">' . h(__('install.db.user')) . '</label><input id="db_user" class="form-control" name="db_user" required value="' . h((string) ($db['username'] ?? '')) . '"></div>';
    echo '<div class="mb-4"><label for="db_password" class="form-label">' . h(__('install.db.password')) . '</label><input id="db_password" class="form-control" type="password" name="db_password" value=""></div>';
    echo '<button class="btn-login" type="submit">' . h(__('install.step_b.test')) . '</button>';
    echo '<div class="mt-3"><a class="btn btn-link" href="index.php?step=a">' . h(__('install.step_b.back')) . '</a></div>';
    echo '</form>';

    $body = ob_get_clean();
    header('Content-Type: text/html; charset=utf-8');
    echo installLayout(__('install.step_b.title'), $body);
}

function renderStepC(string $root): void
{
    $csrf = CsrfToken::generate();
    $err = $_SESSION['install_error'] ?? '';
    if (is_string($err) && $err !== '') {
        unset($_SESSION['install_error']);
    } else {
        $err = '';
    }

    ob_start();
    if ($err !== '') {
        echo '<div class="alert alert-danger">' . h($err) . '</div>';
    }
    echo '<p class="section-subtitle">' . h(__('install.step_c.intro')) . '</p>';
    echo '<form method="post" action="index.php" class="mt-4">';
    echo '<input type="hidden" name="step" value="c">';
    echo '<input type="hidden" name="_csrf" value="' . h($csrf) . '">';
    echo '<div class="mb-4"><label for="admin_email" class="form-label">' . h(__('install.admin.email')) . '</label><input id="admin_email" class="form-control" type="email" name="admin_email" required autocomplete="username"></div>';
    echo '<div class="mb-4"><label for="admin_name" class="form-label">' . h(__('install.admin.name')) . '</label><input id="admin_name" class="form-control" name="admin_name" autocomplete="name"></div>';
    echo '<div class="mb-4"><label for="admin_password" class="form-label">' . h(__('install.admin.password')) . '</label><input id="admin_password" class="form-control" type="password" name="admin_password" required minlength="12" autocomplete="new-password"></div>';
    echo '<div class="mb-4"><label for="admin_password2" class="form-label">' . h(__('install.admin.password_confirm')) . '</label><input id="admin_password2" class="form-control" type="password" name="admin_password2" required minlength="12" autocomplete="new-password"></div>';
    echo '<button class="btn-login" type="submit">' . h(__('install.step_c.finish')) . '</button>';
    echo '<div class="mt-3"><a class="btn btn-link" href="index.php?step=b">' . h(__('install.step_b.back')) . '</a></div>';
    echo '</form>';

    $body = ob_get_clean();
    header('Content-Type: text/html; charset=utf-8');
    echo installLayout(__('install.step_c.title'), $body);
}

function installLayout(string $title, string $body): string
{
    $app = h(__('app.name'));
    $ver = h(SEOSCOPE_VERSION);
    $titleEsc = h($title);
    return <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{$app} — {$titleEsc}</title>
    <link rel="stylesheet" href="/assets/fonts/inter/inter.css">
    <link rel="stylesheet" href="/assets/vendor/bootstrap/bootstrap.min.css">
    <link rel="stylesheet" href="/assets/vendor/fontawesome/css/all.min.css">
    <link rel="stylesheet" href="/assets/admin-template/css/admin-template.css">
</head>
<body class="login-page">
    <div class="login-shell">
        <div class="login-wrapper" style="max-width: 760px;">
            <div class="row g-0">
                <div class="col-12">
                    <div class="login-right" style="min-height: auto;">
                        <div class="login-card">
                            <div class="brand mb-4">
                                <span class="brand-icon"><i class="fa-solid fa-wave-square"></i></span>
                                <span>{$app}</span>
                            </div>
                            <h2 class="fw-800">{$titleEsc}</h2>
                            <p class="subtitle">{$ver}</p>
                            {$body}
                            <p class="helper-text mt-4 mb-0"><a href="/">{$app}</a></p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="/assets/vendor/bootstrap/bootstrap.bundle.min.js"></script>
</body>
</html>
HTML;
}

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

function mapDbConnectError(PDOException $e): string
{
    // PDO MySQL exposes driver error code in errorInfo[1] for many failures.
    $info = $e->errorInfo ?? null;
    $driverCode = is_array($info) && isset($info[1]) ? (int) $info[1] : 0;
    return match ($driverCode) {
        1045 => __('install.error.db_access_denied'),
        1049 => __('install.error.db_unknown_database'),
        2002 => __('install.error.db_host_unreachable'),
        default => __('install.error.db_connection'),
    };
}
