#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations

import sqlite3

from db.utils import get_cst_now_str


def get_current_version(conn) -> int:
    cursor = conn.cursor()
    cursor.execute("SELECT version FROM db_version WHERE id = 1")
    row = cursor.fetchone()
    if not row:
        return 0
    try:
        return int(row["version"])
    except Exception:
        try:
            return int(row[0])
        except Exception:
            return 0


def set_current_version(conn, version: int) -> None:
    cursor = conn.cursor()
    cursor.execute("UPDATE db_version SET version = ?, updated_at = ? WHERE id = 1", (int(version), get_cst_now_str()))
    conn.commit()


def _table_exists(cursor, table_name: str) -> bool:
    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (str(table_name),))
    return cursor.fetchone() is not None


def _get_table_columns(cursor, table_name: str) -> set[str]:
    cursor.execute(f"PRAGMA table_info({table_name})")
    return {col[1] for col in cursor.fetchall()}


def _add_column_if_missing(cursor, table_name: str, columns: set[str], column_name: str, column_ddl: str, *, ok_message: str) -> bool:
    if column_name in columns:
        return False
    cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_ddl}")
    columns.add(column_name)
    print(ok_message)
    return True


def _read_row_value(row, key: str, index: int):
    if isinstance(row, sqlite3.Row):
        return row[key]
    return row[index]


def _get_migration_steps():
    return [
        (1, _migrate_to_v1),
        (2, _migrate_to_v2),
        (3, _migrate_to_v3),
        (4, _migrate_to_v4),
        (5, _migrate_to_v5),
        (6, _migrate_to_v6),
        (7, _migrate_to_v7),
        (8, _migrate_to_v8),
        (9, _migrate_to_v9),
        (10, _migrate_to_v10),
        (11, _migrate_to_v11),
        (12, _migrate_to_v12),
        (13, _migrate_to_v13),
        (14, _migrate_to_v14),
        (15, _migrate_to_v15),
        (16, _migrate_to_v16),
        (17, _migrate_to_v17),
        (18, _migrate_to_v18),
        (19, _migrate_to_v19),
        (20, _migrate_to_v20),
        (21, _migrate_to_v21),
        (22, _migrate_to_v22),
    ]


def migrate_database(conn, target_version: int) -> None:
    """数据库迁移：按版本增量升级（向前兼容）。"""
    cursor = conn.cursor()
    cursor.execute("INSERT OR IGNORE INTO db_version (id, version, updated_at) VALUES (1, 0, ?)", (get_cst_now_str(),))
    conn.commit()

    target_version = int(target_version)
    current_version = get_current_version(conn)

    for version, migrate_fn in _get_migration_steps():
        if version > target_version or current_version >= version:
            continue
        migrate_fn(conn)
        current_version = version

    stored_version = get_current_version(conn)
    if stored_version != current_version:
        set_current_version(conn, current_version)

    if current_version != target_version:
        print(f"  [WARN] 目标版本 {target_version} 未完全可达，当前停留在 {current_version}")


def _migrate_to_v1(conn):
    """迁移到版本1 - 添加缺失字段"""
    cursor = conn.cursor()

    system_columns = _get_table_columns(cursor, "system_config")
    _add_column_if_missing(
        cursor,
        "system_config",
        system_columns,
        "schedule_weekdays",
        'TEXT DEFAULT "1,2,3,4,5,6,7"',
        ok_message="  [OK] 添加 schedule_weekdays 字段",
    )
    _add_column_if_missing(
        cursor,
        "system_config",
        system_columns,
        "max_screenshot_concurrent",
        "INTEGER DEFAULT 3",
        ok_message="  [OK] 添加 max_screenshot_concurrent 字段",
    )
    _add_column_if_missing(
        cursor,
        "system_config",
        system_columns,
        "max_concurrent_per_account",
        "INTEGER DEFAULT 1",
        ok_message="  [OK] 添加 max_concurrent_per_account 字段",
    )
    _add_column_if_missing(
        cursor,
        "system_config",
        system_columns,
        "auto_approve_enabled",
        "INTEGER DEFAULT 0",
        ok_message="  [OK] 添加 auto_approve_enabled 字段",
    )
    _add_column_if_missing(
        cursor,
        "system_config",
        system_columns,
        "auto_approve_hourly_limit",
        "INTEGER DEFAULT 10",
        ok_message="  [OK] 添加 auto_approve_hourly_limit 字段",
    )
    _add_column_if_missing(
        cursor,
        "system_config",
        system_columns,
        "auto_approve_vip_days",
        "INTEGER DEFAULT 7",
        ok_message="  [OK] 添加 auto_approve_vip_days 字段",
    )

    task_log_columns = _get_table_columns(cursor, "task_logs")
    _add_column_if_missing(
        cursor,
        "task_logs",
        task_log_columns,
        "duration",
        "INTEGER",
        ok_message="  [OK] 添加 duration 字段到 task_logs",
    )

    conn.commit()


def _migrate_to_v2(conn):
    """迁移到版本2 - 添加代理配置字段"""
    cursor = conn.cursor()

    columns = _get_table_columns(cursor, "system_config")
    _add_column_if_missing(
        cursor,
        "system_config",
        columns,
        "proxy_enabled",
        "INTEGER DEFAULT 0",
        ok_message="  [OK] 添加 proxy_enabled 字段",
    )
    _add_column_if_missing(
        cursor,
        "system_config",
        columns,
        "proxy_api_url",
        'TEXT DEFAULT ""',
        ok_message="  [OK] 添加 proxy_api_url 字段",
    )
    _add_column_if_missing(
        cursor,
        "system_config",
        columns,
        "proxy_expire_minutes",
        "INTEGER DEFAULT 3",
        ok_message="  [OK] 添加 proxy_expire_minutes 字段",
    )
    _add_column_if_missing(
        cursor,
        "system_config",
        columns,
        "enable_screenshot",
        "INTEGER DEFAULT 1",
        ok_message="  [OK] 添加 enable_screenshot 字段",
    )

    conn.commit()


def _migrate_to_v3(conn):
    """迁移到版本3 - 添加账号状态和登录失败计数字段"""
    cursor = conn.cursor()

    columns = _get_table_columns(cursor, "accounts")
    _add_column_if_missing(
        cursor,
        "accounts",
        columns,
        "status",
        'TEXT DEFAULT "active"',
        ok_message="  [OK] 添加 accounts.status 字段 (账号状态)",
    )
    _add_column_if_missing(
        cursor,
        "accounts",
        columns,
        "login_fail_count",
        "INTEGER DEFAULT 0",
        ok_message="  [OK] 添加 accounts.login_fail_count 字段 (登录失败计数)",
    )
    _add_column_if_missing(
        cursor,
        "accounts",
        columns,
        "last_login_error",
        "TEXT",
        ok_message="  [OK] 添加 accounts.last_login_error 字段 (最后登录错误)",
    )

    conn.commit()


def _migrate_to_v4(conn):
    """迁移到版本4 - 添加任务来源字段"""
    cursor = conn.cursor()

    columns = _get_table_columns(cursor, "task_logs")
    _add_column_if_missing(
        cursor,
        "task_logs",
        columns,
        "source",
        'TEXT DEFAULT "manual"',
        ok_message="  [OK] 添加 task_logs.source 字段 (任务来源: manual/scheduled/immediate)",
    )

    conn.commit()


def _migrate_to_v5(conn):
    """迁移到版本5 - 添加用户定时任务表"""
    cursor = conn.cursor()

    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user_schedules'")
    if not cursor.fetchone():
        cursor.execute(
            """
            CREATE TABLE IF NOT EXISTS user_schedules (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                name TEXT DEFAULT '我的定时任务',
                enabled INTEGER DEFAULT 0,
                schedule_time TEXT NOT NULL DEFAULT '08:00',
                weekdays TEXT NOT NULL DEFAULT '1,2,3,4,5',
                browse_type TEXT NOT NULL DEFAULT '应读',
                enable_screenshot INTEGER DEFAULT 1,
                account_ids TEXT,
                last_run_at TIMESTAMP,
                next_run_at TIMESTAMP,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
            )
            """
        )
        print("  [OK] 创建 user_schedules 表 (用户定时任务)")

        cursor.execute(
            """
            CREATE TABLE IF NOT EXISTS schedule_execution_logs (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                schedule_id INTEGER NOT NULL,
                user_id INTEGER NOT NULL,
                schedule_name TEXT,
                execute_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                total_accounts INTEGER DEFAULT 0,
                success_accounts INTEGER DEFAULT 0,
                failed_accounts INTEGER DEFAULT 0,
                total_items INTEGER DEFAULT 0,
                total_attachments INTEGER DEFAULT 0,
                total_screenshots INTEGER DEFAULT 0,
                duration_seconds INTEGER DEFAULT 0,
                status TEXT DEFAULT 'running',
                error_message TEXT,
                FOREIGN KEY (schedule_id) REFERENCES user_schedules (id) ON DELETE CASCADE,
                FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
            )
            """
        )
        print("  [OK] 创建 schedule_execution_logs 表 (定时任务执行日志)")

        cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_user_id ON user_schedules(user_id)")
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_enabled ON user_schedules(enabled)")
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_next_run ON user_schedules(next_run_at)")
        print("  [OK] 创建 user_schedules 表索引")

    conn.commit()


def _migrate_to_v6(conn):
    """迁移到版本6 - 添加公告功能相关表"""
    cursor = conn.cursor()

    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcements'")
    if not cursor.fetchone():
        cursor.execute(
            """
            CREATE TABLE IF NOT EXISTS announcements (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                content TEXT NOT NULL,
                is_active INTEGER DEFAULT 1,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
            """
        )
        print("  [OK] 创建 announcements 表 (公告)")
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcements_active ON announcements(is_active)")
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at)")
        print("  [OK] 创建 announcements 表索引")

    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcement_dismissals'")
    if not cursor.fetchone():
        cursor.execute(
            """
            CREATE TABLE IF NOT EXISTS announcement_dismissals (
                user_id INTEGER NOT NULL,
                announcement_id INTEGER NOT NULL,
                dismissed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY (user_id, announcement_id),
                FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
                FOREIGN KEY (announcement_id) REFERENCES announcements (id) ON DELETE CASCADE
            )
            """
        )
        print("  [OK] 创建 announcement_dismissals 表 (公告永久关闭记录)")
        cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcement_dismissals_user ON announcement_dismissals(user_id)")
        print("  [OK] 创建 announcement_dismissals 表索引")

    conn.commit()


def _migrate_to_v7(conn):
    """迁移到版本7 - 统一存储北京时间（将历史UTC时间字段整体+8小时）"""
    cursor = conn.cursor()
    columns_cache: dict[str, set[str]] = {}

    def shift_utc_to_cst(table_name: str, column_name: str) -> None:
        if not _table_exists(cursor, table_name):
            return

        if table_name not in columns_cache:
            columns_cache[table_name] = _get_table_columns(cursor, table_name)
        if column_name not in columns_cache[table_name]:
            return

        cursor.execute(
            f"""
            UPDATE {table_name}
            SET {column_name} = datetime({column_name}, '+8 hours')
            WHERE {column_name} IS NOT NULL AND {column_name} != ''
            """
        )

    for table, col in [
        ("users", "created_at"),
        ("users", "approved_at"),
        ("admins", "created_at"),
        ("accounts", "created_at"),
        ("password_reset_requests", "created_at"),
        ("password_reset_requests", "processed_at"),
        ("smtp_configs", "created_at"),
        ("smtp_configs", "updated_at"),
        ("smtp_configs", "last_success_at"),
        ("email_settings", "updated_at"),
        ("email_tokens", "created_at"),
        ("email_logs", "created_at"),
        ("email_stats", "last_updated"),
        ("task_checkpoints", "created_at"),
        ("task_checkpoints", "updated_at"),
        ("task_checkpoints", "completed_at"),
    ]:
        shift_utc_to_cst(table, col)

    conn.commit()
    print("  [OK] 时区迁移：历史UTC时间已转换为北京时间")


def _migrate_to_v8(conn):
    """迁移到版本8 - 用户定时 next_run_at 随机延迟落库（O-08）"""
    cursor = conn.cursor()

    # 1) 增量字段：random_delay（旧库可能不存在）
    columns = _get_table_columns(cursor, "user_schedules")
    _add_column_if_missing(
        cursor,
        "user_schedules",
        columns,
        "random_delay",
        "INTEGER DEFAULT 0",
        ok_message="  [OK] 添加 user_schedules.random_delay 字段",
    )
    _add_column_if_missing(
        cursor,
        "user_schedules",
        columns,
        "next_run_at",
        "TIMESTAMP",
        ok_message="  [OK] 添加 user_schedules.next_run_at 字段",
    )

    cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_next_run ON user_schedules(next_run_at)")
    conn.commit()

    # 2) 为历史 enabled schedule 补算 next_run_at（保证索引驱动可用）
    try:
        from services.schedule_utils import compute_next_run_at, format_cst
        from services.time_utils import get_beijing_now

        now_dt = get_beijing_now()
        now_str = format_cst(now_dt)

        cursor.execute(
            """
            SELECT id, schedule_time, weekdays, random_delay, last_run_at, next_run_at
            FROM user_schedules
            WHERE enabled = 1
            """
        )
        rows = cursor.fetchall() or []

        fixed = 0
        for row in rows:
            try:
                schedule_id = _read_row_value(row, "id", 0)
                schedule_time = _read_row_value(row, "schedule_time", 1)
                weekdays = _read_row_value(row, "weekdays", 2)
                random_delay = _read_row_value(row, "random_delay", 3)
                last_run_at = _read_row_value(row, "last_run_at", 4)
                next_run_at = _read_row_value(row, "next_run_at", 5)
            except Exception:
                continue

            next_run_text = str(next_run_at or "").strip()
            # 若 next_run_at 为空/非法/已过期，则重算
            if (not next_run_text) or (next_run_text <= now_str):
                next_dt = compute_next_run_at(
                    now=now_dt,
                    schedule_time=str(schedule_time or "08:00"),
                    weekdays=str(weekdays or "1,2,3,4,5"),
                    random_delay=int(random_delay or 0),
                    last_run_at=str(last_run_at or "") if last_run_at else None,
                )
                next_run_text = format_cst(next_dt)
                cursor.execute(
                    "UPDATE user_schedules SET next_run_at = ?, updated_at = ? WHERE id = ?",
                    (next_run_text, get_cst_now_str(), int(schedule_id)),
                )
                fixed += 1

        conn.commit()
        if fixed:
            print(f"  [OK] 已为 {fixed} 条启用定时任务补算 next_run_at")
    except Exception as e:
        # 迁移过程中不阻断主流程；上线后由 worker 兜底补算
        print(f"  ⚠ v8 迁移补算 next_run_at 失败: {e}")


def _migrate_to_v9(conn):
    """迁移到版本9 - 邮件设置字段迁移（清理 email_service scattered ALTER TABLE）"""
    cursor = conn.cursor()

    if not _table_exists(cursor, "email_settings"):
        # 邮件表由 email_service.init_email_tables 创建；此处仅做增量字段迁移
        return

    columns = _get_table_columns(cursor, "email_settings")

    changed = False
    changed = (
        _add_column_if_missing(
            cursor,
            "email_settings",
            columns,
            "register_verify_enabled",
            "INTEGER DEFAULT 0",
            ok_message="  [OK] 添加 email_settings.register_verify_enabled 字段",
        )
        or changed
    )
    changed = (
        _add_column_if_missing(
            cursor,
            "email_settings",
            columns,
            "base_url",
            "TEXT DEFAULT ''",
            ok_message="  [OK] 添加 email_settings.base_url 字段",
        )
        or changed
    )
    changed = (
        _add_column_if_missing(
            cursor,
            "email_settings",
            columns,
            "task_notify_enabled",
            "INTEGER DEFAULT 0",
            ok_message="  [OK] 添加 email_settings.task_notify_enabled 字段",
        )
        or changed
    )

    if changed:
        conn.commit()


def _migrate_to_v10(conn):
    """迁移到版本10 - users 邮箱字段迁移（避免运行时 ALTER TABLE）"""
    cursor = conn.cursor()
    columns = _get_table_columns(cursor, "users")

    changed = False
    changed = (
        _add_column_if_missing(
            cursor,
            "users",
            columns,
            "email_verified",
            "INTEGER DEFAULT 0",
            ok_message="  [OK] 添加 users.email_verified 字段",
        )
        or changed
    )
    changed = (
        _add_column_if_missing(
            cursor,
            "users",
            columns,
            "email_notify_enabled",
            "INTEGER DEFAULT 1",
            ok_message="  [OK] 添加 users.email_notify_enabled 字段",
        )
        or changed
    )

    if changed:
        conn.commit()


def _migrate_to_v11(conn):
    """迁移到版本11 - 取消注册待审核：历史 pending 用户直接置为 approved"""
    cursor = conn.cursor()
    now_str = get_cst_now_str()

    try:
        cursor.execute(
            """
            UPDATE users
            SET status = 'approved',
                approved_at = COALESCE(NULLIF(approved_at, ''), ?)
            WHERE status = 'pending'
            """,
            (now_str,),
        )
        updated = cursor.rowcount
        conn.commit()

        if updated:
            print(f"  [OK] 已将 {updated} 个 pending 用户迁移为 approved")
    except sqlite3.OperationalError as e:
        print(f"  ⚠️ v11 迁移跳过: {e}")


def _migrate_to_v12(conn):
    """迁移到版本12 - 登录设备/IP记录表"""
    cursor = conn.cursor()

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS login_fingerprints (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL,
            user_agent TEXT NOT NULL,
            first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            last_ip TEXT DEFAULT '',
            UNIQUE (user_id, user_agent),
            FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
        )
        """
    )

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS login_ips (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL,
            ip TEXT NOT NULL,
            first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            UNIQUE (user_id, ip),
            FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
        )
        """
    )

    cursor.execute("CREATE INDEX IF NOT EXISTS idx_login_fingerprints_user ON login_fingerprints(user_id)")
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_login_ips_user ON login_ips(user_id)")

    conn.commit()


def _migrate_to_v13(conn):
    """迁移到版本13 - 安全防护：威胁检测相关表"""
    cursor = conn.cursor()

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS threat_events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            threat_type TEXT NOT NULL,
            score INTEGER NOT NULL DEFAULT 0,
            rule TEXT,
            field_name TEXT,
            matched TEXT,
            value_preview TEXT,
            ip TEXT,
            user_id INTEGER,
            request_method TEXT,
            request_path TEXT,
            user_agent TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_created_at ON threat_events(created_at)")
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_ip ON threat_events(ip)")
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_user_id ON threat_events(user_id)")
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_type ON threat_events(threat_type)")

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS ip_risk_scores (
            ip TEXT PRIMARY KEY,
            risk_score INTEGER NOT NULL DEFAULT 0,
            last_seen TIMESTAMP,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_risk_scores_score ON ip_risk_scores(risk_score)")
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_risk_scores_updated_at ON ip_risk_scores(updated_at)")

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS user_risk_scores (
            user_id INTEGER PRIMARY KEY,
            risk_score INTEGER NOT NULL DEFAULT 0,
            last_seen TIMESTAMP,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_risk_scores_score ON user_risk_scores(risk_score)")
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_risk_scores_updated_at ON user_risk_scores(updated_at)")

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS ip_blacklist (
            ip TEXT PRIMARY KEY,
            reason TEXT,
            is_active INTEGER DEFAULT 1,
            added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            expires_at TIMESTAMP
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_blacklist_active ON ip_blacklist(is_active)")
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_blacklist_expires ON ip_blacklist(expires_at)")

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS threat_signatures (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT UNIQUE NOT NULL,
            threat_type TEXT NOT NULL,
            pattern TEXT NOT NULL,
            pattern_type TEXT DEFAULT 'regex',
            score INTEGER DEFAULT 0,
            is_active INTEGER DEFAULT 1,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_signatures_type ON threat_signatures(threat_type)")
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_signatures_active ON threat_signatures(is_active)")

    conn.commit()


def _migrate_to_v14(conn):
    """迁移到版本14 - 安全防护：用户黑名单表"""
    cursor = conn.cursor()

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS user_blacklist (
            user_id INTEGER PRIMARY KEY,
            reason TEXT,
            is_active INTEGER DEFAULT 1,
            added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            expires_at TIMESTAMP
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_blacklist_active ON user_blacklist(is_active)")
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_blacklist_expires ON user_blacklist(expires_at)")

    conn.commit()


def _migrate_to_v15(conn):
    """迁移到版本15 - 邮件设置：新设备登录提醒全局开关"""
    cursor = conn.cursor()

    if not _table_exists(cursor, "email_settings"):
        # 邮件表由 email_service.init_email_tables 创建；此处仅做增量字段迁移
        return

    columns = _get_table_columns(cursor, "email_settings")

    changed = False
    changed = (
        _add_column_if_missing(
            cursor,
            "email_settings",
            columns,
            "login_alert_enabled",
            "INTEGER DEFAULT 1",
            ok_message="  [OK] 添加 email_settings.login_alert_enabled 字段",
        )
        or changed
    )

    try:
        cursor.execute("UPDATE email_settings SET login_alert_enabled = 1 WHERE login_alert_enabled IS NULL")
        if cursor.rowcount:
            changed = True
    except sqlite3.OperationalError:
        # 列不存在等情况由上方迁移兜底；不阻断主流程
        pass

    if changed:
        conn.commit()


def _migrate_to_v16(conn):
    """迁移到版本16 - 公告支持图片字段"""
    cursor = conn.cursor()
    columns = _get_table_columns(cursor, "announcements")

    if _add_column_if_missing(
        cursor,
        "announcements",
        columns,
        "image_url",
        "TEXT",
        ok_message="  [OK] 添加 announcements.image_url 字段",
    ):
        conn.commit()


def _migrate_to_v17(conn):
    """迁移到版本17 - 金山文档上传配置与用户开关"""
    cursor = conn.cursor()

    system_columns = _get_table_columns(cursor, "system_config")
    system_fields = [
        ("kdocs_enabled", "INTEGER DEFAULT 0"),
        ("kdocs_doc_url", "TEXT DEFAULT ''"),
        ("kdocs_default_unit", "TEXT DEFAULT ''"),
        ("kdocs_sheet_name", "TEXT DEFAULT ''"),
        ("kdocs_sheet_index", "INTEGER DEFAULT 0"),
        ("kdocs_unit_column", "TEXT DEFAULT 'A'"),
        ("kdocs_image_column", "TEXT DEFAULT 'D'"),
        ("kdocs_admin_notify_enabled", "INTEGER DEFAULT 0"),
        ("kdocs_admin_notify_email", "TEXT DEFAULT ''"),
    ]
    for field, ddl in system_fields:
        _add_column_if_missing(
            cursor,
            "system_config",
            system_columns,
            field,
            ddl,
            ok_message=f"  [OK] 添加 system_config.{field} 字段",
        )

    user_columns = _get_table_columns(cursor, "users")
    user_fields = [
        ("kdocs_unit", "TEXT DEFAULT ''"),
        ("kdocs_auto_upload", "INTEGER DEFAULT 0"),
    ]
    for field, ddl in user_fields:
        _add_column_if_missing(
            cursor,
            "users",
            user_columns,
            field,
            ddl,
            ok_message=f"  [OK] 添加 users.{field} 字段",
        )

    conn.commit()


def _migrate_to_v18(conn):
    """迁移到版本18 - 金山文档上传：有效行范围配置"""
    cursor = conn.cursor()

    columns = _get_table_columns(cursor, "system_config")
    _add_column_if_missing(
        cursor,
        "system_config",
        columns,
        "kdocs_row_start",
        "INTEGER DEFAULT 0",
        ok_message="  [OK] 添加 system_config.kdocs_row_start 字段",
    )
    _add_column_if_missing(
        cursor,
        "system_config",
        columns,
        "kdocs_row_end",
        "INTEGER DEFAULT 0",
        ok_message="  [OK] 添加 system_config.kdocs_row_end 字段",
    )

    conn.commit()



def _migrate_to_v19(conn):
    """迁移到版本19 - 报表与调度查询复合索引优化"""
    cursor = conn.cursor()

    index_statements = [
        "CREATE INDEX IF NOT EXISTS idx_users_status_created_at ON users(status, created_at)",
        "CREATE INDEX IF NOT EXISTS idx_task_logs_status_created_at ON task_logs(status, created_at)",
        "CREATE INDEX IF NOT EXISTS idx_user_schedules_enabled_next_run ON user_schedules(enabled, next_run_at)",
        "CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_status_created_at ON bug_feedbacks(status, created_at)",
        "CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_user_created_at ON bug_feedbacks(user_id, created_at)",
    ]

    for statement in index_statements:
        cursor.execute(statement)

    conn.commit()



def _migrate_to_v20(conn):
    """迁移到版本20 - 慢SQL阈值系统配置"""
    cursor = conn.cursor()

    columns = _get_table_columns(cursor, "system_config")
    _add_column_if_missing(
        cursor,
        "system_config",
        columns,
        "db_slow_query_ms",
        "INTEGER DEFAULT 120",
        ok_message="  [OK] 添加 system_config.db_slow_query_ms 字段",
    )

    conn.commit()


def _migrate_to_v21(conn):
    """迁移到版本21 - Passkey 认证设备表"""
    cursor = conn.cursor()

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS passkeys (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            owner_type TEXT NOT NULL,
            owner_id INTEGER NOT NULL,
            device_name TEXT NOT NULL,
            credential_id TEXT UNIQUE NOT NULL,
            public_key TEXT NOT NULL,
            sign_count INTEGER DEFAULT 0,
            transports TEXT DEFAULT '',
            aaguid TEXT DEFAULT '',
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_passkeys_owner ON passkeys(owner_type, owner_id)")
    cursor.execute(
        "CREATE INDEX IF NOT EXISTS idx_passkeys_owner_last_used ON passkeys(owner_type, owner_id, last_used_at)"
    )

    conn.commit()


def _migrate_to_v22(conn):
    """迁移到版本22 - Space 聚合登录配置、绑定与短期待绑定凭证。"""
    cursor = conn.cursor()

    system_columns = _get_table_columns(cursor, "system_config")
    system_fields = [
        ("social_login_enabled", "INTEGER DEFAULT 0"),
        ("social_login_endpoint", "TEXT DEFAULT 'https://www.spacezs.cn/connect.php'"),
        ("social_login_appid", "TEXT DEFAULT ''"),
        ("social_login_appkey", "TEXT DEFAULT ''"),
        ("social_login_providers", "TEXT DEFAULT 'qq,wx,alipay'"),
    ]
    for field, ddl in system_fields:
        _add_column_if_missing(
            cursor,
            "system_config",
            system_columns,
            field,
            ddl,
            ok_message=f"  [OK] 添加 system_config.{field} 字段",
        )

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS social_login_bindings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL,
            provider TEXT NOT NULL,
            social_uid TEXT NOT NULL,
            nickname TEXT DEFAULT '',
            avatar_url TEXT DEFAULT '',
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            last_login_at TIMESTAMP,
            UNIQUE (provider, social_uid),
            UNIQUE (user_id, provider),
            FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_login_bindings_user ON social_login_bindings(user_id)")
    cursor.execute(
        "CREATE INDEX IF NOT EXISTS idx_social_login_bindings_provider_uid ON social_login_bindings(provider, social_uid)"
    )

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS social_pending_binds (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            token TEXT NOT NULL UNIQUE,
            provider TEXT NOT NULL,
            social_uid TEXT NOT NULL,
            nickname TEXT DEFAULT '',
            avatar_url TEXT DEFAULT '',
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            expires_at TIMESTAMP NOT NULL
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_token ON social_pending_binds(token)")
    cursor.execute(
        "CREATE INDEX IF NOT EXISTS idx_social_pending_binds_provider_uid ON social_pending_binds(provider, social_uid)"
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_social_pending_binds_expires ON social_pending_binds(expires_at)")

    conn.commit()
