#!/usr/bin/env python3
"""考勤共享存储服务：静态页面 + /api/kaoqin 记录合并接口（多设备共享，时间戳大者胜）"""
import json, os, threading, gzip as _gzip
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler

BASE = os.path.dirname(os.path.abspath(__file__))
DATA_FILE = os.path.join(BASE, "kaoqin_data.json")
CW_FILE = os.path.join(BASE, "chuangwei_data.json")
QJ_FILE = os.path.join(BASE, "qingjie_data.json")
LOCK = threading.Lock()


def load_data():
    try:
        with open(DATA_FILE, "r", encoding="utf-8") as f:
            d = json.load(f)
            return d if isinstance(d, dict) else {}
    except Exception:
        return {}


def save_data(d):
    tmp = DATA_FILE + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(d, f, ensure_ascii=False)
    os.replace(tmp, DATA_FILE)


def merge_entry(old, new):
    """时间戳大者胜（共享文档模型，同床位表 merge_bed）：只有更新的数据才覆盖。
    自动补记/旧快照 u=0 或旧时间戳，永远盖不掉云端已有的较新数据；
    教师/一体机新产生的条目 u=Date.now() 自然最断，天然赢得覆盖权。"""
    if not isinstance(old, dict):
        return new
    ot = old.get("u", 0) or 0
    nt = new.get("u", 0) or 0
    return new if nt > ot else old


def merge_records(server, incoming, replace=False):
    if replace:
        return incoming if isinstance(incoming, dict) else {}
    out = dict(server)
    for d, periods in (incoming or {}).items():
        if not isinstance(periods, dict):
            continue
        out.setdefault(d, {})
        for p, rec in periods.items():
            if not isinstance(rec, dict):
                continue
            dst = dict(out[d].get(p) or {}) if isinstance(out[d].get(p), dict) else {}
            for name, ent in rec.items():
                if not isinstance(ent, dict):
                    continue
                old = dst.get(name)
                dst[name] = merge_entry(old, ent) if isinstance(old, dict) else ent
            out[d][p] = dst
    return out


def load_qj():
    try:
        with open(QJ_FILE, "r", encoding="utf-8") as f:
            d = json.load(f)
            return d if isinstance(d, dict) else {}
    except Exception:
        return {}


def save_qj(d):
    tmp = QJ_FILE + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(d, f, ensure_ascii=False)
    os.replace(tmp, QJ_FILE)

def load_cw():
    try:
        with open(CW_FILE, "r", encoding="utf-8") as f:
            d = json.load(f)
            if isinstance(d, dict) and "u" in d:
                return d
    except Exception:
        pass
    return {"v": None, "u": 0}


def save_cw(d):
    tmp = CW_FILE + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(d, f, ensure_ascii=False)
    os.replace(tmp, CW_FILE)


def merge_bed(old, new):
    """格子级合并（共享文档模型）：床位时间戳 t 大者胜；t 相同保留服务器现有。
    旧数据/未改动的格子 t=0，永远不会盖掉任何有 t 的内容；主动清空的格子带新 t，可正常同步删除。"""
    old = old if isinstance(old, dict) else {}
    new = new if isinstance(new, dict) else {}
    ot = old.get("t", 0) or 0
    nt = new.get("t", 0) or 0
    if nt > ot:
        return new
    return old


def dedupe(out):
    """全班名字唯一：同一学生出现在多个床位时，保留 t 最新的，其余清空（t 取最大+1 保证传播）。"""
    seen = {}
    dups = {}
    for di, dorm in enumerate(out):
        for i in range(1, 17):
            k = str(i)
            e = dorm.get(k) or {}
            n = e.get("name")
            if not n:
                continue
            t = e.get("t", 0) or 0
            if n in seen:
                dups[n] = True
                if t > seen[n][0]:
                    seen[n] = (t, di, k)
            else:
                seen[n] = (t, di, k)
    if not dups:
        return out, []
    removed = []
    for n, (t, di, k) in seen.items():
        if n not in dups:
            continue
        # 找到该名字所有出现位置，只留 seen 中那个
    for di, dorm in enumerate(out):
        for i in range(1, 17):
            k = str(i)
            e = dorm.get(k) or {}
            n = e.get("name")
            if not n or n not in dups:
                continue
            if seen[n][1] == di and seen[n][2] == k:
                continue
            dorm[k] = {"name": None, "half": False, "t": (e.get("t", 0) or 0) + 1}
            removed.append(n)
    return out, removed


def merge_cw(server_v, incoming_v):
    if not isinstance(server_v, list) or len(server_v) != 3:
        server_v = [{}, {}, {}]
    out = []
    for di in range(3):
        so = server_v[di] if isinstance(server_v[di], dict) else {}
        sn = incoming_v[di] if isinstance(incoming_v[di], dict) else {}
        dorm = {}
        for i in range(1, 17):
            k = str(i)
            dorm[k] = merge_bed(so.get(k), sn.get(k))
        out.append(dorm)
    out, removed = dedupe(out)
    return out, removed


def _norm_cell(c):
    """格子规范化：{"names":[...], "t":ts}；兼容旧纯数组/字符串格式（t=0）"""
    if isinstance(c, list):
        names = [n for n in c if isinstance(n, str) and n.strip()]
        return {"names": names[:3], "t": 0}
    if isinstance(c, str):
        return {"names": [c] if c.strip() else [], "t": 0}
    if not isinstance(c, dict):
        return None
    names = c.get("names")
    t = c.get("t", 0)
    if not isinstance(names, list) or not all(isinstance(n, str) and n.strip() for n in names):
        return None
    if not isinstance(t, (int, float)):
        t = 0
    if len(names) > 3:
        names = names[:3]
    return {"names": names, "t": int(t)}


def _norm_dorm_arr(d):
    """宿舍数组规范化：[5天][格子]"""
    out = []
    for di in range(3):
        dorm = d[di] if isinstance(d[di], list) else []
        days = []
        for day in range(5):
            cell = dorm[day] if isinstance(dorm, list) and day < len(dorm) else None
            nc = _norm_cell(cell)
            days.append(nc if nc else {"names": [], "t": 0})
        out.append(days)
    return out


def _norm_sz(sz):
    out = []
    for di in range(3):
        c = sz[di] if isinstance(sz, list) and di < len(sz) else None
        nc = _norm_cell(c)
        out.append(nc if nc else {"names": [], "t": 0})
    return out


def merge_qj(server_v, incoming_v):
    """清洁轮值表格子级合并：每格 t 大者胜；新改动天然覆盖旧数据"""
    s = _norm_dorm_arr(server_v.get("d")) if isinstance(server_v, dict) and server_v.get("d") else None
    szs = _norm_sz(server_v.get("sz")) if isinstance(server_v, dict) and server_v.get("sz") else None
    if not s:
        base = [{"names": [], "t": 0} for _ in range(5)]
        s = [list(base) for _ in range(3)]

    if not szs:
        szs = [{"names": [], "t": 0} for _ in range(3)]
    inc = _norm_dorm_arr(incoming_v.get("d")) if isinstance(incoming_v, dict) and incoming_v.get("d") else None
    inc_sz = _norm_sz(incoming_v.get("sz")) if isinstance(incoming_v, dict) and incoming_v.get("sz") else None
    if inc:
        for di in range(3):
            for day in range(5):
                a, b = s[di][day], inc[di][day]
                if b["t"] > a["t"]:
                    s[di][day] = b
    if inc_sz:
        for di in range(3):
            a, b = szs[di], inc_sz[di]
            if b["t"] > a["t"]:
                szs[di] = b
    return {"d": s, "sz": szs}


ZW_FILE = os.path.join(BASE, "zuowei_data.json")
GB_FILE = os.path.join(BASE, "ganbu_data.json")


def load_zw():
    try:
        with open(ZW_FILE) as f:
            return json.load(f)
    except Exception:
        return {"v": None, "u": 0}


def save_zw(d):
    tmp = ZW_FILE + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(d, f, ensure_ascii=False)
    os.replace(tmp, ZW_FILE)

def load_gb():
    try:
        with open(GB_FILE) as f:
            d = json.load(f)
        if isinstance(d, dict) and "u" in d:
            return d
    except Exception:
        pass
    return {"v": None, "u": 0}


def save_gb(d):
    tmp = GB_FILE + ".tmp"
    with open(tmp, "w") as f:
        json.dump(d, f, ensure_ascii=False)
    os.replace(tmp, GB_FILE)


class Handler(SimpleHTTPRequestHandler):
    def __init__(self, *args, **kw):
        super().__init__(*args, directory=BASE, **kw)

    def _json(self, code, obj):
        body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
        # 响应 gzip 压缩：数据量增大后整包传输超时是同步失败主因（470KB 压后约 10KB）
        if "gzip" in (self.headers.get("Accept-Encoding") or ""):
            body = _gzip.compress(body)
            self.send_response(code)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Cache-Control", "no-store")
            self.send_header("Content-Encoding", "gzip")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def end_headers(self):
        # 所有响应禁缓存（本服务全是动态数据+需即时更新的页面）
        self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
        super().end_headers()

    def do_GET(self):
        # /api/v1/* → 班务通 v1 只读查询层（AI 生态化：小程序 SKILL / WorkBuddy MCP 共用）
        if self.path.split("?")[0].startswith("/api/v1/"):
            try:
                import api_v1
                from urllib.parse import urlsplit, parse_qs
                u = urlsplit(self.path)
                qs = {k: v[0] for k, v in parse_qs(u.query).items()}
                code, obj = api_v1.handle(u.path, qs, dict(self.headers.items()))
                self._json(code, obj)
            except Exception as e:
                self._json(500, {"ok": False, "error": f"v1 layer error: {e}"})
            return
        if self.path.split("?")[0] == "/api/kaoqin":
            with LOCK:
                data = load_data()
            self._json(200, {"records": data})
            return
        if self.path.split("?")[0] == "/api/chuangwei":
            with LOCK:
                cw = load_cw()
            self._json(200, {"v": cw.get("v"), "u": cw.get("u", 0)})
            return
        if self.path.split("?")[0] == "/api/zuowei":
            with LOCK:
                zw = load_zw()
            self._json(200, {"v": zw.get("v"), "u": zw.get("u", 0)})
            return
        if self.path.split("?")[0] == "/api/qingjie":
            with LOCK:
                qj = load_qj()
            self._json(200, {"v": qj.get("v"), "u": qj.get("u", 0)})
            return
        if self.path.split("?")[0] == "/api/ganbu":
            with LOCK:
                gb = load_gb()
            self._json(200, {"v": gb.get("v"), "u": gb.get("u", 0)})
            return
        super().do_GET()

    def do_POST(self):
        path = self.path.split("?")[0]
        if path not in ("/api/kaoqin", "/api/chuangwei", "/api/qingjie", "/api/zuowei", "/api/ganbu"):
            self._json(404, {"error": "not found"})
            return
        try:
            n = int(self.headers.get("Content-Length") or 0)
            payload = json.loads(self.rfile.read(n).decode("utf-8"))
        except Exception:
            self._json(400, {"error": "bad request"})
            return
        if path == "/api/chuangwei":
            # 床位表共享：按床位合并（非空不覆盖非空，床位级时间戳定胜负），返回合并后全量
            v, u = payload.get("v"), payload.get("u", 0)
            if not isinstance(v, list) or not isinstance(u, (int, float)):
                self._json(400, {"error": "bad request"})
                return
            with LOCK:
                cur = load_cw()
                merged, removed = merge_cw(cur.get("v"), v)
                nu = max(int(cur.get("u", 0) or 0), int(u or 0))
                save_cw({"v": merged, "u": nu})
            self._json(200, {"status": "ok", "v": merged, "u": nu, "removed": removed})
            return
        if path == "/api/ganbu":
            # 班干部分工表共享：整表文档 u 大者胜（后修改的整表胜出），返回权威全量
            v, u = payload.get("v"), payload.get("u", 0)
            if not isinstance(u, (int, float)) or not isinstance(v, dict):
                self._json(400, {"error": "bad request"})
                return

            def valid_gb(x):
                if not isinstance(x, dict) or not isinstance(x.get("title"), str):
                    return False
                if not isinstance(x.get("rows"), list):
                    return False
                for r in x["rows"]:
                    if not isinstance(r, list) or len(r) != 3 or not all(isinstance(c, str) for c in r):
                        return False
                # roster 可选（v3 页面名单内置不随表推送）
                return "roster" not in x or (isinstance(x["roster"], list) and all(isinstance(n, str) for n in x["roster"]))

            with LOCK:
                cur = load_gb()
                cu = int(cur.get("u", 0) or 0)
                nu = int(u)
                import time as _t
                if nu > int(_t.time()*1000) + 60000:
                    nu = int(_t.time()*1000)  # 未来时间戳（时钟错误）钳制为当前
                if nu > cu and valid_gb(v):
                    save_gb({"v": v, "u": nu})
                    merged, mu, status = v, nu, "ok"
                else:
                    merged, mu, status = cur.get("v"), cu, ("rejected" if nu > cu else "stale")
            self._json(200, {"status": status, "v": merged, "u": mu})
            return
        if path == "/api/zuowei":
            # 座位表共享：格子级 t 合并（[9][8] = {names:[0/1个],t}），t 大者胜
            v, u = payload.get("v"), payload.get("u", 0)
            if not isinstance(u, (int, float)) or not isinstance(v, list):
                self._json(400, {"error": "bad request"})
                return

            def norm_cell(c):
                if isinstance(c, dict) and isinstance(c.get("names"), list):
                    ns = [n for n in c["names"] if isinstance(n, str) and n.strip()][:1]
                    t = c.get("t", 0)
                    return {"names": ns, "t": int(t) if isinstance(t, (int, float)) else 0}
                return {"names": [], "t": 0}

            with LOCK:
                cur = load_zw()
                cur_v = cur.get("v")
                if not isinstance(cur_v, list) or len(cur_v) != len(v):
                    merged = [[norm_cell(c) for c in row] for row in v]
                else:
                    merged = []
                    for ri, row in enumerate(v):
                        mrow = []
                        for ci, c in enumerate(row):
                            nc = norm_cell(c)
                            sc = cur_v[ri][ci] if ci < len(cur_v[ri]) else {}
                            sc = norm_cell(sc)
                            mrow.append(nc if nc["t"] > sc["t"] else sc)
                        merged.append(mrow)
                save_zw({"v": merged, "u": int(u)})
            self._json(200, {"status": "ok", "v": merged, "u": int(u)})
            return
        if path == "/api/qingjie":
            # 清洁轮值表共享：整表文档，u 大者胜 + 严格格式校验（拒收旧版页面/异常数据）
            v, u = payload.get("v"), payload.get("u", 0)
            if not isinstance(u, (int, float)):
                self._json(400, {"error": "bad request"})
                return

            def valid_qj(x):
                if not isinstance(x, dict) or not isinstance(x.get("d"), list) or not isinstance(x.get("sz"), list):
                    return False
                if len(x["d"]) != 3 or len(x["sz"]) != 3:
                    return False
                for dorm in x["d"]:
                    if not isinstance(dorm, list) or len(dorm) != 5:
                        return False
                    for day in dorm:
                        if not isinstance(day, list):
                            return False
                        for n in day:
                            if not isinstance(n, str) or not n.strip():
                                return False
                return True

            with LOCK:
                cur = load_qj()
                cur_v = cur.get("v")
                # 兼容旧格式云端：非法则当空处理
                if not isinstance(cur_v, dict) or "d" not in cur_v:
                    cur_v = {"d": None, "sz": None}
                merged = merge_qj(cur_v if isinstance(cur_v, dict) and cur_v.get("d") else {"d": None, "sz": None}, v if isinstance(v, dict) else {})
                save_qj({"v": merged, "u": int(u) if isinstance(u, (int, float)) else 0})
            self._json(200, {"status": "ok", "v": merged, "u": int(u) if isinstance(u, (int, float)) else 0})
            return
        incoming = payload.get("records")
        replace = bool(payload.get("replace"))
        with LOCK:
            cur = {} if replace else load_data()
            merged = merge_records(cur, incoming if isinstance(incoming, dict) else {}, replace)
            save_data(merged)
        self._json(200, {"records": merged})

    def log_message(self, *args):
        pass


if __name__ == "__main__":
    ThreadingHTTPServer(("127.0.0.1", 8081), Handler).serve_forever()
