"""
app.py — Flask 后端主文件
=========================
作用：整个 MVP 的"大脑"，负责三件事：
  1. 提供 API 接口（视频列表、AI 对话、行为记录）
  2. 管理 SQLite 数据库（用户、视频、行为日志）
  3. 对接 DeepSeek 大模型 API

产品概念说明：
  - 这里用的是 "单体应用" 架构：一个 app.py 把路由+数据库+API 全包了。
    适合 MVP 阶段快速验证，后续用户量起来再拆分为微服务。
  - 路由 (Route)：浏览器访问不同网址时，Flask 把请求分发给对应的函数处理。
    类比：路由 = 你走进商场，问服务台"女装在三楼"，服务台就是路由层的功能。
"""

import gzip
import json
import os
import sqlite3
from datetime import timedelta

import requests
from flask import Flask, render_template, request, jsonify, send_from_directory

# ---- 初始化 Flask 应用 ----
app = Flask(__name__)
# 静态资源缓存 7 天（模板引用带 ?v= 版本号，更新时递增即可刷新）
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(days=7)

# 配置路径 (使用绝对路径以避免 sqlite 找不到文件)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "data").replace("\\", "/")
# 修改数据库文件名，绕过之前的旧文件锁定问题
DB_PATH = os.path.join(DATA_DIR, "miniapp_v3.db").replace("\\", "/")
VIDEOS_PATH = os.path.join(DATA_DIR, "seed_videos.json").replace("\\", "/")
STORY_PATH = os.path.join(DATA_DIR, "seed_story.json").replace("\\", "/")
MATH_TEEN_STORY_PATH = os.path.join(DATA_DIR, "数学少年线_story.json").replace("\\", "/")
SIMULATOR_PATH = os.path.join(DATA_DIR, "seed_simulator.json").replace("\\", "/")
MATHBTI_PATH = os.path.join(DATA_DIR, "seed_mathbti.json").replace("\\", "/")
ALUMNI_PATH = os.path.join(DATA_DIR, "seed_alumni.json").replace("\\", "/")

# 确保 data 目录存在
os.makedirs(DATA_DIR, exist_ok=True)


# 内容型 GET API 短缓存（种子数据极少变动）
_API_CACHE_SECONDS = {
    '/api/videos': 300,
    '/api/knowledge': 300,
    '/api/story': 300,
    '/api/simulator': 300,
    '/api/alumni': 300,
    '/api/math_personality': 300,
}


@app.after_request
def compress_response(response):
    """gzip 压缩文本响应（HTML/CSS/JS/JSON >500B）+ 内容型 API 短缓存"""
    if request.method == 'GET' and request.path in _API_CACHE_SECONDS:
        response.headers['Cache-Control'] = f"private, max-age={_API_CACHE_SECONDS[request.path]}"
    if (response.status_code != 200
            or 'Content-Encoding' in response.headers):
        return response
    if 'gzip' not in request.headers.get('Accept-Encoding', ''):
        return response
    mimetype = response.mimetype or ''
    if not (mimetype.startswith('text/') or mimetype in ('application/json', 'application/javascript')):
        return response
    if response.direct_passthrough:
        response.direct_passthrough = False  # 静态文件需读入内存才能压缩
    data = response.get_data()
    if len(data) < 500:
        return response
    compressed = gzip.compress(data, compresslevel=6)
    response.set_data(compressed)
    response.headers['Content-Encoding'] = 'gzip'
    response.headers['Content-Length'] = len(compressed)
    response.headers['Vary'] = 'Accept-Encoding'
    return response

# DeepSeek API 配置 — 需要用户填入自己的 API Key
DEEPSEEK_API_KEY = "sk-0cd8b068c4a3439c9fc6d67b08865c21"
DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"


# ============================================================
# 模块 1: 数据库初始化
# ============================================================
def init_db():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("""
        CREATE TABLE IF NOT EXISTS behavior_log (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id TEXT,
            action TEXT,
            target TEXT,
            detail TEXT,
            timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    # 旧库迁移：补充 detail 列
    try:
        c.execute("ALTER TABLE behavior_log ADD COLUMN detail TEXT")
    except Exception:
        pass
    # 知识卡片表
    c.execute("""
        CREATE TABLE IF NOT EXISTS knowledge_cards (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            question TEXT,
            description TEXT,
            detail TEXT,
            color TEXT DEFAULT '#3B82F6',
            icon TEXT DEFAULT '📖',
            topic TEXT,
            read_time TEXT DEFAULT '5分钟阅读',
            sort_order INTEGER DEFAULT 0
        )
    """)
    # 收藏表
    c.execute("""
        CREATE TABLE IF NOT EXISTS favorites (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id TEXT NOT NULL,
            target_type TEXT NOT NULL,
            target_id INTEGER NOT NULL,
            title TEXT,
            cover TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            UNIQUE(user_id, target_type, target_id)
        )
    """)
    # 观看历史表
    c.execute("""
        CREATE TABLE IF NOT EXISTS watch_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id TEXT NOT NULL,
            target_type TEXT NOT NULL,
            target_id INTEGER NOT NULL,
            title TEXT,
            cover TEXT,
            watched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    # 用户表
    c.execute("""
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id TEXT UNIQUE NOT NULL,
            nickname TEXT DEFAULT '游客用户',
            avatar_emoji TEXT DEFAULT 'π',
            avatar_color TEXT DEFAULT '#5B6AF0',
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    # 学生表（B端看板）
    c.execute("""
        CREATE TABLE IF NOT EXISTS students (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            student_id TEXT UNIQUE,
            name TEXT,
            class_name TEXT,
            grade TEXT
        )
    """)
    # 常用查询索引
    c.execute("CREATE INDEX IF NOT EXISTS idx_favorites_user ON favorites(user_id)")
    c.execute("CREATE INDEX IF NOT EXISTS idx_history_user ON watch_history(user_id)")
    c.execute("CREATE INDEX IF NOT EXISTS idx_behavior_user ON behavior_log(user_id)")
    conn.commit()
    conn.close()


def load_seed_knowledge():
    """加载知识卡片种子数据"""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT COUNT(*) FROM knowledge_cards")
    if c.fetchone()[0] > 0:
        conn.close()
        return

    cards = [
        {"title": "被平均数骗过的所有人", "question": "你和马云的平均财富是多少？", "desc": "平均数、中位数、众数——这三兄弟让你看到的「真相」可能截然相反。", "detail": "平均数是最常用的统计量，但它也是最容易被误解的。当数据中有极端值时，平均数会被拉偏。比如你和马云的平均财富可能是千亿级别，但中位数可能只有几万。了解平均数、中位数、众数的区别，是统计学入门的第一课。", "color": "#EF4444", "icon": "📊", "topic": "统计陷阱", "read_time": "5分钟阅读", "sort": 1},
        {"title": "彩票：一场精心设计的数学税", "question": "2元博500万，值不值？", "desc": "你中头奖的概率比你连续被雷劈三次还低。但为什么每天都有人买？", "detail": "双色球头奖概率约1/1772万。对比：被雷劈的概率约1/100万。彩票的期望值永远是负的——这是彩票公司盈利的数学基础。人们购买彩票不是因为他们不懂数学，而是因为损失厌恶和过度乐观的认知偏差。", "color": "#F59E0B", "icon": "🎲", "topic": "概率思维", "read_time": "5分钟阅读", "sort": 2},
        {"title": "为什么所有地图都在骗你", "question": "格陵兰岛有非洲大吗？", "desc": "把球面摊平一定会变形——高斯发现的绝妙定理决定了你看到的世界。", "detail": "墨卡托投影让格陵兰岛看起来和非洲差不多大，实际上非洲面积是格陵兰的14倍。球面无法完美展开成平面，这是高斯的'绝妙定理'（Theorema Egregium）决定的。每种地图投影都有取舍：要么保面积，要么保角度，要么保方向——你永远无法三者兼得。", "color": "#3B82F6", "icon": "🗺️", "topic": "几何直觉", "read_time": "5分钟阅读", "sort": 3},
        {"title": "大自然最爱的数列", "question": "兔子繁殖与斐波那契", "desc": "从向日葵的花瓣到鹦鹉螺的壳，斐波那契数列无处不在。这是巧合吗？", "detail": "斐波那契数列：1, 1, 2, 3, 5, 8, 13, 21... 后一项等于前两项之和。这个简单的规则在大自然中反复出现：松果的螺旋数、花瓣的排列、菠萝的鳞片——都是斐波那契数。这不是巧合，而是植物在生长过程中最优空间利用的结果。", "color": "#8B5CF6", "icon": "🔢", "topic": "数列之美", "read_time": "6分钟阅读", "sort": 4},
        {"title": "博弈论：竞争的最优解", "question": "为什么奶茶店总是开在隔壁？", "desc": "纳什均衡解释了一个奇怪的现象：竞争对手为什么总是扎堆？", "detail": "两个冰淇淋摊在海滩上竞争，最后都会摆到海滩正中间——这就是霍特林模型。纳什均衡告诉我们：当每个人都做出对自己最优的选择时，结果可能对所有人都不好。这个原理适用于从商业选址到交通拥堵的各种场景。", "color": "#10B981", "icon": "⚖️", "topic": "博弈策略", "read_time": "5分钟阅读", "sort": 5},
        {"title": "混沌理论：看不见的秩序", "question": "一只蝴蝶能引起台风吗？", "desc": "天气预报为什么不准？非线性动力学如何揭示混乱中的规律。", "detail": "洛伦兹在1961年发现：输入数据小数点后三位的微小差异，会导致完全不同的天气预报结果。这就是'蝴蝶效应'。混沌理论最迷人的地方在于：混沌不是随机——它是确定的系统产生不可预测的行为。这种'确定性的随机'挑战了我们对因果关系的直觉。", "color": "#EC4899", "icon": "🦋", "topic": "混沌理论", "read_time": "7分钟阅读", "sort": 6},
        {"title": "蒙提霍尔：三门悖论", "question": "主持人打开一扇空门后，你该换吗？", "desc": "一个让数学家和大众都吵翻天的概率谜题。", "detail": "游戏节目有三扇门：一扇后面是汽车，两扇后面是山羊。你选择一扇后，主持人打开另一扇有山羊的门。现在你可以坚持原来的选择或换到剩下那扇——换门的中奖概率是2/3，不是1/2。这个结论让无数人（包括数学家）抓狂，但它是对的。关键：主持人的行为提供了信息。", "color": "#06B6D4", "icon": "🚪", "topic": "概率思维", "read_time": "4分钟阅读", "sort": 7},
        {"title": "本福特定律：数字会说谎", "question": "财务报表里1开头的数字占比是多少？", "desc": "审计师用这条数学定律抓财务造假，准确率高得惊人。", "detail": "自然界里，首位数字为1的概率约为30%，不是直觉中的1/9。河流长度、人口数量、股票价格——都遵循本福特定律。聪明的会计师如果不知道这条定律，造出的假账一眼就会被看穿。美国国税局就用它来筛选审计目标。", "color": "#F97316", "icon": "🔍", "topic": "统计陷阱", "read_time": "5分钟阅读", "sort": 8},
    ]
    for c_data in cards:
        c.execute("""
            INSERT INTO knowledge_cards (title, question, description, detail, color, icon, topic, read_time, sort_order)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (c_data["title"], c_data["question"], c_data["desc"], c_data["detail"],
              c_data["color"], c_data["icon"], c_data["topic"], c_data["read_time"], c_data["sort"]))
    conn.commit()
    conn.close()
    print(f"[完成] 已导入 {len(cards)} 条知识卡片")


def load_seed_videos():
    """
    从 JSON 文件加载预设视频数据写入数据库。
    """
    if not os.path.exists(VIDEOS_PATH):
        print(f"[提示] 种子数据文件不存在: {VIDEOS_PATH}，跳过导入")
        return

    with open(VIDEOS_PATH, "r", encoding="utf-8") as f:
        videos = json.load(f)

    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()

    # 重建视频表（确保新增字段生效）
    c.execute("DROP TABLE IF EXISTS videos")
    c.execute("""
        CREATE TABLE videos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            module TEXT NOT NULL,
            sub_category TEXT,
            discipline TEXT,
            discipline_icon TEXT,
            video_url TEXT,
            cover_url TEXT,
            description TEXT,
            duration_min INTEGER DEFAULT 5,
            author TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)

    for v in videos:
        c.execute("""
            INSERT INTO videos (title, module, sub_category, discipline, discipline_icon, video_url, cover_url, description, duration_min, author)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (v["title"], v["module"],
              v.get("sub_category", ""), v.get("discipline", ""), v.get("discipline_icon", ""),
              v.get("video_url", ""), v.get("cover_url", ""),
              v.get("description", ""), v.get("duration_min", 5), v.get("author", "")))

    conn.commit()
    conn.close()
    print(f"[完成] 已导入 {len(videos)} 条视频数据")


# ============================================================
# 模块 2: 路由定义（API 接口）
# ============================================================

@app.route("/")
def index():
    """
    主页路由 — 返回 SPA（单页应用）的 HTML。
    前端所有页面切换都在这一个 HTML 里完成，不刷新浏览器。
    """
    return render_template("index.html")


@app.route("/demo")
def demo():
    """返回独立静态 Demo 页面"""
    return send_from_directory(BASE_DIR, "mathbrain-demo.html")


@app.route("/web")
def web_desktop():
    """桌面端网页版 — 左侧栏 + 横向布局"""
    return render_template("desktop.html")


@app.route("/data/<path:filename>")
def data_files(filename):
    """本地开发时提供 data/ 下的公开 JSON 数据"""
    if not filename.lower().endswith(".json"):
        return jsonify({"code": -1, "msg": "not allowed"}), 404
    return send_from_directory(DATA_DIR, filename)


@app.route("/math_teen")
def math_teen():
    """数学少年线互动剧本独立播放页"""
    return render_template("math_teen.html")


@app.route("/api/videos")
def api_videos():
    """
    获取视频列表 API。
    支持 module 和 discipline 参数筛选：
      /api/videos?module=数人
      /api/videos?module=数人&discipline=计算机与AI
    """
    module = request.args.get("module", "")
    discipline = request.args.get("discipline", "")
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    c = conn.cursor()

    query = "SELECT * FROM videos WHERE 1=1"
    params = []
    if module:
        query += " AND module=?"
        params.append(module)
    if discipline:
        query += " AND discipline=?"
        params.append(discipline)
    query += " ORDER BY module, id DESC"

    c.execute(query, params)
    rows = [dict(r) for r in c.fetchall()]

    # 同时返回当前模块的学科列表（用于前端渲染分类筛选）
    if module == "数人":
        c.execute("SELECT DISTINCT discipline, discipline_icon FROM videos WHERE module='数人' AND discipline != '' ORDER BY discipline")
        disciplines = [dict(r) for r in c.fetchall()]
        conn.close()
        return jsonify({"code": 0, "data": rows, "disciplines": disciplines})

    conn.close()
    return jsonify({"code": 0, "data": rows})


@app.route("/api/chat", methods=["POST"])
def api_chat():
    """
    AI 对话接口 — 转发用户消息给 DeepSeek，支持上下文感知。

    请求格式: { "message": "学数学对学医有什么用？", "context": {"type": "video", "title": "xxx"} }
    """
    data = request.get_json()
    user_message = data.get("message", "")
    student_id = data.get("student_id", "")
    context = data.get("context") or {}
    ctx_type = context.get("type", "")
    ctx_title = context.get("title", "")

    if not DEEPSEEK_API_KEY or DEEPSEEK_API_KEY == "your-deepseek-api-key-here":
        return jsonify({
            "code": -1,
            "reply": "DeepSeek API Key 尚未配置，请在 app.py 中填入你的 Key。\n申请地址: https://platform.deepseek.com/",
            "is_fallback": True
        })

    # 根据上下文构建 system prompt
    system_prompt = (
        "你是'葫芦数学'的AI助教，面向中小学生和数学爱好者。你的核心任务是：\n"
        "1. 用生活化的语言解释数学在各学科中的基础作用\n"
        "2. 引导学生发现'原来数学这么有用'\n"
        "3. 回答尽量简短、有趣，控制在200字以内\n"
        "4. 不要出现超出初中生理解范围的专业术语"
    )
    if ctx_type == "video" and ctx_title:
        system_prompt = (
            f"用户正在观看视频《{ctx_title}》。你是'葫芦数学'的AI助教。\n"
            f"请围绕这个视频主题，用生活化的语言帮助用户理解其中的数学概念。\n"
            "回答简短有趣，控制在200字以内。"
        )
    elif ctx_type == "knowledge" and ctx_title:
        system_prompt = (
            f"用户正在阅读知识卡片《{ctx_title}》。你是'葫芦数学'的AI助教。\n"
            f"请围绕这个主题深入浅出地解释，可以补充有趣的例子和冷知识。\n"
            "回答简短有趣，控制在200字以内。"
        )
    elif ctx_type == "simulator":
        system_prompt = (
            "用户正在玩'数学专业模拟器'——这是模拟大学数学系4年学习生涯的互动游戏。\n"
            "你是'葫芦数学'的AI助教，请扮演大学数学系的学长/学姐角色，\n"
            "用鼓励、有趣的方式回答用户关于数学专业的问题。\n"
            "可以分享一些大学数学系的真实体验和趣事。回答在200字以内。"
        )

    try:
        resp = requests.post(
            DEEPSEEK_API_URL,
            headers={
                "Authorization": f"Bearer {DEEPSEEK_API_KEY}",
                "Content-Type": "application/json"
            },
            json={
                "model": "deepseek-chat",
                "messages": [
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": user_message}
                ],
                "temperature": 0.7,
                "max_tokens": 500
            },
            timeout=15
        )
        result = resp.json()
        reply = result["choices"][0]["message"]["content"]
        is_fallback = False

    except Exception as e:
        reply = f"[助手暂时离线] API 调用失败: {str(e)[:100]}"
        is_fallback = True

    # 记录行为日志
    _log_behavior(student_id, "send_message", detail=user_message)

    return jsonify({"code": 0, "reply": reply, "is_fallback": is_fallback, "context_type": ctx_type})


@app.route("/api/log", methods=["POST"])
def api_log():
    data = request.json
    user_id = data.get("user_id", "guest")
    action = data.get("action", "")
    target = data.get("target", "")

    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute(
        "INSERT INTO behavior_log (user_id, action, target) VALUES (?, ?, ?)",
        (user_id, action, target)
    )
    conn.commit()
    conn.close()
    return jsonify({"code": 0, "msg": "logged"})


@app.route("/api/students")
def api_students():
    """获取预设学生列表（B端看板用）"""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    c = conn.cursor()
    c.execute("SELECT * FROM students ORDER BY grade, class_name")
    rows = [dict(r) for r in c.fetchall()]
    conn.close()
    return jsonify({"code": 0, "data": rows})


@app.route("/api/story")
def api_story():
    """获取数学武林故事数据"""
    if not os.path.exists(STORY_PATH):
        return jsonify({"code": -1, "msg": "故事数据文件不存在"})
    with open(STORY_PATH, "r", encoding="utf-8") as f:
        story = json.load(f)
    return jsonify({"code": 0, "data": story})


@app.route("/api/story/math_teen")
def api_story_math_teen():
    """获取数学少年线互动剧情数据"""
    if not os.path.exists(MATH_TEEN_STORY_PATH):
        return jsonify({"code": -1, "msg": "数学少年线故事数据文件不存在"})
    with open(MATH_TEEN_STORY_PATH, "r", encoding="utf-8") as f:
        story = json.load(f)
    return jsonify({"code": 0, "data": story})


@app.route("/api/alumni")
def api_alumni():
    """获取数学校友访谈与四年度模拟预留数据"""
    if not os.path.exists(ALUMNI_PATH):
        return jsonify({"code": 0, "data": [], "meta": {"status": "reserved"}})
    with open(ALUMNI_PATH, "r", encoding="utf-8") as f:
        payload = json.load(f)
    return jsonify({"code": 0, "data": payload.get("alumni", []), "meta": payload.get("meta", {})})


@app.route("/api/simulator")
def api_simulator():
    """获取数学专业模拟器数据"""
    if not os.path.exists(SIMULATOR_PATH):
        return jsonify({"code": -1, "msg": "模拟器数据文件不存在"})
    with open(SIMULATOR_PATH, "r", encoding="utf-8") as f:
        sim = json.load(f)
    return jsonify({"code": 0, "data": sim})


@app.route("/api/simulator/save", methods=["POST"])
def api_simulator_save():
    """保存模拟器进度"""
    data = request.get_json()
    user_id = data.get("user_id", "guest")
    save_data = json.dumps(data.get("state", {}))
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("""
        CREATE TABLE IF NOT EXISTS simulator_saves (
            user_id TEXT PRIMARY KEY,
            state TEXT,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    c.execute("INSERT OR REPLACE INTO simulator_saves (user_id, state) VALUES (?, ?)", (user_id, save_data))
    conn.commit()
    conn.close()
    return jsonify({"code": 0, "msg": "saved"})


@app.route("/api/simulator/save", methods=["GET"])
def api_simulator_load():
    """加载模拟器进度"""
    user_id = request.args.get("user_id", "guest")
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("CREATE TABLE IF NOT EXISTS simulator_saves (user_id TEXT PRIMARY KEY, state TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
    c.execute("SELECT state FROM simulator_saves WHERE user_id=?", (user_id,))
    row = c.fetchone()
    conn.close()
    if row:
        return jsonify({"code": 0, "data": json.loads(row[0])})
    return jsonify({"code": 0, "data": None})


@app.route("/api/knowledge")
def api_knowledge():
    """获取知识卡片列表"""
    topic = request.args.get("topic", "")
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    c = conn.cursor()
    query = "SELECT * FROM knowledge_cards WHERE 1=1"
    params = []
    if topic:
        query += " AND topic=?"
        params.append(topic)
    query += " ORDER BY sort_order"
    c.execute(query, params)
    rows = [dict(r) for r in c.fetchall()]
    # 获取所有话题
    c.execute("SELECT DISTINCT topic FROM knowledge_cards WHERE topic != '' ORDER BY topic")
    topics = [r["topic"] for r in c.fetchall()]
    conn.close()
    return jsonify({"code": 0, "data": rows, "topics": topics})


@app.route("/api/knowledge/<int:card_id>")
def api_knowledge_detail(card_id):
    """获取单个知识卡片详情"""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    c = conn.cursor()
    c.execute("SELECT * FROM knowledge_cards WHERE id=?", (card_id,))
    row = c.fetchone()
    conn.close()
    if row:
        return jsonify({"code": 0, "data": dict(row)})
    return jsonify({"code": -1, "msg": "卡片不存在"})


@app.route("/api/favorites", methods=["GET", "POST", "DELETE"])
def api_favorites():
    """收藏管理"""
    user_id = request.args.get("user_id") or (request.get_json() or {}).get("user_id", "guest")

    if request.method == "GET":
        target_type = request.args.get("type", "")
        conn = sqlite3.connect(DB_PATH)
        conn.row_factory = sqlite3.Row
        c = conn.cursor()
        if target_type:
            c.execute("SELECT * FROM favorites WHERE user_id=? AND target_type=? ORDER BY created_at DESC", (user_id, target_type))
        else:
            c.execute("SELECT * FROM favorites WHERE user_id=? ORDER BY created_at DESC", (user_id,))
        rows = [dict(r) for r in c.fetchall()]
        conn.close()
        return jsonify({"code": 0, "data": rows})

    elif request.method == "POST":
        data = request.get_json()
        target_type = data.get("target_type", "video")
        target_id = data.get("target_id", 0)
        title = data.get("title", "")
        cover = data.get("cover", "")
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        try:
            c.execute(
                "INSERT OR IGNORE INTO favorites (user_id, target_type, target_id, title, cover) VALUES (?, ?, ?, ?, ?)",
                (user_id, target_type, target_id, title, cover)
            )
            conn.commit()
            added = c.rowcount > 0
        except Exception:
            added = False
        conn.close()
        return jsonify({"code": 0, "msg": "collected" if added else "already_exists"})

    elif request.method == "DELETE":
        data = request.get_json()
        target_type = data.get("target_type", "video")
        target_id = data.get("target_id", 0)
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        c.execute("DELETE FROM favorites WHERE user_id=? AND target_type=? AND target_id=?", (user_id, target_type, target_id))
        conn.commit()
        conn.close()
        return jsonify({"code": 0, "msg": "removed"})


@app.route("/api/favorites/check")
def api_favorites_check():
    """批量检查收藏状态"""
    user_id = request.args.get("user_id", "guest")
    target_type = request.args.get("type", "video")
    ids_str = request.args.get("ids", "")
    if not ids_str:
        return jsonify({"code": 0, "data": {}})
    ids = [int(x) for x in ids_str.split(",") if x.strip().isdigit()]
    if not ids:
        return jsonify({"code": 0, "data": {}})
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    placeholders = ",".join("?" * len(ids))
    c.execute(f"SELECT target_id FROM favorites WHERE user_id=? AND target_type=? AND target_id IN ({placeholders})",
              [user_id, target_type] + ids)
    favorited = {row[0]: True for row in c.fetchall()}
    conn.close()
    return jsonify({"code": 0, "data": favorited})


@app.route("/api/history", methods=["GET", "POST"])
def api_history():
    """观看历史"""
    user_id = request.args.get("user_id") or (request.get_json() or {}).get("user_id", "guest")

    if request.method == "GET":
        target_type = request.args.get("type", "")
        limit = int(request.args.get("limit", 20))
        conn = sqlite3.connect(DB_PATH)
        conn.row_factory = sqlite3.Row
        c = conn.cursor()
        if target_type:
            c.execute("SELECT * FROM watch_history WHERE user_id=? AND target_type=? ORDER BY watched_at DESC LIMIT ?", (user_id, target_type, limit))
        else:
            c.execute("SELECT * FROM watch_history WHERE user_id=? ORDER BY watched_at DESC LIMIT ?", (user_id, limit))
        rows = [dict(r) for r in c.fetchall()]
        conn.close()
        return jsonify({"code": 0, "data": rows})

    elif request.method == "POST":
        data = request.get_json()
        target_type = data.get("target_type", "video")
        target_id = data.get("target_id", 0)
        title = data.get("title", "")
        cover = data.get("cover", "")
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        c.execute(
            "INSERT INTO watch_history (user_id, target_type, target_id, title, cover) VALUES (?, ?, ?, ?, ?)",
            (user_id, target_type, target_id, title, cover)
        )
        conn.commit()
        conn.close()
        return jsonify({"code": 0, "msg": "recorded"})


@app.route("/api/user/profile", methods=["GET", "POST"])
def api_user_profile():
    """用户个人资料"""
    user_id = request.args.get("user_id") or (request.get_json() or {}).get("user_id", "guest")

    if request.method == "GET":
        conn = sqlite3.connect(DB_PATH)
        conn.row_factory = sqlite3.Row
        c = conn.cursor()
        c.execute("SELECT * FROM users WHERE user_id=?", (user_id,))
        row = c.fetchone()
        if not row:
            c.execute("INSERT INTO users (user_id) VALUES (?)", (user_id,))
            conn.commit()
            c.execute("SELECT * FROM users WHERE user_id=?", (user_id,))
            row = c.fetchone()
        conn.close()
        return jsonify({"code": 0, "data": dict(row)})

    elif request.method == "POST":
        data = request.get_json()
        nickname = data.get("nickname", "")
        avatar_emoji = data.get("avatar_emoji", "")
        avatar_color = data.get("avatar_color", "")
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        c.execute("INSERT INTO users (user_id) VALUES (?) ON CONFLICT(user_id) DO NOTHING", (user_id,))
        if nickname:
            c.execute("UPDATE users SET nickname=? WHERE user_id=?", (nickname, user_id))
        if avatar_emoji:
            c.execute("UPDATE users SET avatar_emoji=? WHERE user_id=?", (avatar_emoji, user_id))
        if avatar_color:
            c.execute("UPDATE users SET avatar_color=? WHERE user_id=?", (avatar_color, user_id))
        conn.commit()
        conn.close()
        return jsonify({"code": 0, "msg": "updated"})


@app.route("/api/user/stats")
def api_user_stats():
    """用户统计数据"""
    user_id = request.args.get("user_id", "guest")
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT COUNT(*) FROM watch_history WHERE user_id=?", (user_id,))
    watch_count = c.fetchone()[0] or 0
    c.execute("SELECT COUNT(*) FROM favorites WHERE user_id=?", (user_id,))
    fav_count = c.fetchone()[0] or 0
    conn.close()
    return jsonify({"code": 0, "data": {"watch_count": watch_count, "favorite_count": fav_count}})


# ---- 工具函数 ----
def _log_behavior(student_id, action_type, target_id="", detail=""):
    """内部函数：写行为日志"""
    try:
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        c.execute(
            "INSERT INTO behavior_log (user_id, action, target, detail) VALUES (?, ?, ?, ?)",
            (student_id, action_type, target_id, detail)
        )
        conn.commit()
        conn.close()
    except Exception:
        pass  # 日志写入失败不阻塞主流程


# ---- 预设学生数据（模拟学校导入） ----
def seed_students():
    """插入预设学生，模拟'三下乡'时学校提供的名单"""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT COUNT(*) FROM students")
    if c.fetchone()[0] == 0:
        students = [
            ("2024001", "张三", "七年级(1)班", "7"),
            ("2024002", "李四", "七年级(1)班", "7"),
            ("2024003", "王五", "七年级(2)班", "7"),
            ("2024004", "赵六", "八年级(1)班", "8"),
            ("2024005", "陈七", "八年级(1)班", "8"),
            ("2024006", "刘八", "九年级(1)班", "9"),
        ]
        c.executemany(
            "INSERT INTO students (student_id, name, class_name, grade) VALUES (?, ?, ?, ?)",
            students
        )
    conn.commit()
    conn.close()


# ============================================================
# MathBTI — 数学人格测试
# ============================================================

@app.route("/mathbti")
def mathbti_page():
    """MathBTI 独立页面 — 可分享链接"""
    return render_template("mathbti.html")


@app.route("/api/math_personality")
def api_math_personality():
    """获取数学人格测试题目数据"""
    if not os.path.exists(MATHBTI_PATH):
        return jsonify({"code": -1, "msg": "测试数据文件不存在"})
    with open(MATHBTI_PATH, "r", encoding="utf-8") as f:
        data = json.load(f)
    return jsonify({"code": 0, "data": data})


@app.route("/api/math_personality/calculate", methods=["POST"])
def api_math_personality_calculate():
    """
    根据用户答案计算结果
    请求格式: { "answers": [0,3,2,1,0,3,2,1,0,3,2,1] }  # 12个得分值
    返回: { code: 0, data: { binary_code: "0101", result: {...}, axis_scores: {...} } }
    """
    req_data = request.get_json()
    answers = req_data.get("answers", []) if req_data else []

    if not os.path.exists(MATHBTI_PATH):
        return jsonify({"code": -1, "msg": "测试数据文件不存在"})

    with open(MATHBTI_PATH, "r", encoding="utf-8") as f:
        test_data = json.load(f)

    if len(answers) != 12:
        return jsonify({"code": -1, "msg": "需要12个答案"})

    questions = test_data.get("questions", [])
    axes = test_data.get("scoring", {}).get("axes", ["style", "purpose", "era", "social"])
    cutoff = test_data.get("scoring", {}).get("cutoff", 4)

    # 计算每轴总分
    axis_scores = {ax: 0 for ax in axes}
    for i, q in enumerate(questions):
        ax = q.get("axis", "")
        score = answers[i] if i < len(answers) else 0
        if ax in axis_scores:
            axis_scores[ax] += score

    # 生成二进制码
    binary_code = ""
    for ax in axes:
        total = axis_scores.get(ax, 0)
        binary_code += "1" if total > cutoff else "0"

    # 获取结果
    results = test_data.get("results", {})
    result = results.get(binary_code, results.get("0000", {}))

    return jsonify({
        "code": 0,
        "data": {
            "binary_code": binary_code,
            "result": result,
            "axis_scores": axis_scores
        }
    })


# ============================================================
# 模块 3: 应用启动入口
# ============================================================
if __name__ == "__main__":
    print("=" * 50)
    print("  数学桥梁计划 · MVP 后端启动中...")
    print("=" * 50)
    init_db()
    load_seed_videos()
    load_seed_knowledge()
    seed_students()
    print(f"\n  访问地址: http://127.0.0.1:5000\n")
    app.run(debug=False, host="0.0.0.0", port=5000)
