#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""班务通 /api/v1 只读查询层（阶段 0 · AI 生态化地基）

设计原则：
- 纯只读：本层不写任何数据文件，写操作仍走网页端 /api/* 原接口
- 不侵入：独立模块，server.py 仅在 do_GET 加一行分发
- 统一响应：{ok, data, meta} / {ok:false, error}
- 鉴权：教师口令 token（与页面 ?k=<口令> 相同），口令不写死在代码
- 面向 AI 调用：字段名语义化，附带中文说明字段，便于小程序 SKILL / WorkBuddy MCP 直接引用

接口清单（全部 GET /api/v1/<name>）：
  /api/v1/ping            无鉴权，连通性探测
  /api/v1/overview        当日班务总览（考勤+值日+班干部）
  /api/v1/attendance?date=YYYY-MM-DD[&name=学生姓名]  某日考勤（默认今天）
  /api/v1/attendance_summary?from=YYYY-MM-DD&to=YYYY-MM-DD&name=...  区间统计
  /api/v1/seating         座位表（9排×8列）
  /api/v1/beds            床位表（3舍×16床）
  /api/v1/duty            清洁轮值（按宿舍×星期）
  /api/v1/cadres          班干部分工
  /api/v1/student?name=张三  单个学生全景（座位+床位+值日+近期考勤）
  /api/v1/token 校验：?token=<教师口令> 或 Header X-Banwu-Token
"""
import json, os, re, time, datetime, calendar

BASE = os.path.dirname(os.path.abspath(__file__))
F_KQ = os.path.join(BASE, "kaoqin_data.json")
F_ZW = os.path.join(BASE, "zuowei_data.json")
F_CW = os.path.join(BASE, "chuangwei_data.json")
F_QJ = os.path.join(BASE, "qingjie_data.json")
F_GB = os.path.join(BASE, "ganbu_data.json")
F_TOKEN = os.path.join(BASE, "api_v1_token.txt")

# ---------- 载入器（失败返回安全默认值，绝不让查询层崩溃主服务）----------
def _load(path, default):
    try:
        with open(path, "r", encoding="utf-8") as f:
            d = json.load(f)
            return d
    except Exception:
        return default

def load_kaoqin(): return _load(F_KQ, {})
def load_zuowei(): return _load(F_ZW, {"v": None, "u": 0})
def load_chuangwei(): return _load(F_CW, {"v": None, "u": 0})
def load_qingjie(): return _load(F_QJ, {"v": None, "u": 0})
def load_ganbu(): return _load(F_GB, {"v": None, "u": 0})

def _load_token():
    try:
        with open(F_TOKEN, "r", encoding="utf-8") as f:
            return f.read().strip()
    except Exception:
        return None

def check_token(supplied):
    """token 文件不存在 → 拒绝所有带 token 检查的接口（安全默认）。"""
    real = _load_token()
    if not real:
        return False
    return supplied == real

# ---------- 工具 ----------
def _today(): return datetime.date.today().isoformat()

def _valid_date(s):
    return bool(re.fullmatch(r"\d{4}-\d{2}-\d{2}", s or ""))

DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
NAME_RE = re.compile(r"^[\u4e00-\u9fa5]{1,5}·?[a-zA-Z0-9]{0,4}$")

def _norm_name(s):
    if not isinstance(s, str):
        return None
    s = s.strip()
    return s or None

# ---------- 业务视图（把原始 JSON 翻译成 AI 友好结构）----------
PERIODS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]  # 9 个节次
PERIOD_LABELS = ["第1节", "第2节", "第3节", "第4节", "5/6节", "第7节", "8/9节", "晚自习一", "晚自习二"]

def view_attendance(date, name=None):
    recs = load_kaoqin().get("records", {}) if isinstance(load_kaoqin(), dict) else {}
    # 兼容老结构：顶层即日期 → 节次 → 学生
    data = load_kaoqin()
    recs = data.get("records") if isinstance(data, dict) and isinstance(data.get("records"), dict) else (data if isinstance(data, dict) else {})
    day = recs.get(date) or {}
    periods = {}
    absent_all = []  # 全天缺席者
    for p in PERIODS:
        praw = day.get(p) or {}
        entries = []
        for nm, ent in praw.items():
            if not isinstance(ent, dict):
                continue
            if name and nm != name:
                continue
            entries.append({
                "name": nm,
                "status": ent.get("st") or ent.get("s") or "未知",
                "marked_at": ent.get("u", 0),
            })
        entries.sort(key=lambda e: e["name"])
        periods[p] = {"label": PERIOD_LABELS[int(p)-1] if p.isdigit() and 1 <= int(p) <= 9 else p, "entries": entries}
    total_marks = sum(len(v["entries"]) for v in periods.values())
    return {
        "date": date,
        "periods": periods,
        "marked_total": total_marks,
        "note": "status 含：正常/病假/事假/旷课/迟到/早退等，s 为页面字段名",
    }

def view_attendance_summary(from_d, to_d, name=None):
    data = load_kaoqin()
    recs = data.get("records") if isinstance(data, dict) and isinstance(data.get("records"), dict) else (data if isinstance(data, dict) else {})
    stats = {}  # name -> {status: count}
    dates = []
    try:
        d0 = datetime.date.fromisoformat(from_d)
        d1 = datetime.date.fromisoformat(to_d)
    except Exception:
        return {"error": "invalid date range"}
    if (d1 - d0).days > 92:
        return {"error": "date range too long (max 92 days)"}
    cur = d0
    while cur <= d1:
        dates.append(cur.isoformat())
        cur += datetime.timedelta(days=1)
    for d in dates:
        day = recs.get(d) or {}
        for p, praw in day.items():
            if not isinstance(praw, dict):
                continue
            for nm, ent in praw.items():
                if not isinstance(ent, dict):
                    continue
                if name and nm != name:
                    continue
                st = ent.get("st") or ent.get("s") or "未知"
                stats.setdefault(nm, {})
                stats[nm][st] = stats[nm].get(st, 0) + 1
    return {
        "from": from_d, "to": to_d,
        "per_student": [
            {"name": k, "counts": v, "total_marks": sum(v.values())}
            for k, v in sorted(stats.items())
        ],
        "days_covered": dates,
    }

def view_seating():
    zw = load_zuowei().get("v")
    if not isinstance(zw, list):
        return {"error": "no seating data"}
    rows = []
    for ri, row in enumerate(zw):
        cells = []
        for ci, c in enumerate(row):
            names = []
            if isinstance(c, dict) and isinstance(c.get("names"), list):
                names = [n for n in c["names"] if isinstance(n, str) and n.strip()]
            cells.append({"col": ci + 1, "names": names})
        rows.append({"row": ri + 1, "cells": cells})
    seated = [n for r in rows for c in r["cells"] for n in c["names"]]
    return {"rows": rows, "seated_count": len(seated)}

def view_beds():
    cw = load_chuangwei().get("v")
    if not isinstance(cw, list):
        return {"error": "no bed data"}
    DORMS = ["2103", "2104", "2105"]
    dorms = []
    for di, dorm in enumerate(cw):
        beds = []
        if isinstance(dorm, dict):
            for i in range(1, 17):
                e = dorm.get(str(i)) or {}
                beds.append({
                    "bed_no": i,
                    "name": e.get("name"),
                    "half": bool(e.get("half")),  # 晚走读
                    "updated_at": e.get("t", 0),
                })
        dorms.append({"dorm": DORMS[di] if di < 3 else str(di), "beds": beds})
    residents = [b["name"] for d in dorms for b in d["beds"] if b["name"]]
    return {"dorms": dorms, "residents_count": len(residents)}

def view_duty():
    qj = load_qingjie().get("v")
    if not isinstance(qj, dict):
        return {"error": "no duty data"}
    DAYS = ["周一", "周二", "周三", "周四", "周五"]
    DORMS = ["2103", "2104", "2105"]
    d = qj.get("d")
    sz = qj.get("sz")
    out = []
    if isinstance(d, list):
        for di, dorm in enumerate(d):
            days = []
            if isinstance(dorm, list):
                for day_i, day in enumerate(dorm):
                    names = []
                    if isinstance(day, dict) and isinstance(day.get("names"), list):
                        names = day["names"]
                    elif isinstance(day, list):
                        names = day
                    days.append({"day": DAYS[day_i] if day_i < 5 else f"day{day_i}", "names": names})
            out.append({"dorm": DORMS[di] if di < 3 else str(di), "days": days})
    leaders = []
    if isinstance(sz, list):
        for di, s in enumerate(sz):
            if isinstance(s, dict) and isinstance(s.get("names"), list):
                leaders.append({"dorm": DORMS[di] if di < 3 else str(di), "leader": (s["names"] or [None])[0]})
    return {"dorms": out, "dorm_leaders": leaders}

def view_cadres():
    gb = load_ganbu().get("v")
    if not isinstance(gb, dict):
        return {"error": "no cadre data"}
    rows = gb.get("rows") or []
    positions = []
    for r in rows:
        if isinstance(r, list) and len(r) >= 3 and all(isinstance(c, str) for c in r[:3]):
            positions.append({"position": r[0], "duty": r[1], "members": r[2]})
    return {"title": gb.get("title"), "positions": positions}

def view_student(name):
    name = _norm_name(name)
    if not name:
        return {"error": "missing name"}
    out = {"name": name}
    # 座位
    seat = None
    zw = load_zuowei().get("v")
    if isinstance(zw, list):
        for ri, row in enumerate(zw):
            for ci, c in enumerate(row):
                if isinstance(c, dict) and name in [n for n in (c.get("names") or []) if isinstance(n, str)]:
                    seat = f"第{ri+1}排第{ci+1}列"
    out["seat"] = seat
    # 床位
    bed = None
    cw = load_chuangwei().get("v")
    if isinstance(cw, list):
        DORMS = ["2103", "2104", "2105"]
        for di, dorm in enumerate(cw):
            if not isinstance(dorm, dict):
                continue
            for i in range(1, 17):
                e = dorm.get(str(i)) or {}
                if e.get("name") == name:
                    bed = f"{DORMS[di] if di<3 else di} 第{i}床" + ("（晚走读）" if e.get("half") else "")
    out["bed"] = bed
    # 值日
    duty = []
    qj = load_qingjie().get("v")
    if isinstance(qj, dict) and isinstance(qj.get("d"), list):
        DAYS = ["周一", "周二", "周三", "周四", "周五"]
        DORMS = ["2103", "2104", "2105"]
        for di, dorm in enumerate(qj["d"]):
            if not isinstance(dorm, list):
                continue
            for day_i, day in enumerate(dorm):
                names = day.get("names") if isinstance(day, dict) and isinstance(day.get("names"), list) else (day if isinstance(day, list) else [])
                if name in [n for n in names if isinstance(n, str)]:
                    duty.append(f"{DORMS[di] if di<3 else di} {DAYS[day_i] if day_i<5 else day_i}")
    out["duty"] = duty
    # 近 7 天考勤
    today = datetime.date.today()
    recent = {}
    data = load_kaoqin()
    recs = data.get("records") if isinstance(data, dict) and isinstance(data.get("records"), dict) else (data if isinstance(data, dict) else {})
    for k in range(7):
        d = (today - datetime.timedelta(days=k)).isoformat()
        day = recs.get(d) or {}
        for p, praw in day.items():
            ent = praw.get(name) if isinstance(praw, dict) else None
            if isinstance(ent, dict):
                st = ent.get("st") or ent.get("s")
                if st and st != "正常":
                    recent.setdefault(d, []).append({"period": p, "status": st})
    out["attendance_recent_7d"] = recent if recent else "近7天全勤（或无记录）"
    return out

def view_overview():
    today = _today()
    att = view_attendance(today)
    # 汇总当日异常
    abnormal = {}
    for p, blk in att["periods"].items():
        for e in blk["entries"]:
            if e["status"] not in ("正常", None, ""):
                abnormal.setdefault(e["name"], set()).add(e["status"])
    abnormal = {k: sorted(v) for k, v in abnormal.items()}
    # 今日值日（按星期几）
    wk = ["周一","周二","周三","周四","周五","周六","周日"][datetime.date.today().weekday()]
    duty = view_duty()
    today_duty = []
    for dorm in duty.get("dorms", []):
        for dd in dorm["days"]:
            if dd["day"] == wk:
                today_duty.append({"dorm": dorm["dorm"], "names": dd["names"]})
    cadres = view_cadres()
    return {
        "date": today,
        "weekday": wk,
        "attendance_marked": att["marked_total"],
        "attendance_abnormal": abnormal,
        "today_duty": today_duty,
        "cadre_positions": len(cadres.get("positions", [])),
        "roster_note": "在读 50 人（历史名单 54 人，4 人已退学）",
    }

# ---------- 路由 ----------
def handle(path, query, headers):
    """path: /api/v1/xxx；返回 (status_code, obj) 或 None(不匹配)"""
    if not path.startswith("/api/v1/"):
        return None
    sub = path[len("/api/v1/"):]
    q = query or {}
    token = q.get("token") or (headers.get("X-Banwu-Token") if headers else None)

    if sub == "ping":
        return 200, {"ok": True, "data": {"service": "banwu", "api": "v1", "time": time.strftime("%Y-%m-%d %H:%M:%S")}}

    # 其余接口需要 token
    if not check_token(token):
        return 401, {"ok": False, "error": "unauthorized: token missing or wrong (token=<教师口令> 或 Header X-Banwu-Token)"}

    if sub == "overview":
        return 200, {"ok": True, "data": view_overview()}
    if sub == "attendance":
        d = q.get("date") or _today()
        if not _valid_date(d):
            return 400, {"ok": False, "error": "bad date, use YYYY-MM-DD"}
        return 200, {"ok": True, "data": view_attendance(d, name=q.get("name"))}
    if sub == "attendance_summary":
        f, t = q.get("from"), q.get("to")
        if not (_valid_date(f) and _valid_date(t)):
            return 400, {"ok": False, "error": "need from=YYYY-MM-DD&to=YYYY-MM-DD"}
        return 200, {"ok": True, "data": view_attendance_summary(f, t, name=q.get("name"))}
    if sub == "seating":
        return 200, {"ok": True, "data": view_seating()}
    if sub == "api" or sub == "docs":
        return 200, {"ok": True, "data": {
            "endpoints": ["ping", "overview", "attendance?date=&name=", "attendance_summary?from=&to=&name=",
                           "seating", "beds", "duty", "cadres", "student?name="]
        }}
    if sub == "beds":
        return 200, {"ok": True, "data": view_beds()}
    if sub == "duty":
        return 200, {"ok": True, "data": view_duty()}
    if sub == "cadres":
        return 200, {"ok": True, "data": view_cadres()}
    if sub == "student":
        if not q.get("name"):
            return 400, {"ok": False, "error": "need name parameter"}
        return 200, {"ok": True, "data": view_student(q.get("name"))}
    return 404, {"ok": False, "error": f"unknown endpoint /api/v1/{sub}, see /api/v1/docs"}

def _parse_query(qs):
    from urllib.parse import parse_qs
    p = parse_qs(qs or "")
    return {k: v[0] for k, v in p.items()}
