"""Command-line interface.

Three sub-commands cover the common operator workflows:

- ``ana listen`` — subscribe and pretty-print envelopes (for ops / debugging)
- ``ana send`` — publish a query / reply / ack / discovery from a shell script
- ``ana probe`` — send a discovery query to a peer and wait for the reply

Each command takes ``--nats``, ``--prefix``, and ``--identity`` so you can drop
this into any environment without writing Python.
"""
from __future__ import annotations

import argparse
import asyncio
import json
import os
import sys
from typing import Any

from ana.client import AgentBus, InboundContext
from ana.envelope import Envelope, Query
from ana.subjects import SubjectScheme


def _make_bus(args: argparse.Namespace) -> AgentBus:
    return AgentBus(
        identity=args.identity,
        nats_url=args.nats,
        scheme=SubjectScheme(prefix=args.prefix),
        audit_log_path=args.audit_log,
    )


async def _cmd_listen(args: argparse.Namespace) -> None:
    bus = _make_bus(args)
    await bus.connect()

    scope_subject = (
        bus.scheme.all_under_prefix() if args.scope == "all" else bus.scheme.all_for_agent(bus.identity)
    )

    out_fh = None
    if args.output:
        out_path = os.path.expanduser(args.output)
        os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
        out_fh = open(out_path, "a", buffering=1, encoding="utf-8")  # noqa: SIM115 — handle lives for entire daemon lifetime

    async def _handle(env: Envelope, ctx: InboundContext) -> None:
        # Skip own publishes that the wildcard sub echoes back
        if env.from_ == bus.identity:
            return

        record: dict[str, Any] = {
            "subject": ctx.subject,
            "envelope": env.model_dump(by_alias=True, exclude_none=True),
        }
        line = json.dumps(record, ensure_ascii=False)
        if out_fh is not None:
            out_fh.write(line + "\n")
        else:
            print(line, flush=True)

        if args.auto_ack and isinstance(env, Query) and env.to == bus.identity:
            parsed = bus.scheme.parse(ctx.subject)
            topic = parsed.topic or "default"
            await bus.ack(ack_for=ctx.subject, topic=topic, note="ana-listener daemon alive")

    await bus.subscribe(scope_subject, _handle)
    print(
        f"[ana] listening on {scope_subject}"
        + (f" -> {args.output}" if args.output else "")
        + (" (auto-ack on)" if args.auto_ack else "")
        + " (Ctrl-C to stop)",
        file=sys.stderr,
        flush=True,
    )
    try:
        await asyncio.Event().wait()
    finally:
        await bus.close()
        if out_fh is not None:
            out_fh.close()


async def _cmd_send(args: argparse.Namespace) -> None:
    bus = _make_bus(args)
    await bus.connect()
    try:
        if args.kind == "query":
            subj = await bus.query(
                to=args.to,
                query=args.query,
                topic=args.topic,
                fields=args.fields,
            )
        elif args.kind == "reply":
            data = json.loads(args.data) if args.data else None
            subj = await bus.reply(
                reply_for=args.reply_for,
                data=data,
                topic=args.topic,
                note=args.note,
            )
        elif args.kind == "ack":
            subj = await bus.ack(ack_for=args.ack_for, topic=args.topic, note=args.note)
        elif args.kind == "discovery":
            subj = await bus.announce_discovery(
                role=args.role,
                cluster=args.cluster,
                subjects_owned=args.subjects_owned,
            )
        else:
            raise ValueError(f"unknown send kind: {args.kind!r}")
        print(f"[ana] sent {args.kind} on {subj}", file=sys.stderr)
    finally:
        await bus.close()


async def _cmd_probe(args: argparse.Namespace) -> None:
    bus = _make_bus(args)
    await bus.connect()
    try:
        reply = await bus.query_and_wait(
            args.target,
            args.query,
            topic=args.topic,
            timeout_s=args.timeout,
            accept_ack=not args.no_ack,
        )
        if reply is None:
            print(
                json.dumps({"ok": False, "reason": "timeout", "target": args.target}),
                file=sys.stdout,
            )
            sys.exit(2)
        print(
            json.dumps(
                {
                    "ok": True,
                    "target": args.target,
                    "reply": reply.model_dump(by_alias=True, exclude_none=True),
                },
                ensure_ascii=False,
            )
        )
    finally:
        await bus.close()


def _build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="ana", description="agent-to-agent NATS bus CLI")
    p.add_argument("--nats", default="nats://127.0.0.1:4222", help="NATS server URL")
    p.add_argument("--prefix", default="cc.fleet", help="Subject prefix")
    p.add_argument("--identity", required=True, help="This agent's identity (e.g. miraku-home)")
    p.add_argument("--audit-log", default=None, help="Path to write per-publish audit JSONL")

    sub = p.add_subparsers(dest="cmd", required=True)

    listen_p = sub.add_parser("listen", help="Subscribe and pretty-print envelopes")
    listen_p.add_argument(
        "--scope",
        choices=["all", "self"],
        default="self",
        help="all = <prefix>.> ; self = <prefix>.<identity>.>",
    )
    listen_p.add_argument(
        "--output",
        default=None,
        help="Write JSONL envelopes to this file instead of stdout (for daemon use)",
    )
    listen_p.add_argument(
        "--auto-ack",
        action="store_true",
        help="Auto-publish an ack for every inbound query addressed to this identity (liveness signal)",
    )

    s = sub.add_parser("send", help="Publish one envelope and exit")
    s.add_argument("kind", choices=["query", "reply", "ack", "discovery"])
    s.add_argument("--to", help="Target agent (query)")
    s.add_argument("--query", help="Query string (query)")
    s.add_argument("--fields", nargs="*", help="Requested fields (query)")
    s.add_argument("--reply-for", help="Subject the reply is for (reply)")
    s.add_argument("--data", help="JSON-encoded reply data (reply)")
    s.add_argument("--ack-for", help="Subject the ack is for (ack)")
    s.add_argument("--note", help="Optional human-readable note")
    s.add_argument("--role", help="Role identifier (discovery)")
    s.add_argument("--cluster", help="Cluster name (discovery)")
    s.add_argument("--subjects-owned", nargs="*", help="Subjects this agent owns (discovery)")
    s.add_argument("--topic", default="default", help="Topic suffix on the subject")

    pr = sub.add_parser("probe", help="Send a discovery-style query and wait for reply or ack")
    pr.add_argument("target", help="Target agent")
    pr.add_argument("--query", default="status", help="Query string")
    pr.add_argument("--topic", default="default", help="Topic suffix on the subject")
    pr.add_argument("--timeout", type=float, default=10.0, help="Reply timeout (seconds)")
    pr.add_argument(
        "--no-ack",
        action="store_true",
        help="Wait for a full Reply only — don't accept an Ack as success",
    )

    return p


def main(argv: list[str] | None = None) -> None:
    parser = _build_parser()
    args = parser.parse_args(argv)

    if args.cmd == "listen":
        asyncio.run(_cmd_listen(args))
    elif args.cmd == "send":
        asyncio.run(_cmd_send(args))
    elif args.cmd == "probe":
        asyncio.run(_cmd_probe(args))
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()
