<?php
declare(strict_types=1);

if (session_status() === PHP_SESSION_NONE) {
    $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
    ini_set('session.use_strict_mode', '1');
    ini_set('session.use_only_cookies', '1');
    session_set_cookie_params([
        'lifetime' => 0,
        'path' => '/',
        'secure' => $https,
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
    session_start();
}

defined('DB_PATH') || define('DB_PATH', getenv('ADNOVEL_DB_PATH') ?: '/var/lib/adnovel/blog.db');
defined('BASE_URL') || define('BASE_URL', getenv('ADNOVEL_BASE_URL') ?: '/blog');

// 旧版会话通道（无 uid 的 $_SESSION['admin']）作废，需重新登录
if (isset($_SESSION['admin']) && empty($_SESSION['uid'])) {
    unset($_SESSION['admin']);
}

function db(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $pdo = new PDO('sqlite:' . DB_PATH);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->exec('PRAGMA foreign_keys = ON');
    }
    return $pdo;
}

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

function safe_local_redirect(string $path): ?string {
    if ($path === '' || $path[0] !== '/' || str_starts_with($path, '//') || preg_match('/[\r\n]/', $path)) {
        return null;
    }
    $parts = parse_url($path);
    if ($parts === false || isset($parts['scheme']) || isset($parts['host'])) {
        return null;
    }
    return $path;
}

// ---------------- schema 迁移（幂等，每次加载时保证结构） ----------------
function ensure_schema(PDO $pdo): void {
    $tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(PDO::FETCH_COLUMN);

    if (!in_array('users', $tables, true)) {
        $pdo->exec("CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            username TEXT NOT NULL UNIQUE,
            password_hash TEXT NOT NULL,
            role TEXT NOT NULL DEFAULT 'user',
            created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
        )");
    }
    if (!in_array('groups', $tables, true)) {
        $pdo->exec("CREATE TABLE groups (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL UNIQUE
        )");
    }
    if (!in_array('user_groups', $tables, true)) {
        $pdo->exec("CREATE TABLE user_groups (
            user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
            PRIMARY KEY (user_id, group_id)
        )");
    }
    if (!in_array('comments', $tables, true)) {
        $pdo->exec("CREATE TABLE comments (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            article_id INTEGER NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
            user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            content TEXT NOT NULL,
            created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
        )");
    }
    if (!in_array('article_groups', $tables, true)) {
        $pdo->exec("CREATE TABLE article_groups (
            article_id INTEGER NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
            group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
            PRIMARY KEY (article_id, group_id)
        )");
    }
    if (!in_array('games', $tables, true)) {
        $pdo->exec("CREATE TABLE games (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            icon TEXT NOT NULL DEFAULT '',
            description TEXT NOT NULL DEFAULT '',
            file TEXT NOT NULL,
            enabled INTEGER NOT NULL DEFAULT 1,
            created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
        )");
    }
    if (!in_array('daily_checkins', $tables, true)) {
        $pdo->exec("CREATE TABLE daily_checkins (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            checkin_date TEXT NOT NULL,
            credit_awarded INTEGER NOT NULL DEFAULT 10,
            created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
            UNIQUE(user_id, checkin_date)
        )");
    }
    if (!in_array('feedback', $tables, true)) {
        $pdo->exec("CREATE TABLE feedback (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
            name TEXT NOT NULL DEFAULT '',
            contact TEXT NOT NULL DEFAULT '',
            content TEXT NOT NULL,
            status TEXT NOT NULL DEFAULT 'new',
            created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
        )");
    }
    if (!in_array('game_scores', $tables, true)) {
        $pdo->exec("CREATE TABLE game_scores (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            game_key TEXT NOT NULL,
            mode TEXT NOT NULL,
            score_value REAL NOT NULL DEFAULT 0,
            duration_ms INTEGER NOT NULL DEFAULT 0,
            meta_json TEXT NOT NULL DEFAULT '{}',
            achieved_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
            updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
            UNIQUE(user_id, game_key, mode)
        )");
    }

    $cols = [];
    foreach ($pdo->query('PRAGMA table_info(articles)') as $row) {
        $cols[] = $row['name'];
    }
    if (!in_array('visibility', $cols, true)) {
        $pdo->exec("ALTER TABLE articles ADD COLUMN visibility TEXT NOT NULL DEFAULT 'public'");
    }

    $userCols = [];
    foreach ($pdo->query('PRAGMA table_info(users)') as $row) {
        $userCols[] = $row['name'];
    }
    if (!in_array('credit', $userCols, true)) {
        $pdo->exec("ALTER TABLE users ADD COLUMN credit INTEGER NOT NULL DEFAULT 0");
    }
    if (!in_array('is_vip', $userCols, true)) {
        $pdo->exec("ALTER TABLE users ADD COLUMN is_vip INTEGER NOT NULL DEFAULT 0");
    }

    $pdo->exec('CREATE INDEX IF NOT EXISTS idx_checkins_user_date ON daily_checkins(user_id, checkin_date)');
    $pdo->exec('CREATE INDEX IF NOT EXISTS idx_feedback_status_created ON feedback(status, created_at DESC)');
    $pdo->exec('CREATE INDEX IF NOT EXISTS idx_game_scores_board ON game_scores(game_key, mode, score_value, duration_ms)');
    $pdo->exec('CREATE INDEX IF NOT EXISTS idx_game_scores_user ON game_scores(user_id, updated_at DESC)');

    // 旧 admin 密码哈希迁入 users 表（仅当 users 表为空且存在旧哈希）
    $cnt = (int)$pdo->query('SELECT count(*) FROM users')->fetchColumn();
    if ($cnt === 0) {
        $st = $pdo->prepare('SELECT value FROM settings WHERE key = ?');
        $st->execute(['admin_password_hash']);
        $hash = $st->fetchColumn();
        if ($hash !== false) {
            $ins = $pdo->prepare('INSERT INTO users(username, password_hash, role) VALUES(?,?,?)');
            $ins->execute(['admin', (string)$hash, 'admin']);
        }
    }
}

// ---------------- 认证 ----------------
function current_user(): ?array {
    if (empty($_SESSION['uid'])) {
        return null;
    }
    static $cache = [];
    $uid = (int)$_SESSION['uid'];
    if (array_key_exists($uid, $cache)) {
        return $cache[$uid];
    }
    $st = db()->prepare('SELECT * FROM users WHERE id = ?');
    $st->execute([$uid]);
    $u = $st->fetch(PDO::FETCH_ASSOC);
    $cache[$uid] = $u ?: null;
    return $cache[$uid];
}

function current_user_id(): ?int {
    $u = current_user();
    return $u ? (int)$u['id'] : null;
}

function is_logged_in(): bool {
    return current_user() !== null;
}

function is_admin(): bool {
    $u = current_user();
    return $u !== null && $u['role'] === 'admin';
}

function require_admin(): void {
    if (!is_admin()) {
        header('Location: ' . BASE_URL . '/login.php');
        exit;
    }
}

function require_login(): void {
    if (!is_logged_in()) {
        $next = $_SERVER['REQUEST_URI'] ?? BASE_URL . '/';
        header('Location: ' . BASE_URL . '/login.php?next=' . urlencode($next));
        exit;
    }
}

// ---------------- CREDIT / 签到 ----------------
function credit_level(int $credit): int {
    return min(99, intdiv(max(0, $credit), 100) + 1);
}

function game_leaderboard_catalog(): array {
    return [
        'schulte' => [
            'title' => '舒尔特方格', 'metric' => 'time', 'direction' => 'asc',
            'modes' => ['5x5' => '5×5', '7x7' => '7×7', '10x10' => '10×10', 'hard' => '困难', 'hell' => '地狱魔'],
            'min_duration' => 1200, 'max_duration' => 3600000, 'min_score' => 0, 'max_score' => 10000,
        ],
        'minesweeper' => [
            'title' => '扫雷', 'metric' => 'time', 'direction' => 'asc',
            'modes' => ['classic' => '经典', 'heat' => '热力', 'compass' => '罗盘', 'color' => '多色', 'fog' => '迷雾'],
            'min_duration' => 2500, 'max_duration' => 3600000, 'min_score' => 0, 'max_score' => 10000,
        ],
        '2048' => [
            'title' => '2048', 'metric' => 'score', 'direction' => 'desc',
            'modes' => ['classic' => '经典'],
            'min_duration' => 1000, 'max_duration' => 86400000, 'min_score' => 0, 'max_score' => 100000000,
        ],
        'flappy' => [
            'title' => '爱飞行的小小鸟', 'metric' => 'score', 'direction' => 'desc',
            'modes' => ['classic' => '经典'],
            'min_duration' => 500, 'max_duration' => 86400000, 'min_score' => 0, 'max_score' => 10000000,
        ],
        'breakout' => [
            'title' => '打砖块', 'metric' => 'score', 'direction' => 'desc',
            'modes' => ['arcade' => '街机'],
            'min_duration' => 1000, 'max_duration' => 86400000, 'min_score' => 0, 'max_score' => 1000000000,
        ],
        'internet_exam' => [
            'title' => '互联网通史', 'metric' => 'score', 'direction' => 'desc',
            'modes' => ['exam' => '考试模式'],
            'min_duration' => 1000, 'max_duration' => 14400000, 'min_score' => 0, 'max_score' => 100,
        ],
        'quizlab' => [
            'title' => '知衡 QuizLab', 'metric' => 'score', 'direction' => 'desc',
            'modes' => ['exam' => '考试模式'],
            'min_duration' => 1000, 'max_duration' => 14400000, 'min_score' => 0, 'max_score' => 100,
        ],
        'physics_exam' => [
            'title' => '伏安法测电阻', 'metric' => 'score', 'direction' => 'desc',
            'modes' => ['exam' => '12 分钟考核'],
            'min_duration' => 1000, 'max_duration' => 900000, 'min_score' => 0, 'max_score' => 100,
        ],
    ];
}

function checked_in_today(int $uid): bool {
    $st = db()->prepare("SELECT 1 FROM daily_checkins WHERE user_id = ? AND checkin_date = date('now','localtime')");
    $st->execute([$uid]);
    return (bool)$st->fetchColumn();
}

function claim_daily_checkin(int $uid): array {
    $pdo = db();
    $award = 10;
    $pdo->beginTransaction();
    try {
        $st = $pdo->prepare("INSERT OR IGNORE INTO daily_checkins(user_id, checkin_date, credit_awarded) VALUES(?, date('now','localtime'), ?)");
        $st->execute([$uid, $award]);
        if ($st->rowCount() !== 1) {
            $pdo->rollBack();
            return ['ok' => false, 'award' => 0];
        }
        $pdo->prepare('UPDATE users SET credit = credit + ? WHERE id = ?')->execute([$award, $uid]);
        $pdo->commit();
        return ['ok' => true, 'award' => $award];
    } catch (Throwable $ex) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $ex;
    }
}

function user_in_groups(int $uid): array {
    $st = db()->prepare('SELECT group_id FROM user_groups WHERE user_id = ?');
    $st->execute([$uid]);
    return array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
}

function article_groups_of(int $articleId): array {
    $st = db()->prepare('SELECT group_id FROM article_groups WHERE article_id = ?');
    $st->execute([$articleId]);
    return array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
}

function group_list(PDO $pdo): array {
    return $pdo->query('SELECT * FROM groups ORDER BY name')->fetchAll(PDO::FETCH_ASSOC);
}

function article_visible(array $a): bool {
    if (($a['visibility'] ?? 'public') === 'public') {
        return true;
    }
    if (is_admin()) {
        return true;
    }
    $uid = current_user_id();
    if (!$uid) {
        return false;
    }
    $allowed = article_groups_of((int)$a['id']);
    if (!$allowed) {
        return false;
    }
    return count(array_intersect(user_in_groups($uid), $allowed)) > 0;
}

// ---------------- 按 IP 的登录限流 ----------------
function client_ip(): string {
    return (string)($_SERVER['REMOTE_ADDR'] ?? '');
}

function login_throttle_blocked(): bool {
    $map = json_decode((string)db_setting(db(), 'login_throttle'), true);
    if (!is_array($map)) {
        return false;
    }
    $e = $map[client_ip()] ?? null;
    return $e !== null && ($e['until'] ?? 0) > time();
}

function login_throttle_register_failure(): void {
    $pdo = db();
    $map = json_decode((string)db_setting($pdo, 'login_throttle'), true);
    if (!is_array($map)) {
        $map = [];
    }
    $ip = client_ip();
    $e = $map[$ip] ?? ['fails' => 0, 'until' => 0];
    $e['fails']++;
    if ($e['fails'] >= 8) {
        $e['until'] = time() + 900;
        $e['fails'] = 0;
    }
    $map[$ip] = $e;
    db_set($pdo, 'login_throttle', json_encode($map));
}

function login_throttle_clear(): void {
    $pdo = db();
    $map = json_decode((string)db_setting($pdo, 'login_throttle'), true);
    if (!is_array($map)) {
        return;
    }
    unset($map[client_ip()]);
    db_set($pdo, 'login_throttle', json_encode($map));
}

// ---------------- CSRF / 工具 ----------------
function csrf_token(): string {
    if (empty($_SESSION['csrf'])) {
        $_SESSION['csrf'] = bin2hex(random_bytes(16));
    }
    return $_SESSION['csrf'];
}

function csrf_field(): string {
    return '<input type="hidden" name="csrf" value="' . e(csrf_token()) . '">';
}

function csrf_check(): void {
    $token = $_POST['csrf'] ?? '';
    if (!is_string($token) || !hash_equals($_SESSION['csrf'] ?? '', $token)) {
        http_response_code(403);
        exit('CSRF 校验失败');
    }
}

function safe_filename(string $name): bool {
    return (bool)preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]*$/', $name);
}

function db_setting(PDO $pdo, string $key): ?string {
    $st = $pdo->prepare('SELECT value FROM settings WHERE key = ?');
    $st->execute([$key]);
    $v = $st->fetchColumn();
    return $v === false ? null : (string)$v;
}

function db_set(PDO $pdo, string $key, string $value): void {
    $st = $pdo->prepare('INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value = excluded.value');
    $st->execute([$key, $value]);
}

function article_images(PDO $pdo, int $articleId): array {
    $st = $pdo->prepare('SELECT * FROM images WHERE article_id = ? ORDER BY id');
    $st->execute([$articleId]);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}

function handle_upload(PDO $pdo, int $articleId): array {
    if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
        return ['ok' => false, 'error' => '上传失败，请重试'];
    }
    $f = $_FILES['image'];
    if ((int)$f['size'] > 10 * 1024 * 1024) {
        return ['ok' => false, 'error' => '图片不能超过 10MB'];
    }
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $f['tmp_name']);
    finfo_close($finfo);
    $allowed = [
        'image/jpeg' => 'jpg',
        'image/png'  => 'png',
        'image/gif'  => 'gif',
        'image/webp' => 'webp',
    ];
    $ext = $allowed[$mime] ?? null;
    if (!$ext) {
        return ['ok' => false, 'error' => '仅支持 JPG/PNG/GIF/WebP 图片'];
    }
    $filename = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
    $dest = __DIR__ . '/uploads/' . $filename;
    if (!move_uploaded_file($f['tmp_name'], $dest)) {
        return ['ok' => false, 'error' => '保存图片失败'];
    }
    $st = $pdo->prepare('INSERT INTO images(article_id, filename) VALUES(?,?)');
    $st->execute([$articleId, $filename]);
    return [
        'ok'       => true,
        'id'       => (int)$pdo->lastInsertId(),
        'filename' => $filename,
        'url'      => BASE_URL . '/uploads/' . $filename,
    ];
}

function render_content(PDO $pdo, string $content, int $articleId): string {
    require_once __DIR__ . '/assets/Parsedown.php';
    $pd = new Parsedown();
    $pd->setSafeMode(true);
    $html = $pd->text($content);

    $images = article_images($pdo, $articleId);
    $byId = [];
    foreach ($images as $img) {
        $byId[(int)$img['id']] = $img;
    }
    $used = [];
    $html = preg_replace_callback('/\{\{img:(\d+)\}\}/', function ($m) use ($byId, &$used) {
        $id = (int)$m[1];
        if (!isset($byId[$id]) || !safe_filename($byId[$id]['filename'])) {
            return '';
        }
        $used[$id] = true;
        $f = $byId[$id]['filename'];
        return '<figure class="article-img"><img src="' . BASE_URL . '/uploads/' . e($f) . '" alt="" loading="lazy"></figure>';
    }, $html);

    $gallery = '';
    foreach ($images as $img) {
        if (isset($used[(int)$img['id']]) || !safe_filename($img['filename'])) {
            continue;
        }
        $gallery .= '<img class="gallery-img" src="' . BASE_URL . '/uploads/' . e($img['filename']) . '" alt="" loading="lazy">';
    }
    if ($gallery !== '') {
        $html .= '<h3 class="gallery-title">图集</h3><div class="gallery">' . $gallery . '</div>';
    }
    return $html;
}

// ---------------- 页面骨架 ----------------
function page_header(string $title): void {
    echo '<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2447d8">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/adnovel-icon-32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/adnovel-icon-180.png">
<link rel="manifest" href="/site-manifest.json">
<title>' . e($title) . '</title>
<link rel="stylesheet" href="' . BASE_URL . '/assets/style.css">
</head>
<body>
<div class="site-frame">
<header class="site-header">
  <a class="logo" href="/"><span class="logo-mark">P/R</span><span>游戏与看文</span></a>
  <nav class="site-nav" aria-label="主导航">';
    $script = basename((string)($_SERVER['SCRIPT_NAME'] ?? ''));
    $readingClass = $script === 'checkin.php' ? '' : ' class="is-current"';
    $checkinClass = $script === 'checkin.php' ? ' class="is-current"' : '';
    echo '<a href="/">游戏</a>
    <a' . $readingClass . ' href="' . BASE_URL . '/">看文</a>
    <a' . $checkinClass . ' href="' . BASE_URL . '/checkin.php">签到</a>
    <a href="/open-source/">开源</a>';
    if (is_admin()) {
        echo '<a href="' . BASE_URL . '/admin/">后台</a>';
    }
    $u = current_user();
    if ($u) {
        echo '<span class="who"><b>LV.' . credit_level((int)$u['credit']) . '</b> · ' . (int)$u['credit'] . ' CREDIT' . ((int)$u['is_vip'] === 1 ? ' · VIP' : '') . '</span>
        <a href="' . BASE_URL . '/logout.php">退出</a>';
    } else {
        echo '<a href="' . BASE_URL . '/login.php">登录</a>
        <a href="' . BASE_URL . '/register.php">注册</a>';
    }
    echo '</nav>
</header>
<main class="wrap">';
}

function page_footer(): void {
    echo '</main>
<footer class="site-footer"><div class="footer-note"><span>PLAY / READ</span><span>游戏与文字，同一张入口。</span></div><a class="record-link" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">湘ICP备2026038574号</a></footer>
</div>
</body>
</html>';
}

function admin_header(string $title): void {
    echo '<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2447d8">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/adnovel-icon-32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/adnovel-icon-180.png">
<link rel="manifest" href="/site-manifest.json">
<title>' . e($title) . '</title>
<link rel="stylesheet" href="' . BASE_URL . '/assets/style.css">
</head>
<body>
<div class="site-frame">
<header class="site-header is-admin"><a class="logo" href="/"><span class="logo-mark">P/R</span><span>游戏与看文</span></a><nav class="site-nav" aria-label="主导航"><a href="/">游戏</a><a href="' . BASE_URL . '/">看文</a><a href="/open-source/">开源</a></nav></header>
<main class="wrap">';
}

function admin_footer(): void {
    echo '</main>
</div>
</body>
</html>';
}

// 保证结构存在（每次请求，个人站开销可忽略）
ensure_schema(db());
