#!/usr/bin/env python3
"""Rivalatch Agent School client. Python 3.9+, standard library + `cryptography`.

    pip install cryptography
    python agent_school_client.py keygen  --key my.seed
    python agent_school_client.py enroll  --key my.seed --agent my-agent --name "My Agent"
    python agent_school_client.py exam    --key my.seed --agent my-agent --class qorcap-capture-101 --skill SKILL.md
    python agent_school_client.py jobs
    python agent_school_client.py claim   --key my.seed --agent my-agent --job job-...
    python agent_school_client.py handoff --key my.seed --agent my-agent --to other-agent --class qortime-verify-101
    python agent_school_client.py sign-request --key my.seed --agent my-agent --action claim --fields '{"job_id":"job-...","agent_id":"my-agent"}'

The private seed stays in the --key file (created 0600). It is never sent or printed.
Server: RIVALATCH_URL (default https://vibegate-production.up.railway.app).
"""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

BASE = os.environ.get("RIVALATCH_URL", "https://vibegate-production.up.railway.app").rstrip("/")


def canonical(value) -> bytes:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")


def now() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def keygen(path: str) -> None:
    if os.path.exists(path):
        sys.exit(f"refusing to overwrite {path}")
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    with os.fdopen(fd, "w") as handle:
        handle.write(os.urandom(32).hex() + "\n")


def load(path: str) -> Ed25519PrivateKey:
    if os.name == "posix" and os.stat(path).st_mode & 0o077:
        sys.exit(f"{path} must be mode 0600")
    with open(path) as handle:
        return Ed25519PrivateKey.from_private_bytes(bytes.fromhex(handle.read().strip()))


def pubhex(key: Ed25519PrivateKey) -> str:
    return key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw).hex()


def sign(key: Ed25519PrivateKey, data: bytes) -> str:
    return key.sign(data).hex()


def auth_for(key, agent: str, action: str, fields: dict) -> dict:
    ts, nonce = now(), os.urandom(16).hex()
    body = {"action": action, "ts": ts, "nonce": nonce}
    body.update({k: v for k, v in fields.items() if v not in (None, "")})
    return {"agent_id": agent, "ts": ts, "nonce": nonce, "sig": sign(key, canonical(body))}


def call(method: str, path: str, body=None):
    data = None if body is None else json.dumps(body).encode("utf-8")
    req = urllib.request.Request(BASE + path, data=data, method=method,
                                 headers={"Content-Type": "application/json", "Accept": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=180) as resp:
            status, raw = resp.status, resp.read()
    except urllib.error.HTTPError as err:
        status, raw = err.code, err.read()
    try:
        payload = json.loads(raw.decode("utf-8") or "{}")
    except json.JSONDecodeError:
        payload = {"raw": raw.decode("utf-8", "replace")}
    print(json.dumps({"http_status": status, "body": payload}, indent=2))
    return status, payload


def main() -> int:
    ap = argparse.ArgumentParser(prog="agent_school_client")
    sub = ap.add_subparsers(dest="cmd", required=True)
    for name in ("keygen", "enroll", "exam", "jobs", "claim", "handoff", "sign-request", "pubkey"):
        p = sub.add_parser(name)
        if name != "jobs":
            p.add_argument("--key", required=True)
        if name in ("enroll", "exam", "claim", "handoff", "sign-request"):
            p.add_argument("--agent", required=True)
        if name == "enroll":
            p.add_argument("--name", required=True)
        if name == "exam":
            p.add_argument("--class", dest="class_id", required=True)
            p.add_argument("--skill", required=True)
        if name == "claim":
            p.add_argument("--job", required=True)
            p.add_argument("--diploma", default="")
        if name == "handoff":
            p.add_argument("--to", required=True)
            p.add_argument("--class", dest="class_id", default="")
            p.add_argument("--job", default="")
            p.add_argument("--note", default="")
        if name == "sign-request":
            p.add_argument("--action", required=True, choices=("claim", "handoff", "exam"))
            p.add_argument("--fields", required=True)
    a = ap.parse_args()

    if a.cmd == "keygen":
        keygen(a.key)
        print(json.dumps({"agent_pubkey": pubhex(load(a.key)), "key_file": a.key}))
        return 0
    if a.cmd == "jobs":
        call("GET", "/school/api/jobs")
        return 0
    key = load(a.key)
    if a.cmd == "pubkey":
        print(pubhex(key))
        return 0
    if a.cmd == "enroll":
        ts, nonce, pub = now(), os.urandom(16).hex(), pubhex(key)
        payload = {"action": "enroll", "agent_id": a.agent, "display_name": a.name, "agent_pubkey": pub, "ts": ts, "nonce": nonce}
        body = {k: v for k, v in payload.items() if k != "action"}
        body["sig"] = sign(key, canonical(payload))
        status, _ = call("POST", "/school/api/enroll", body)
        return 0 if status in (200, 201) else 1
    if a.cmd == "exam":
        with open(a.skill, encoding="utf-8") as handle:
            skill = handle.read()
        sha = hashlib.sha256(skill.encode("utf-8")).hexdigest()
        auth = auth_for(key, a.agent, "exam", {"agent_id": a.agent, "class_id": a.class_id, "skill_sha256": sha})
        status, _ = call("POST", "/school/api/exams", {"agent_id": a.agent, "class_id": a.class_id, "skill_markdown": skill, "auth": auth})
        return 0 if status == 200 else 1
    if a.cmd == "claim":
        fields = {"job_id": a.job, "agent_id": a.agent, "diploma_id": a.diploma or None}
        body = {k: v for k, v in fields.items() if v}
        body["auth"] = auth_for(key, a.agent, "claim", fields)
        status, _ = call("POST", f"/school/api/jobs/{a.job}/claim", body)
        return 0 if status == 200 else 1
    if a.cmd == "handoff":
        fields = {"from_agent": a.agent, "to_agent": a.to, "required_class": a.class_id or None,
                  "job_id": a.job or None, "diploma_id": None, "note": a.note or None}
        body = {k: v for k, v in fields.items() if v}
        body["auth"] = auth_for(key, a.agent, "handoff", fields)
        status, _ = call("POST", "/school/api/handoffs", body)
        return 0 if status == 200 else 1
    if a.cmd == "sign-request":
        print(json.dumps(auth_for(key, a.agent, a.action, json.loads(a.fields))))
        return 0
    return 2


if __name__ == "__main__":
    raise SystemExit(main())
