"""議事録の自動送信パイプラインのうち、手元で確かめられる部分を検証する。
実際のZoom/Teams/Meet/Notion/Driveには接続しない（アカウントとプランが要るため）。
1) Teams形式VTT -> テキスト変換  2) Notion APIの公式制限を再現したモックでブロック数を数える
3) Zoom Webhookの受け口（CRC応答と署名検証、3秒ルール）  4) aachat Webhookのtext上限(100,000 bytes)に収まるか
"""
import hashlib, hmac, json, re, sys, threading, time, urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer

T = open("sample-transcript.txt", encoding="utf-8").read()
utts = [l for l in T.splitlines() if l.strip()]

# ---- 1) Teams形式VTTを作って、読み戻す（形式はMicrosoft Graph公式docsの例に合わせた）
def to_vtt(lines):
    out, t = ["WEBVTT", ""], 0.0
    for l in lines:
        who, _, text = l.partition(": ")
        dur = max(1.5, len(text) / 6.0)  # 1秒6文字の仮ペース
        f = lambda s: f"{int(s//3600):02d}:{int(s%3600//60):02d}:{s%60:06.3f}"
        out += [f"{f(t)} --> {f(t+dur)}", f"<v {who}>{text}</v>", ""]
        t += dur
    return "\n".join(out)

def vtt_to_utts(vtt):
    return [f"{m.group(1)}: {m.group(2)}" for m in re.finditer(r"<v ([^>]+)>(.*?)</v>", vtt)]

vtt = to_vtt(utts)
back = vtt_to_utts(vtt)
assert back == utts, "VTT往復で内容が変わった"
print(f"[1] 発言{len(utts)}件 / 文字数{len(T)} / UTF-8 {len(T.encode())} bytes / VTT往復OK")

# ---- 2) Notionの公式制限（request-limits）を再現したモック
LIMIT_TEXT, LIMIT_CHILDREN, LIMIT_BLOCKS, LIMIT_BYTES, FREE_LIFETIME = 2000, 100, 1000, 500_000, 1000
def notion_validate(children):
    if len(children) > LIMIT_CHILDREN: return 400, "children > 100"
    if len(json.dumps(children, ensure_ascii=False).encode()) > LIMIT_BYTES: return 400, "payload > 500KB"
    for b in children:
        for rt in b["paragraph"]["rich_text"]:
            if len(rt["text"]["content"]) > LIMIT_TEXT: return 400, "rich_text > 2000"
    return 200, "ok"

def para(s): return {"object": "block", "type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": s}}]}}
def split(s, n=LIMIT_TEXT):
    out = []
    while len(s) > n:
        cut = s.rfind("。", 0, n); cut = cut + 1 if cut > 0 else n
        out.append(s[:cut]); s = s[cut:]
    return out + [s]

def plan(name, texts):
    blocks = [para(p) for t in texts for p in split(t)]
    reqs = [blocks[i:i+LIMIT_CHILDREN] for i in range(0, len(blocks), LIMIT_CHILDREN)]
    assert all(notion_validate(r)[0] == 200 for r in reqs)
    print(f"[2] {name}: {len(blocks)}ブロック / {len(reqs)}リクエスト / 無料1,000ブロックで {FREE_LIFETIME // len(blocks)} 会議分")
    return len(blocks)

plan("1発言1ブロック（全文）", utts)
plan("2000字ごとにまとめて全文", ["\n".join(utts)])
plan("要約だけ（見出し3+箇条書き10の想定）", ["決定事項"] + ["項目"] * 10 + ["ToDo"] + ["項目"] * 8)
# 違反の再現
print("[2] 違反例 2001文字 ->", notion_validate([para("あ" * 2001)]))
print("[2] 違反例 101ブロック ->", notion_validate([para("あ")] * 101))

# ---- 3) Zoom Webhook受け口
SECRET = "test-secret-token"
class H(BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def do_POST(self):
        body = self.rfile.read(int(self.headers["Content-Length"])).decode()
        ev = json.loads(body)
        if ev.get("event") == "endpoint.url_validation":
            p = ev["payload"]["plainToken"]
            res = {"plainToken": p, "encryptedToken": hmac.new(SECRET.encode(), p.encode(), hashlib.sha256).hexdigest()}
        else:
            ts = self.headers.get("x-zm-request-timestamp", "")
            want = "v0=" + hmac.new(SECRET.encode(), f"v0:{ts}:{body}".encode(), hashlib.sha256).hexdigest()
            if not hmac.compare_digest(want, self.headers.get("x-zm-signature", "")):
                self.send_response(401); self.end_headers(); return
            res = {"received": ev["event"]}
        b = json.dumps(res).encode()
        self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers(); self.wfile.write(b)

srv = HTTPServer(("127.0.0.1", 0), H); port = srv.server_port
threading.Thread(target=srv.serve_forever, daemon=True).start()
def post(body, headers=None):
    r = urllib.request.Request(f"http://127.0.0.1:{port}/", data=body.encode(), headers=headers or {}, method="POST")
    t0 = time.time()
    try:
        with urllib.request.urlopen(r) as x: return x.status, x.read().decode(), time.time() - t0
    except urllib.error.HTTPError as e: return e.code, "", time.time() - t0
s, b, dt = post(json.dumps({"event": "endpoint.url_validation", "payload": {"plainToken": "abc123"}}))
expect = hmac.new(SECRET.encode(), b"abc123", hashlib.sha256).hexdigest()
print(f"[3] CRC応答 status={s} {dt*1000:.1f}ms encryptedToken一致={json.loads(b)['encryptedToken']==expect}")
body = json.dumps({"event": "meeting.summary_completed", "payload": {"object": {"summary_content": "# 要約"}}}, ensure_ascii=False)
ts = "1760000000"
sig = "v0=" + hmac.new(SECRET.encode(), f"v0:{ts}:{body}".encode(), hashlib.sha256).hexdigest()
print("[3] 正しい署名 ->", post(body, {"x-zm-request-timestamp": ts, "x-zm-signature": sig})[0])
print("[3] 本文を1文字改ざん ->", post(body.replace("要約", "要紙"), {"x-zm-request-timestamp": ts, "x-zm-signature": sig})[0])
print("[3] 署名ヘッダー無し ->", post(body, {"x-zm-request-timestamp": ts})[0])
# 受け取ったJSONを読み込んで再整形（空白を変える）してから署名を計算すると合わない
reser = json.dumps(json.loads(body), ensure_ascii=False, separators=(",", ":"))
print("[3] 受信本文と再整形後が同じバイト列か ->", reser == body)
print("[3] 再整形した本文で署名検証 ->", post(reser, {"x-zm-request-timestamp": ts, "x-zm-signature": sig})[0], "（署名は元の本文で計算）")

# ---- 4) aachat Webhook text上限（100,000 bytes）
n = len(T.encode())
print(f"[4] このサンプル {n} bytes -> 上限100,000の {n/1000:.1f}%。上限に達する文字数の目安 {100000//3}字(日本語1字3bytes換算)")
