#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
blind-votes-consumer.py — 迭代平台决策账本消费器 v0 (2026-08-27)

职责: 尾扫所有 episode 工作区的 .pipeline-assets/blind-votes.jsonl,
     把上次游标之后的新决策事件格式化输出到 stdout。
交付形态: 配合 Hermes cronjob(no_agent=True) 使用 —— stdout 非空即送达 Telegram,
         空输出保持静默(经典 watchdog 语义);也可交互单独跑做验收。

游标状态存 ~/.cache/blind-votes-cursor.json(按 文件inode+size 追踪;
     inode 变化视为轮转/重建,从尾重新起步并最多回读 5 行防丢首事件)。

用法:
    python3 blind-votes-consumer.py            # 正常消费
    python3 blind-votes-consumer.py --reset    # 清游标全量重放(调试用)
    python3 blind-votes-consumer.py --peek     # 只看不消费(不动游标)
"""
import json
import os
import sys
from pathlib import Path

EPISODES_ROOTS = [
    Path(os.environ.get("KMC_EPISODES_ROOT",
                        "/data/workspace/kais-hermes-skills/skills/kais-movie-pipeline/episodes")),
    Path("/data/workspace/kais-hermes-skills/skills/kais-movie-pipeline/runs"),
]
CURSOR_FILE = Path(os.path.expanduser("~/.cache/blind-votes-cursor.json"))
MAX_BACKREAD_ON_ROTATE = 5

TRACK_LABEL = {
    "human_blind": "👤人工盲选",
    "gold_auto": "🥇金标自动",
    "gold_gated": "🥇金标待确认",
}


def load_cursor() -> dict:
    try:
        return json.loads(CURSOR_FILE.read_text(encoding="utf-8"))
    except Exception:
        return {}


def save_cursor(cur: dict) -> None:
    CURSOR_FILE.parent.mkdir(parents=True, exist_ok=True)
    tmp = CURSOR_FILE.with_suffix(".tmp")
    tmp.write_text(json.dumps(cur, ensure_ascii=False), encoding="utf-8")
    os.replace(tmp, CURSOR_FILE)


def find_ledgers() -> list[Path]:
    out = []
    seen = set()
    for root in EPISODES_ROOTS:
        if not root.is_dir():
            continue
        for ledger in root.glob("*/.pipeline-assets/blind-votes.jsonl"):
            rp = ledger.resolve()
            if rp not in seen:
                seen.add(rp)
                out.append(rp)
    return sorted(out)


def fmt_event(ev: dict, ep_name: str) -> str:
    track = ev.get("track", "?")
    sel = ev.get("selector", {}) or {}
    tags = sel.get("reason_tags") or []
    tag_s = f" 标签[{','.join(tags[:4])}]" if tags else ""
    note = sel.get("operator_note")
    note_s = f"\n   📝 {note}" if note else ""
    status = " ↩️已翻案撤销" if ev.get("revoked") else ""
    blind = "蒙眼直选" if ev.get("was_blind") else "揭晓后改判"
    std = sel.get("standard_ref")
    std_s = f" · 参照 {std}" if std else ""
    return (
        f"🗳 {TRACK_LABEL.get(track, track)} · {ep_name} · {ev.get('group_key', '?')}\n"
        f"   胜者 {ev.get('winner_node_id', '?')}({blind}){tag_s}{std_s}{status}"
        f"{note_s}\n"
        f"   {ev.get('recorded_at', '')} · 会话 {ev.get('session_id', '-')}"
    )


def consume(write_cursor: bool) -> list[str]:
    cur = load_cursor()  # 无论何种模式都以已存游标为基线;peek 只是最后不落盘
    lines_out = []
    new_cur = {}
    for ledger in find_ledgers():
        key = str(ledger)
        ep_name = ledger.parent.parent.name
        try:
            st = ledger.stat()
            inode_key = f"{st.st_ino}:{key}"
            prev = cur.get(inode_key, {})
            prev_size = prev.get("size", 0)
            offset = prev_size if st.st_size >= prev_size else max(
                0, st.st_size - MAX_BACKREAD_ON_ROTATE * 512)
            if st.st_size == offset:
                new_cur[inode_key] = {"size": st.st_size}
                continue
            with ledger.open("r", encoding="utf-8") as fh:
                fh.seek(offset)
                fresh = fh.read()
            for raw in fresh.splitlines():
                raw = raw.strip()
                if not raw:
                    continue
                try:
                    ev = json.loads(raw)
                except json.JSONDecodeError:
                    lines_out.append(f"⚠️ {ep_name}: 一行账本无法解析(可能写入中断): {raw[:80]}…")
                    continue
                lines_out.append(fmt_event(ev, ep_name))
            new_cur[inode_key] = {"size": st.st_size}
        except OSError as e:
            lines_out.append(f"⚠️ 读账本失败 {ep_name}: {e}")
    merged = {}
    if write_cursor:
        merged.update(load_cursor())
    merged.update(new_cur)
    if write_cursor and merged != cur:
        save_cursor(merged)
    return lines_out


def main() -> int:
    args = sys.argv[1:]
    peek = "--peek" in args
    if "--reset" in args:
        CURSOR_FILE.unlink(missing_ok=True)
        print("(游标已清空,将全量重放)", file=sys.stderr)
    events = consume(write_cursor=not peek)
    if not events:
        return 0  # 静默: 无新决策
    print(f"📈 迭代平台决策账本 · {len(events)} 条新记录\n")
    print("\n\n".join(events))
    return 0


if __name__ == "__main__":
    sys.exit(main())
