#!/usr/bin/env python3
"""ornith — one CLI to install, run, and use the local agentic coding pipeline.

  ornith init      one-time per-machine setup (aliases, binary check, machine.json)
  ornith doctor    health-check every service + dependency
  ornith up        start the proxies (think-off :11999, forge :8081) if not running
  ornith down      stop the proxies this CLI started
  ornith tui       full-screen TUI: give it work, it does it   (Concept A — local model)
  ornith coord     coordinator REPL (same brain as the TUI, line-based)
  ornith mcp       run the MCP server on stdio                  (Concept B — Claude Code drives)
  ornith agent     low-level interactive agent in the current repo
  ornith run F [N] deterministic pipeline on feature file F, N reps
  ornith verify    run the compile gate on the current repo

Config: defaults below, overridable in machine.json (ports/model/backend_url).
Nothing machine-specific is baked into code — it lives in machine.json.
"""
import json, os, socket, subprocess, sys, time, urllib.request

HERE = os.path.dirname(os.path.abspath(__file__))
RUNDIR = os.path.expanduser("~/.ornith/run"); os.makedirs(RUNDIR, exist_ok=True)

def _cfg():
    c = {"ollama_port": 11434, "thinkoff_port": 11999, "forge_port": 8081,
         "model": "ornith-iq3m", "planner": "ornith-plan",
         "backend_url": "http://localhost:11434"}
    mj = os.path.join(HERE, "machine.json")
    if os.path.exists(mj):
        try: c.update({k: v for k, v in json.load(open(mj)).items() if k in c})
        except Exception: pass
    return c
CFG = _cfg()

def _listening(port):
    s = socket.socket(); s.settimeout(0.5)
    try: s.connect(("127.0.0.1", port)); return True
    except Exception: return False
    finally: s.close()

def _ollama_ok():
    try:
        urllib.request.urlopen(f"http://127.0.0.1:{CFG['ollama_port']}/api/version", timeout=3); return True
    except Exception: return False

def _models():
    try:
        d = json.load(urllib.request.urlopen(f"http://127.0.0.1:{CFG['ollama_port']}/api/tags", timeout=5))
        return {m["name"].split(":")[0] for m in d.get("models", [])} | {m["name"] for m in d.get("models", [])}
    except Exception: return set()

def _start(name, argv, port, log):
    if _listening(port):
        print(f"  {name}: already listening on :{port}"); return
    lf = open(os.path.join(RUNDIR, log), "a")
    p = subprocess.Popen(argv, cwd=HERE, stdout=lf, stderr=lf,
                         stdin=subprocess.DEVNULL, start_new_session=True)
    open(os.path.join(RUNDIR, name + ".pid"), "w").write(str(p.pid))
    for _ in range(20):
        if _listening(port): print(f"  {name}: started (pid {p.pid}) on :{port}"); return
        time.sleep(0.5)
    print(f"  {name}: launched (pid {p.pid}) but :{port} not yet listening — see {log}")

def cmd_up(_):
    print("Starting services:")
    if not _ollama_ok():
        print(f"  ollama: NOT reachable on :{CFG['ollama_port']} — run `ollama serve` "
              "(OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0)")
    else:
        print(f"  ollama: ok on :{CFG['ollama_port']}")
    _start("thinkoff", [sys.executable, "think_off_proxy.py"], CFG["thinkoff_port"], "thinkoff.log")
    _start("forge", ["forge-proxy", "--backend", "ollama", "--backend-url", CFG["backend_url"],
                     "--model", CFG["model"], "--port", str(CFG["forge_port"]),
                     "--budget-mode", "manual", "--budget-tokens", "8192",
                     "--inject-respond-tool", "--reasoning-replay", "keep-last",
                     "--max-retries", "3", "--max-tool-errors", "3"], CFG["forge_port"], "forge.log")

def cmd_down(_):
    print("Stopping services this CLI started:")
    for name in ("forge", "thinkoff"):
        pf = os.path.join(RUNDIR, name + ".pid")
        if not os.path.exists(pf): print(f"  {name}: no pidfile"); continue
        pid = int(open(pf).read().strip())
        try: os.kill(pid, 15); print(f"  {name}: stopped (pid {pid})")
        except ProcessLookupError: print(f"  {name}: not running")
        os.remove(pf)

def cmd_doctor(_):
    ok = True
    def line(name, good, detail):
        nonlocal ok; ok = ok and good
        print(f"  [{'OK' if good else '--'}] {name}: {detail}")
    print("ornith doctor:")
    line("ollama", _ollama_ok(), f":{CFG['ollama_port']}" if _ollama_ok() else "unreachable — `ollama serve`")
    ms = _models()
    line(f"model {CFG['model']}", CFG["model"] in ms, "present" if CFG["model"] in ms else "missing — `ornith init`")
    line(f"planner {CFG['planner']}", CFG["planner"] in ms, "present" if CFG["planner"] in ms else "missing — `ornith init`")
    line("think-off proxy", _listening(CFG["thinkoff_port"]), f":{CFG['thinkoff_port']}" if _listening(CFG["thinkoff_port"]) else "down — `ornith up`")
    line("forge proxy", _listening(CFG["forge_port"]), f":{CFG['forge_port']}" if _listening(CFG["forge_port"]) else "down — `ornith up`")
    binp = os.path.join(HERE, "v2", "codebase-memory-mcp")
    line("codebase-memory", os.access(binp, os.X_OK), binp if os.access(binp, os.X_OK) else "missing/not executable")
    line("forge-proxy CLI", _which("forge-proxy"), "on PATH" if _which("forge-proxy") else "missing — `pip install forge-guardrails`")
    print("  =>", "all green" if ok else "issues above")
    sys.exit(0 if ok else 1)

def _which(prog):
    return any(os.access(os.path.join(d, prog), os.X_OK) for d in os.environ.get("PATH", "").split(os.pathsep) if d)

def cmd_init(_):
    print("ornith init (per-machine):")
    if not _ollama_ok():
        print(f"  ! ollama not reachable on :{CFG['ollama_port']}. Start it first: "
              "`OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 ollama serve`"); sys.exit(1)
    ms = _models()
    base = "maxwell1500/ornith-9b:IQ3_M"
    if not any(base.split(":")[0] in m for m in ms):
        print(f"  pulling {base} …")
        subprocess.run(["ollama", "pull", base])
    for alias, mf in ((CFG["model"], "Modelfile.iq3m"), (CFG["planner"], "Modelfile.plan")):
        if alias in _models():
            print(f"  alias {alias}: present"); continue
        mfp = os.path.join(HERE, mf)
        if not os.path.exists(mfp): print(f"  ! {mf} missing — cannot create {alias}"); continue
        print(f"  creating alias {alias} from {mf} …")
        subprocess.run(["ollama", "create", alias, "-f", mfp])
    binp = os.path.join(HERE, "v2", "codebase-memory-mcp")
    print(f"  codebase-memory binary: {'ok' if os.access(binp, os.X_OK) else 'MISSING — place the per-OS binary at '+binp}")
    print(f"  forge-proxy: {'ok' if _which('forge-proxy') else 'MISSING — pip install forge-guardrails'}")
    if not os.path.exists(os.path.join(HERE, "machine.json")):
        json.dump({"build_env": {}, "extra_tool_dirs": []}, open(os.path.join(HERE, "machine.json"), "w"), indent=2)
        print("  wrote default machine.json")
    print("  done. Run `ornith doctor` to verify, then `ornith up`.")

def _ensure_services():
    if not _listening(CFG["forge_port"]):
        print("[ornith] forge not up — starting services (`ornith up`) …")
        cmd_up(None)

def cmd_agent(argv):
    _ensure_services()
    repo = os.getcwd()
    os.execvp(sys.executable, [sys.executable, os.path.join(HERE, "hermes_agent.py"), "--repo", repo, *argv])

def cmd_run(argv):
    if not argv: print("usage: ornith run <feature.txt> [reps]"); sys.exit(2)
    _ensure_services()
    feature = os.path.abspath(argv[0]); reps = argv[1] if len(argv) > 1 else "3"
    # v2 orchestrator operates on a worktree; default to cwd
    os.execvp(sys.executable, [sys.executable, os.path.join(HERE, "run_v2.py"), feature, os.getcwd(), reps])

def cmd_verify(_):
    env = {**os.environ, "WS": os.getcwd()}
    subprocess.run([sys.executable, os.path.join(HERE, "verify.py")], cwd=HERE, env=env)

def cmd_tui(_):
    _ensure_services()
    os.execvp(sys.executable, [sys.executable, os.path.join(HERE, "tui.py"), "--repo", os.getcwd()])

def cmd_coord(argv):
    _ensure_services()
    os.execvp(sys.executable, [sys.executable, os.path.join(HERE, "coordinator.py"), "--repo", os.getcwd(), *argv])

def cmd_mcp(_):
    # stdio server — Claude Code launches this; repo defaults to cwd via ORNITH_REPO
    os.execvpe(sys.executable, [sys.executable, os.path.join(HERE, "mcp_server.py")],
               {**os.environ, "ORNITH_REPO": os.getcwd()})

CMDS = {"init": cmd_init, "doctor": cmd_doctor, "up": cmd_up, "down": cmd_down,
        "tui": cmd_tui, "coord": cmd_coord, "mcp": cmd_mcp,
        "agent": cmd_agent, "run": cmd_run, "verify": cmd_verify}

def main():
    if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"):
        print(__doc__); return
    cmd = sys.argv[1]
    if cmd not in CMDS:
        print(f"unknown command: {cmd}\n"); print(__doc__); sys.exit(2)
    CMDS[cmd](sys.argv[2:])

if __name__ == "__main__":
    main()
