#!/usr/bin/env python3
"""Small CLI used by the persona and owner on the Singapore server."""

from __future__ import annotations

import argparse
import json
import os
import sys
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen


def request(
    base: str, token: str, method: str, path: str, body: dict[str, Any] | None = None
) -> dict[str, Any]:
    data = None if body is None else json.dumps(body).encode("utf-8")
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
    if data is not None:
        headers["Content-Type"] = "application/json"
    req = Request(base.rstrip("/") + path, data=data, headers=headers, method=method)
    try:
        with urlopen(req, timeout=30) as response:
            return json.load(response)
    except HTTPError as exc:
        print(exc.read().decode("utf-8", errors="replace"), file=sys.stderr)
        raise


def token_for(role: str) -> str:
    name = {
        "admin": "CANGER_ADMIN_TOKEN",
        "owner": "CANGER_OWNER_TOKEN",
        "persona": "CANGER_PERSONA_TOKEN",
        "operator": "CANGER_OPERATOR_TOKEN",
    }[role]
    value = os.environ.get(name, "")
    if not value:
        raise RuntimeError(f"{name} is not set")
    return value


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--url", default=os.environ.get("CANGER_CONTROL_URL", "http://127.0.0.1:8787")
    )
    sub = parser.add_subparsers(dest="command", required=True)
    sub.add_parser("pair-code")
    sub.add_parser("nodes")
    task = sub.add_parser("task")
    task.add_argument("--node", required=True)
    task.add_argument("--action", required=True)
    task.add_argument("--args", default="{}")
    approve = sub.add_parser("approve")
    approve.add_argument("task_id")
    reject = sub.add_parser("reject")
    reject.add_argument("task_id")
    show = sub.add_parser("show")
    show.add_argument("task_id")
    watch = sub.add_parser("watch")
    watch.add_argument("task_id")
    grant = sub.add_parser("grant")
    grant.add_argument("--node", required=True)
    grant.add_argument("--hours", type=int, default=4)
    grant.add_argument("--reason", default="苍耳人格体编程、调试与诊断")
    sub.add_parser("grants")
    revoke = sub.add_parser("revoke")
    revoke.add_argument("grant_id")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    if args.command == "pair-code":
        result = request(args.url, token_for("admin"), "POST", "/v1/pairings", {})
    elif args.command == "nodes":
        result = request(args.url, token_for("operator"), "GET", "/v1/nodes")
    elif args.command == "task":
        result = request(
            args.url,
            token_for("operator"),
            "POST",
            "/v1/tasks",
            {
                "node_id": args.node,
                "action": args.action,
                "args": json.loads(args.args),
            },
        )
    elif args.command == "approve":
        result = request(
            args.url,
            token_for("owner"),
            "POST",
            f"/v1/tasks/{args.task_id}/approve",
            {},
        )
    elif args.command == "reject":
        result = request(
            args.url,
            token_for("owner"),
            "POST",
            f"/v1/tasks/{args.task_id}/reject",
            {},
        )
    elif args.command == "show":
        result = request(
            args.url,
            token_for("operator"),
            "GET",
            f"/v1/tasks/{args.task_id}",
        )
    elif args.command == "watch":
        after = 0
        while True:
            events = request(
                args.url,
                token_for("operator"),
                "GET",
                f"/v1/tasks/{args.task_id}/events?after={after}",
            )
            for event in events["events"]:
                after = max(after, event["seq"])
                print(
                    f"[{event['seq']}] {event['kind']}: "
                    + json.dumps(event["payload"], ensure_ascii=False),
                    flush=True,
                )
            state = request(
                args.url,
                token_for("operator"),
                "GET",
                f"/v1/tasks/{args.task_id}",
            )
            if state["status"] in {"completed", "rejected"}:
                result = state
                break
            time.sleep(1)
    elif args.command == "grant":
        result = request(
            args.url,
            token_for("operator"),
            "POST",
            "/v1/grants",
            {
                "node_id": args.node,
                "duration_seconds": args.hours * 3600,
                "reason": args.reason,
            },
        )
    elif args.command == "grants":
        result = request(args.url, token_for("operator"), "GET", "/v1/grants")
    else:
        result = request(
            args.url,
            token_for("owner"),
            "POST",
            f"/v1/grants/{args.grant_id}/revoke",
            {},
        )
    print(json.dumps(result, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
