#!/usr/bin/env python3
"""keychat agent CLI — 与网页端互通的端到端加密聊天室命令行。

用法:
  agent.py read <口令> [--since N] [--json]     读取并解密房间消息
  agent.py send <口令> --text "内容" [--from 名字]  加密并发送一条消息
  agent.py burn <口令> --yes                     焚毁整个房间（删除服务器上的密文）

  口令也可以用环境变量 KEYCHAT_PASS 传（避免进 shell 历史）:
      KEYCHAT_PASS=xxx agent.py read -
  服务地址默认 https://miyu.hirkond.online，可用 --url 覆盖。

依赖: pip install cryptography   （协议详情见 <url>/api/help）
"""
import argparse
import base64
import hashlib
import json
import os
import sys
import time
import urllib.request

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

DEFAULT_URL = "https://miyu.hirkond.online"


def derive(passphrase: str) -> tuple[str, bytes, str]:
    rid = hashlib.sha256(("keychat-room-v1:" + passphrase).encode()).hexdigest()[:32]
    key = hashlib.pbkdf2_hmac("sha256", passphrase.encode(), b"keychat-key-v1", 310_000, 32)
    fp = hashlib.sha256(b"keychat-fp-v1" + key).hexdigest()[:8]
    return rid, key, fp


def http(url: str, payload: dict | None = None) -> dict:
    req = urllib.request.Request(
        url,
        data=json.dumps(payload).encode() if payload is not None else None,
        headers={"Content-Type": "application/json"},
        method="POST" if payload is not None else "GET",
    )
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.load(resp)


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("cmd", choices=["read", "send", "burn"])
    ap.add_argument("passphrase", help="共享口令；传 - 则读环境变量 KEYCHAT_PASS")
    ap.add_argument("--text", help="send: 要发送的内容")
    ap.add_argument("--from", dest="sender", default="Agent", help="send: 显示名（默认 Agent）")
    ap.add_argument("--since", type=int, default=0, help="read: 只取 seq 大于此值的消息")
    ap.add_argument("--json", action="store_true", help="read: 按 JSON 行输出（方便程序处理）")
    ap.add_argument("--yes", action="store_true", help="burn: 确认删除")
    ap.add_argument("--url", default=DEFAULT_URL)
    args = ap.parse_args()

    passphrase = os.environ.get("KEYCHAT_PASS", "") if args.passphrase == "-" else args.passphrase
    if not passphrase:
        sys.exit("错误: 口令为空（用位置参数传，或 KEYCHAT_PASS 环境变量 + 位置参数 -）")

    rid, key, fp = derive(passphrase)
    base = args.url.rstrip("/")
    aes = AESGCM(key)

    if args.cmd == "send":
        if not args.text:
            sys.exit("错误: send 需要 --text")
        iv = os.urandom(12)
        ct = aes.encrypt(iv, json.dumps(
            {"from": args.sender, "text": args.text}, ensure_ascii=False).encode(), None)
        out = http(f"{base}/api/rooms/{rid}/messages",
                   {"iv": base64.b64encode(iv).decode(), "ct": base64.b64encode(ct).decode()})
        print(f"已发送 seq={out['seq']} 房间指纹={fp}")
        return

    if args.cmd == "burn":
        if not args.yes:
            sys.exit("burn 会删除服务器上这个房间的全部密文，确认请加 --yes")
        http(f"{base}/api/rooms/{rid}/burn", {})
        print(f"房间已焚毁（指纹 {fp}）")
        return

    data = http(f"{base}/api/rooms/{rid}/messages?since={args.since}")
    if not data["messages"]:
        print(f"(房间为空或无新消息，指纹 {fp}, last={data['last']})", file=sys.stderr)
    for m in data["messages"]:
        try:
            pt = json.loads(aes.decrypt(
                base64.b64decode(m["iv"]), base64.b64decode(m["ct"]), None))
        except Exception:
            pt = {"from": "?", "text": "<无法解密：密文损坏或口令不符>"}
        if args.json:
            print(json.dumps({"seq": m["seq"], "ts": m["ts"], **pt}, ensure_ascii=False))
        else:
            stamp = time.strftime("%m-%d %H:%M", time.localtime(m["ts"]))
            print(f"[{m['seq']:>4} {stamp}] {pt.get('from', '?')}: {pt.get('text', '')}")


if __name__ == "__main__":
    main()
