#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""study.py — 对话内学习伴侣引擎（Telegram 学习模式）

用法（协调者 agent 通过 exec 调用）：
  python3 scripts/study.py init                     # 从 flashcards 导入卡片（可重复执行=增量合并）
  python3 scripts/study.py due [--module 01] [--limit 10]   # 到期卡片（含 Q/A）
  python3 scripts/study.py rate <card_id> <0-5> [card_id q ...]  # 记录评分（SM-2）
  python3 scripts/study.py plan                     # 今天的计划（按 STUDY_PLAN 推进）
  python3 scripts/study.py done <W1D1>              # 标记某天完成 → 推进计划
  python3 scripts/study.py quiz <module> <0-100>    # 记录模块测验得分
  python3 scripts/study.py conf <module> <1-5>      # 记录模块信心度
  python3 scripts/study.py progress                 # 进度总览
  python3 scripts/study.py stats                    # 闪卡统计
纯标准库；状态存 learner/state.json。
"""
import csv, json, sys, argparse
from datetime import date, timedelta
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
STATE = ROOT / 'learner' / 'state.json'
FLASHCARDS = ROOT / 'flashcards'

# ---- STUDY_PLAN v1.0 的 42 天计划（6 周 × 7 天，周日=复习） ----
PLAN = [
    ("W1D1", "01", "读 §Token/分词/注意力；01 前测；跑 token_estimation.py"),
    ("W1D2", "01", "读 §上下文窗口/温度/幻觉；exam-style 正式做；Lab 1–2"),
    ("W1D3", "01", "读 §训练vs推理 + architect-questions；剩余实验"),
    ("W1D4", "02", "读 §模型家族；前测；跑 model_matrix.py"),
    ("W1D5", "02", "读 §消息/stop reasons/streaming；exam-style；Labs"),
    ("W1D6", "02", "读 §token计量/成本/生命周期；stop_reason_router.py"),
    ("W1D7", "--", "周日复习：01+02 闪卡 + 错题"),
    ("W2D1", "03", "读 §认证/Messages API；api_walkthrough.py"),
    ("W2D2", "03", "读 §错误/重试；retry_backoff.py 实跑"),
    ("W2D3", "03", "读 §caching/Batches/并发；Labs"),
    ("W2D4", "05", "读 §提示工程/XML/few-shot；few_shot_builder.py"),
    ("W2D5", "05", "读 §结构化输出；validate_retry_harness.py"),
    ("W2D6", "05", "读 §thinking 现状+版本管理；Labs"),
    ("W2D7", "--", "周日复习：03+05"),
    ("W3D1", "04", "读 §工具回路；tool_loop.py"),
    ("W3D2", "04", "读 §tool_choice/错误回传/幂等；exam-style"),
    ("W3D3", "04", "「谁决定什么」三栏表 + 场景题"),
    ("W3D4", "06", "读 §三方架构+三原语；01_server_primitives_demo.py"),
    ("W3D5", "06", "读 §transports/MCP vs API；示例 02+03"),
    ("W3D6", "06", "读 §MCP 安全；Labs"),
    ("W3D7", "--", "周日复习：04+06"),
    ("W4D1", "07", "读 §embeddings/BM25/hybrid；示例 01"),
    ("W4D2", "07", "读 §chunking/rerank/查询改写；示例 02"),
    ("W4D3", "07", "五路对比 + lost-in-middle + 场景题"),
    ("W4D4", "08", "读 §workflow vs agent；agent_loop_simulator.py"),
    ("W4D5", "08", "读 §编排模式；02_workflow_vs_agent_decision.py"),
    ("W4D6", "08", "读 §memory/subagent + BEA 精读；03 骨架"),
    ("W4D7", "--", "周日复习：07+08"),
    ("W5D1", "09", "09 README 全篇；Lab 1–2"),
    ("W5D2", "09", "§permissions/hooks/subagents/MCP；Labs"),
    ("W5D3", "10", "§参考架构/故障三件套；01_resilient…py"),
    ("W5D4", "10", "§成本三角；02_cost_latency_estimator.py"),
    ("W5D5", "11", "§注入攻防/应用层强制；两个对照实验"),
    ("W5D6", "12", "§evals/金标/指标；评分器红→修绿"),
    ("W5D7", "--", "周日复习：09–12"),
    ("W6D1", "13", "Capstone README + 参考架构；D01–D05"),
    ("W6D2", "13", "bad-vs-better + rubric；D06–D10"),
    ("W6D3", "13", "D11–D15 + rubric 自评"),
    ("W6D4", "14", "domain-map/vocabulary；mock-01 限时盲测"),
    ("W6D5", "14", "common-traps/strategy；mock-02 + 架构练习抽做"),
    ("W6D6", "14", "last-week-review + 总库闪卡薄弱类"),
    ("W6D7", "14", "考前：CHEATSHEET + last-day-review"),
]

def load():
    return json.loads(STATE.read_text()) if STATE.exists() else {"cards": {}, "modules": {}, "plan_done": [], "sessions": []}

def save(s):
    STATE.parent.mkdir(parents=True, exist_ok=True)
    STATE.write_text(json.dumps(s, ensure_ascii=False, indent=1))

def today(): return date.today().isoformat()

def touch(s, action):
    s['sessions'].append({"d": today(), "a": action})

def sm2(card, q):
    q = max(0, min(5, q))
    if q < 3:
        card['reps'] = 0
        card['interval'] = 1
        card['lapses'] = card.get('lapses', 0) + 1
    else:
        if card.get('reps', 0) == 0: card['interval'] = 1
        elif card.get('reps', 0) == 1: card['interval'] = 6
        else: card['interval'] = max(1, round(card['interval'] * card['ease']))
        card['reps'] = card.get('reps', 0) + 1
    card['ease'] = max(1.3, min(3.0, card.get('ease', 2.5) + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))))
    card['due'] = (date.today() + timedelta(days=card['interval'])).isoformat()
    card['last'] = today()

def cmd_init():
    s = load()
    s.setdefault('cards', {}); s.setdefault('modules', {}); s.setdefault('plan_done', []); s.setdefault('sessions', [])
    n = 0
    for f in sorted(FLASHCARDS.glob('flashcards-[0-9][0-9].csv')):
        mod = f.stem.split('-')[1]
        for i, row in enumerate(csv.reader(f.open()), 1):
            if i == 1 or not row or not row[0].strip(): continue
            cid = f"M{mod}-{i-1:03d}"
            if cid not in s['cards']:
                s['cards'][cid] = {"q": row[0], "a": row[1], "cat": row[2] if len(row) > 2 else "", "due": today(), "interval": 0, "ease": 2.5, "reps": 0, "lapses": 0}
                n += 1
    save(s); print(f"init ok: +{n} 新卡, 总 {len(s['cards'])} 张")

def cmd_due(module=None, limit=10):
    s = load()
    t = today()
    due = [(cid, c) for cid, c in s['cards'].items() if c['due'] <= t]
    if module: due = [(cid, c) for cid, c in due if cid.startswith(f"M{module}")]
    due.sort(key=lambda x: (x[1]['due'], x[0]))
    print(f"到期 {len(due)} 张（今日 {t}）" + (f"，模块 {module}" if module else ""))
    for cid, c in due[:limit]:
        print(f"\n[{cid}] ({c['cat']})\nQ: {c['q']}\nA: {c['a']}")

def cmd_rate(pairs):
    s = load()
    for i in range(0, len(pairs), 2):
        cid, q = pairs[i], int(pairs[i+1])
        if cid in s['cards']:
            sm2(s['cards'][cid], q)
            print(f"{cid} -> q{q}, interval={s['cards'][cid]['interval']}d, due={s['cards'][cid]['due']}")
    touch(s, 'rate'); save(s)

def cmd_plan():
    s = load()
    for day, mod, task in PLAN:
        if day not in s['plan_done']:
            prev = [d for d in s['plan_done']]
            print(f"📅 下一步：{day}（模块 {mod}）{task}")
            if prev: print(f"已完成：{len(prev)} 天（最新 {prev[-1]}）")
            return
    print("🎉 42 天计划全部完成！进入自由复习：每天 due + 模拟卷错题重做")

def cmd_done(day):
    s = load()
    if day not in s['plan_done']: s['plan_done'].append(day)
    s['plan_done'].sort(key=lambda d: PLAN.index(next(p for p in PLAN if p[0] == d)) if d in [p[0] for p in PLAN] else 0)
    touch(s, 'plan'); save(s); print(f"{day} ✅ 完成"); cmd_plan()

def cmd_quiz(module, score):
    s = load(); m = s['modules'].setdefault(module, {"quiz": [], "conf": None})
    m['quiz'].append({"d": today(), "s": score})
    touch(s, 'quiz'); save(s)
    best = max(x['s'] for x in m['quiz'])
    print(f"模块 {module} 测验 {score}% 已记录（历史最好 {best}%）" + ("  ✅ 达标" if score >= 80 else "  ⚠️ <80%，标记需复习"))

def cmd_conf(module, c):
    s = load(); s['modules'].setdefault(module, {"quiz": [], "conf": None})['conf'] = c
    touch(s, 'conf'); save(s); print(f"模块 {module} 信心度 {c}/5 已记录" + ("  ⚠️ ≤3 建议重读 bad vs better + 错题" if c <= 3 else ""))

def cmd_progress():
    s = load(); t = today()
    print(f"📊 学习进度（{t}）")
    # 计划
    done = s['plan_done']; nexts = next((p for p in PLAN if p[0] not in done), None)
    print(f"📅 计划：{len(done)}/42 天完成" + (f"；下一步 {nexts[0]}（模块 {nexts[1]}）" if nexts else "；已全部完成"))
    # 模块
    for mod in [f"{i:02d}" for i in range(1, 15)]:
        m = s['modules'].get(mod)
        if m and (m['quiz'] or m['conf']):
            best = max((x['s'] for x in m['quiz']), default=None)
            print(f"  模块 {mod}: 测验 {'最高 ' + str(best) + '%' if best is not None else '—'} · 信心 {str(m['conf']) + '/5' if m['conf'] else '—'}")
    # 卡
    cards = s['cards'].values()
    due_n = sum(1 for c in cards if c['due'] <= t)
    new_n = sum(1 for c in cards if c['reps'] == 0)
    mat_n = sum(1 for c in cards if c['interval'] >= 21)
    print(f"🃏 闪卡：总 {len(cards)} · 今日到期 {due_n} · 未学 {new_n} · 已巩固(≥21d) {mat_n}")
    # 连续天数
    days = sorted({x['d'] for x in s['sessions']}, reverse=True)
    streak = 0; d = date.today()
    for _ in range(400):
        if d.isoformat() in days: streak += 1; d -= timedelta(days=1)
        else: break
    print(f"🔥 连续学习：{streak} 天（累计活跃 {len(days)} 天）")

def cmd_stats():
    s = load(); t = today()
    per = {}
    for cid, c in s['cards'].items():
        mod = cid[1:3]
        d = per.setdefault(mod, {"total": 0, "due": 0, "learned": 0})
        d['total'] += 1
        if c['due'] <= t: d['due'] += 1
        if c['interval'] >= 21: d['learned'] += 1
    print("模块 | 总数 | 到期 | 巩固")
    for mod in sorted(per):
        d = per[mod]
        print(f"  {mod}  | {d['total']:4d} | {d['due']:4d} | {d['learned']:4d}")

if __name__ == '__main__':
    ap = argparse.ArgumentParser()
    ap.add_argument('cmd', choices=['init', 'due', 'rate', 'plan', 'done', 'quiz', 'conf', 'progress', 'stats'])
    ap.add_argument('args', nargs='*')
    ap.add_argument('--module'); ap.add_argument('--limit', type=int, default=10)
    a = ap.parse_args()
    if a.cmd == 'init': cmd_init()
    elif a.cmd == 'due': cmd_due(a.module, a.limit)
    elif a.cmd == 'rate': cmd_rate(a.args)
    elif a.cmd == 'plan': cmd_plan()
    elif a.cmd == 'done': cmd_done(a.args[0])
    elif a.cmd == 'quiz': cmd_quiz(a.args[0], int(a.args[1]))
    elif a.cmd == 'conf': cmd_conf(a.args[0], int(a.args[1]))
    elif a.cmd == 'progress': cmd_progress()
    elif a.cmd == 'stats': cmd_stats()
