#!/usr/bin/env python3
"""Backend for `ncp` subcommands (chat/memory/playbook/history).

Not a second CLI — `ncp` is the only entrypoint. This just hits the same REST
API the web UI uses, so the terminal matches the general features. httpx and
argparse only (both already present); no curses, no framework.
"""
import argparse
import json
import sys

import httpx

from synapse.nexus_config import settings

BASE = settings.api_url


def _client():
    return httpx.Client(base_url=BASE, timeout=None)


def _die_if_down(exc: Exception):
    if isinstance(exc, (httpx.ConnectError, httpx.ConnectTimeout)):
        sys.exit(f"Backend not reachable at {BASE}. Start it: ncp start -b")
    raise exc


def iter_chunks(lines):
    """Yield ('kind', payload) from raw SSE lines. kind is 'chunk' for reply
    text, else the event name ('meta'/'done'/'error'/'title'/...). Pure so the
    stream parsing is unit-testable without a live server (see test_nexus_api.py)."""
    event = "message"
    for line in lines:
        if line == "":            # blank line ends an event block
            event = "message"
            continue
        if line.startswith("event:"):
            event = line[6:].strip()
        elif line.startswith("data:"):
            data = line[5:].strip()
            if event in ("message", ""):
                yield "chunk", json.loads(data)   # server json-encodes each token
            else:
                yield event, data


def cmd_chat(args):
    body = {"message": " ".join(args.message)}
    if args.model:
        body["model"] = args.model
    try:
        with _client() as c, c.stream("POST", "/chat/stream", json=body) as r:
            r.raise_for_status()
            for kind, payload in iter_chunks(r.iter_lines()):
                if kind == "chunk":
                    sys.stdout.write(payload)
                    sys.stdout.flush()
                elif kind == "escalating":
                    sys.stderr.write("\n[escalating to Claude…]\n")
                elif kind == "error":
                    sys.exit("\n" + json.loads(payload).get("detail", "chat failed"))
                elif kind == "done":
                    break
        print()
    except Exception as e:
        _die_if_down(e)


def cmd_memory(args):
    try:
        with _client() as c:
            if args.action == "list":
                items = c.get("/memory").json()["items"]
                if not items:
                    print("No memory facts.")
                    return
                section = None
                for m in items:
                    if m["section"] != section:
                        section = m["section"]
                        print(f"\n## {section}")
                    print(f"  {m['id'][:8]}  {m['text']}")
            elif args.action == "add":
                m = c.post("/memory", json={"text": " ".join(args.rest),
                                            "section": args.section}).json()
                print(f"added {m['id'][:8]} to {m['section']}")
            elif args.action == "rm":
                c.delete(f"/memory/{args.rest[0]}").raise_for_status()
                print("deleted")
    except Exception as e:
        _die_if_down(e)


def cmd_playbook(args):
    try:
        with _client() as c:
            if args.action == "list":
                pbs = c.get("/playbooks").json()["playbooks"]
                for i, p in enumerate(pbs):
                    mark = "* " if i == 0 else "  "   # first = active system prompt
                    tags = f"  [{', '.join(p['tags'])}]" if p["tags"] else ""
                    print(f"{mark}{p['id'][:8]}  {p['title']}{tags}")
            elif args.action == "show":
                p = c.get(f"/playbooks/{args.rest[0]}").json()
                print(f"# {p['title']}\n\nGoal: {p['goal']}\n\n{p['instructions']}")
    except Exception as e:
        _die_if_down(e)


def cmd_history(args):
    try:
        with _client() as c:
            params = {"q": args.query} if args.query else {}
            convs = c.get("/conversations", params=params).json()["conversations"]
            for cv in convs[:args.limit]:
                title = cv.get("title") or cv.get("preview") or "(untitled)"
                print(f"  {cv['id'][:8]}  {title}")
    except Exception as e:
        _die_if_down(e)


def main(argv=None, prog="nexus"):
    p = argparse.ArgumentParser(prog=prog)
    sub = p.add_subparsers(dest="cmd", required=True)

    c = sub.add_parser("chat"); c.add_argument("message", nargs="+")
    c.add_argument("--model"); c.set_defaults(fn=cmd_chat)

    m = sub.add_parser("memory"); m.add_argument("action", choices=["list", "add", "rm"])
    m.add_argument("rest", nargs="*"); m.add_argument("--section", default="General")
    m.set_defaults(fn=cmd_memory)

    pb = sub.add_parser("playbook"); pb.add_argument("action", choices=["list", "show"])
    pb.add_argument("rest", nargs="*"); pb.set_defaults(fn=cmd_playbook)

    h = sub.add_parser("history"); h.add_argument("query", nargs="?")
    h.add_argument("--limit", type=int, default=20); h.set_defaults(fn=cmd_history)

    args = p.parse_args(argv)
    result = args.fn(args)
    return result if isinstance(result, int) else 0


if __name__ == "__main__":
    main()
