"""SQL 题判题核心：在内存 SQLite 中分别执行标准答案和学生 SQL 并比对结果。

查询题（mode="query"）比对最后一条 SELECT 的结果集；
增删改题（mode="modify"）比对执行后所有用户表的最终状态。
学生 SQL 通过 authorizer（禁 ATTACH/PRAGMA，查询题只读）、
progress_handler（墙钟超时）和 max_page_count + SQLITE_LIMIT_LENGTH（内存上限）三重防护；
受信脚本（初始化/标准答案）不受超时和只读限制，但同样禁止 ATTACH/DETACH。
"""

import sqlite3
import time

from submission.models import JudgeStatus

# 单结果集/单表最大行数，防 CROSS JOIN 撑爆 worker 内存
ROW_LIMIT = 10_000
# progress_handler 检查粒度（SQLite VM 指令数）
PROGRESS_STEP = 1_000
ERROR_MESSAGE_MAX_LEN = 200

# 题目页展示的行数上限（示例数据/期望结果）
DISPLAY_ROW_LIMIT = 20

# prepare 阶段的语法类错误，映射为 COMPILE_ERROR
_SYNTAX_ERROR_MARKERS = ("syntax error", "unrecognized token", "incomplete input")

# 两种模式都禁止的授权码：挂载外部库 / 数据库参数
_DENIED_ALWAYS = {sqlite3.SQLITE_ATTACH, sqlite3.SQLITE_DETACH, sqlite3.SQLITE_PRAGMA}

# 受信脚本（初始化/标准答案）也必须禁止挂载外部库：ATTACH 能在服务器上读写任意 SQLite 文件，
# 而这些脚本在保存/预览题目时跑在 Django 请求进程里，等于把出题权限提成任意文件读写。
_TRUSTED_DENIED = {sqlite3.SQLITE_ATTACH, sqlite3.SQLITE_DETACH}
_ATTACH_DENIED_HINT = "脚本中禁止使用 ATTACH/DETACH 挂载外部数据库"


def _trusted_authorizer(action, arg1, arg2, db_name, trigger):
    return sqlite3.SQLITE_DENY if action in _TRUSTED_DENIED else sqlite3.SQLITE_OK


# 查询题允许的授权码（白名单外一律拒绝，防先 INSERT 伪造数据再 SELECT）
_QUERY_MODE_ALLOWED = {getattr(sqlite3, name) for name in ("SQLITE_SELECT", "SQLITE_READ", "SQLITE_FUNCTION", "SQLITE_RECURSIVE", "SQLITE_TRANSACTION") if hasattr(sqlite3, name)}


class SQLCaseError(Exception):
    """携带 JudgeStatus 的判题异常。SYSTEM_ERROR 级别（出题配置问题）会传播到 dispatcher。"""

    def __init__(self, result, message):
        super().__init__(message)
        self.result = result
        self.message = message


def _truncate(message):
    message = str(message)
    if len(message) > ERROR_MESSAGE_MAX_LEN:
        return message[:ERROR_MESSAGE_MAX_LEN] + "..."
    return message


def split_statements(script):
    """用 sqlite3.complete_statement 累积切分多条语句，正确处理字符串/注释里的分号；末尾缺分号自动补。"""
    statements = []
    buf = ""
    for part in script.split(";"):
        buf += part + ";"
        if sqlite3.complete_statement(buf):
            stmt = buf.strip()
            buf = ""
            if stmt and stmt != ";":
                statements.append(stmt)
    # 残句（未闭合的引号/注释，或末尾缺分号但上面已补），交给 execute 报错或执行
    remainder = buf.strip()
    if remainder and remainder != ";":
        statements.append(remainder)
    return statements


def _canonical_value(v):
    """值归一化并打类型标签，防止 NULL/"NULL"、1/"1" 碰撞；数值统一比对（1 == 1.0，浮点 6 位有效数字）。"""
    if v is None:
        return ("null",)
    if isinstance(v, int):
        return ("num", str(v))
    if isinstance(v, float):
        if v.is_integer() and abs(v) < 2**53:
            return ("num", str(int(v)))
        return ("num", format(v, ".6g"))
    if isinstance(v, bytes):
        return ("blob", v.hex())
    return ("str", str(v))


def _canonical_row(row):
    return tuple(_canonical_value(v) for v in row)


def _new_db(memory_limit_mb):
    limit_mb = max(int(memory_limit_mb), 1)
    conn = sqlite3.connect(":memory:", isolation_level=None)  # autocommit，脚本行为可预期
    conn.execute("PRAGMA page_size=4096")
    # 4096B/页 × 256 页/MB，超限报 "database or disk is full"
    conn.execute(f"PRAGMA max_page_count={limit_mb * 256}")
    # max_page_count 只约束数据库页，管不住查询期的单值分配：默认 1GB 上限下
    # 一条 SELECT hex(zeroblob(5e8)) 就能在 worker 进程里吃掉 GB 级内存且秒级返回，
    # progress_handler 的指令粒度也拦不到（分配发生在单条 opcode 内）。
    conn.setlimit(sqlite3.SQLITE_LIMIT_LENGTH, limit_mb * 1024 * 1024)
    # 整条连接生命周期内禁止 ATTACH/DETACH（学生 SQL 由 _run_student 换上更严的 authorizer）
    conn.set_authorizer(_trusted_authorizer)
    return conn


def _execute_statements(conn, script, deadline=None):
    """逐条执行，返回最后一条产生结果集的语句的 (列数, 行列表)；无结果集返回 None。"""
    last_result = None
    for stmt in split_statements(script):
        if deadline is not None and time.monotonic() > deadline:
            raise sqlite3.OperationalError("interrupted")
        cursor = conn.execute(stmt)
        if cursor.description is not None:
            rows = cursor.fetchmany(ROW_LIMIT + 1)
            if len(rows) > ROW_LIMIT:
                raise SQLCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, f"查询结果超过 {ROW_LIMIT} 行")
            last_result = (len(cursor.description), [_canonical_row(r) for r in rows])
        cursor.close()
    return last_result


def _dump_tables(conn):
    """dump 所有用户表：{表名: (列数, 行多重集)}，行内排序，表状态天然无序。"""
    cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
    tables = [r[0] for r in cursor.fetchall()]
    state = {}
    for table in tables:
        quoted = table.replace('"', '""')
        cur = conn.execute(f'SELECT * FROM "{quoted}"')
        rows = cur.fetchmany(ROW_LIMIT + 1)
        if len(rows) > ROW_LIMIT:
            raise SQLCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, f"表 {table} 超过 {ROW_LIMIT} 行")
        state[table] = (len(cur.description), sorted(_canonical_row(r) for r in rows))
    return state


def _trusted_error_text(e):
    """受信脚本的 sqlite 错误转出题人能看懂的提示。"""
    msg = str(e)
    if "not authorized" in msg or "prohibited" in msg:
        return _ATTACH_DENIED_HINT
    if "interrupted" in msg:
        return "超时"
    return _truncate(msg)


def _execute_trusted(conn, script, deadline, error_prefix):
    """执行受信脚本，任何失败都是出题问题 → SYSTEM_ERROR。"""
    conn.set_progress_handler(lambda: 1 if time.monotonic() > deadline else 0, PROGRESS_STEP)
    try:
        return _execute_statements(conn, script, deadline)
    except SQLCaseError as e:
        raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"{error_prefix}: {e.message}")
    except sqlite3.Error as e:
        raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"{error_prefix}: {_trusted_error_text(e)}")
    finally:
        conn.set_progress_handler(None, PROGRESS_STEP)


def _run_student(conn, script, mode, deadline):
    """带三重防护执行学生 SQL，异常映射为学生级 JudgeStatus。"""
    denied_hints = []

    def authorizer(action, arg1, arg2, db_name, trigger):
        if action in _DENIED_ALWAYS:
            denied_hints.append("禁止使用 ATTACH/DETACH/PRAGMA 等数据库管理语句")
            return sqlite3.SQLITE_DENY
        if mode == "query" and action not in _QUERY_MODE_ALLOWED:
            denied_hints.append("本题为查询题，禁止修改数据或表结构（INSERT/UPDATE/DELETE/CREATE 等）")
            return sqlite3.SQLITE_DENY
        return sqlite3.SQLITE_OK

    conn.set_authorizer(authorizer)
    conn.set_progress_handler(lambda: 1 if time.monotonic() > deadline else 0, PROGRESS_STEP)
    try:
        last_result = _execute_statements(conn, script, deadline)
    except sqlite3.Error as e:
        # 注意：authorizer 拒绝抛的是 DatabaseError 基类而非 OperationalError，统一按消息映射
        msg = str(e)
        if "interrupted" in msg:
            raise SQLCaseError(JudgeStatus.CPU_TIME_LIMIT_EXCEEDED, "SQL 执行超时")
        if "database or disk is full" in msg:
            raise SQLCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, "数据量超出内存限制")
        # SQLITE_LIMIT_LENGTH 触顶，如 zeroblob/group_concat 构造出的超大单值
        if "too big" in msg:
            raise SQLCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, "单个数据值超出内存限制")
        if "not authorized" in msg or "prohibited" in msg:
            hint = denied_hints[-1] if denied_hints else "本题禁止使用该语句"
            raise SQLCaseError(JudgeStatus.RUNTIME_ERROR, hint)
        if any(marker in msg for marker in _SYNTAX_ERROR_MARKERS):
            raise SQLCaseError(JudgeStatus.COMPILE_ERROR, _truncate(msg))
        raise SQLCaseError(JudgeStatus.RUNTIME_ERROR, _truncate(msg))
    finally:
        conn.set_progress_handler(None, PROGRESS_STEP)
        # 还原连接级的 ATTACH/DETACH 防护，而不是彻底放开
        conn.set_authorizer(_trusted_authorizer)

    if mode == "query":
        return last_result
    # dump 是我们自己的读取，不应吃学生的超时/授权限制（上面已清除）
    return _dump_tables(conn)


def _compare(expected, actual, mode, order_sensitive):
    if mode == "query":
        exp_cols, exp_rows = expected
        act_cols, act_rows = actual
        if exp_cols != act_cols:
            return False
        if order_sensitive:
            return exp_rows == act_rows
        return sorted(exp_rows) == sorted(act_rows)
    # modify: dump dict 里行已排序
    return expected == actual


def run_case(init_sql, ref_sql, student_sql, *, mode, order_sensitive, time_limit_ms, memory_limit_mb):
    """判一个测试点，返回与外部 judger 单测试点同构的 dict。

    学生错误（CE/WA/TLE/MLE/RE）体现在返回值里；
    出题配置错误（初始化/标准答案失败）抛 SQLCaseError(SYSTEM_ERROR)，由 dispatcher 处理。
    """
    time_limit_s = time_limit_ms / 1000
    # 受信脚本（初始化/标准答案）的运行上限放宽，避免出题数据较大时误报；仍防 worker 永久阻塞
    trusted_limit_s = max(time_limit_s * 5, 10)

    ref_conn = _new_db(memory_limit_mb)
    try:
        _execute_trusted(ref_conn, init_sql, time.monotonic() + trusted_limit_s, "初始化脚本执行失败")
        last_result = _execute_trusted(ref_conn, ref_sql, time.monotonic() + trusted_limit_s, "标准答案执行失败")
        if mode == "query":
            expected = last_result
        else:
            try:
                expected = _dump_tables(ref_conn)
            except SQLCaseError as e:
                raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"标准答案结果超出限制: {e.message}")
    finally:
        ref_conn.close()
    if mode == "query" and expected is None:
        raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, "标准答案未产生查询结果集")

    case = {
        "test_case": "",
        "result": JudgeStatus.ACCEPTED,
        "cpu_time": 0,
        "real_time": 0,
        "memory": 0,
        "signal": 0,
        "exit_code": 0,
        "error": 0,
        "output_md5": "",
        "error_message": None,
    }

    stu_conn = _new_db(memory_limit_mb)
    try:
        _execute_trusted(stu_conn, init_sql, time.monotonic() + trusted_limit_s, "初始化脚本执行失败")
        start = time.monotonic()
        try:
            actual = _run_student(stu_conn, student_sql, mode, start + time_limit_s)
        except SQLCaseError as e:
            elapsed = int((time.monotonic() - start) * 1000)
            case.update(result=e.result, error_message=e.message, cpu_time=elapsed, real_time=elapsed)
            return case
        elapsed = int((time.monotonic() - start) * 1000)
    finally:
        stu_conn.close()

    case["cpu_time"] = case["real_time"] = elapsed
    if mode == "query" and actual is None:
        case.update(result=JudgeStatus.WRONG_ANSWER, error_message="提交的 SQL 未产生查询结果集")
    elif not _compare(expected, actual, mode, order_sensitive):
        case["result"] = JudgeStatus.WRONG_ANSWER
    return case


def _display_value(v):
    """转为 JSON 可序列化的展示值；bytes 转 hex，None/数值/字符串原样。"""
    if isinstance(v, bytes):
        return v.hex()
    return v


def _query_result_columns(names, tables):
    """给查询结果的列名标上类型：按列名回查数据表的声明类型，与数据表展示同源（如 VARCHAR(20)）。

    表达式/聚合列（COUNT(*)、别名等）在数据表里无同名列，类型留空（前端隐藏）。
    """
    col_types = {c["name"]: c["type"] for t in tables for c in t["columns"]}
    return [{"name": name, "type": col_types.get(name, "")} for name in names]


def _dump_display_tables(conn, only=None):
    """按建表顺序 dump 用户表的原始行用于展示（区别于 _dump_tables 的归一化判题态）。

    only 为表名集合时只导出这些表。
    """
    cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
    tables = [r[0] for r in cursor.fetchall()]
    result = []
    for table in tables:
        if only is not None and table not in only:
            continue
        quoted = table.replace('"', '""')
        columns = [{"name": r[1], "type": r[2] or ""} for r in conn.execute(f'PRAGMA table_info("{quoted}")').fetchall()]
        total = conn.execute(f'SELECT COUNT(*) FROM "{quoted}"').fetchone()[0]
        rows = conn.execute(f'SELECT * FROM "{quoted}" LIMIT {DISPLAY_ROW_LIMIT}').fetchall()
        result.append(
            {
                "name": table,
                "columns": columns,
                "rows": [[_display_value(v) for v in row] for row in rows],
                "total_rows": total,
                "truncated": total > DISPLAY_ROW_LIMIT,
            }
        )
    return result


def build_display(init_sql, ref_sql, mode, *, memory_limit_mb=64):
    """生成题目页展示数据：初始数据表 + 期望结果。

    供管理端保存题目时调用；任何失败抛 SQLCaseError（出题配置问题，调用方转为报错信息）。
    """
    trusted_limit_s = 10
    conn = _new_db(memory_limit_mb)
    try:
        _execute_trusted(conn, init_sql, time.monotonic() + trusted_limit_s, "初始化脚本执行失败")
        tables = _dump_display_tables(conn)
        if mode == "query":
            expected = None
            deadline = time.monotonic() + trusted_limit_s
            conn.set_progress_handler(lambda: 1 if time.monotonic() > deadline else 0, PROGRESS_STEP)
            try:
                for stmt in split_statements(ref_sql):
                    cursor = conn.execute(stmt)
                    if cursor.description is not None:
                        names = [d[0] for d in cursor.description]
                        rows = cursor.fetchmany(ROW_LIMIT + 1)
                        if len(rows) > ROW_LIMIT:
                            raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"标准答案结果超过 {ROW_LIMIT} 行")
                        expected = {
                            "columns": _query_result_columns(names, tables),
                            "rows": [[_display_value(v) for v in row] for row in rows[:DISPLAY_ROW_LIMIT]],
                            "total_rows": len(rows),
                            "truncated": len(rows) > DISPLAY_ROW_LIMIT,
                        }
                    cursor.close()
            except sqlite3.Error as e:
                raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"标准答案执行失败: {_trusted_error_text(e)}")
            finally:
                conn.set_progress_handler(None, PROGRESS_STEP)
            if expected is None:
                raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, "标准答案未产生查询结果集")
        else:
            before = _dump_tables(conn)
            _execute_trusted(conn, ref_sql, time.monotonic() + trusted_limit_s, "标准答案执行失败")
            after = _dump_tables(conn)
            changed = {name for name in set(before) | set(after) if before.get(name) != after.get(name)}
            if not changed:
                raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, "标准答案未修改任何表数据，请检查题目配置")
            changed_tables = _dump_display_tables(conn, only=changed)
            # 被标准答案 DROP 的表已不在库中，用初始展示数据补齐条目（前端据 dropped 提示“表已删除”）
            existing = {t["name"] for t in changed_tables}
            for t in tables:
                if t["name"] in changed and t["name"] not in existing:
                    changed_tables.append({"name": t["name"], "columns": t["columns"], "rows": [], "total_rows": 0, "truncated": False, "dropped": True})
            expected = {"changed_tables": changed_tables}
        return {"tables": tables, "expected": expected}
    finally:
        conn.close()
