#!/usr/bin/env python3
"""session-monitor — the Session Monitor CLI.

Manage THIS container's keeper and query the fleet. The keeper is a long-running
daemon; this CLI is how you install/start/stop/inspect it. (The
/adom-remote-control skill just *guides* you to these commands — the CLI does the
work.)

  session-monitor update                                     pull latest + restart the keeper onto it (one-shot)
  session-monitor keeper start [--token T] [--service URL]   install + supervise the keeper here
  session-monitor keeper stop                                 stop this container's keeper
  session-monitor keeper status                               local process + code version + reporting (flags STALE)
  session-monitor keeper logs [-n N]                          tail the keeper log
  session-monitor status                                      keeper-fleet health (all your containers)
  session-monitor open                                        print the dashboard URL (with your token)
  session-monitor token mint <email>                          mint a token (operator; on the service container)
  session-monitor version

Add --json to any command for a machine-readable envelope.
"""
import os, sys, json, socket, subprocess, urllib.request

HOME = os.path.expanduser("~")
KEEP_DIR = os.path.join(HOME, ".session-keeper")
CLI_HOME = os.path.join(HOME, ".session-monitor-cli")                       # where the pkg install hook drops the runtime
SKILL_DIR = os.path.join(HOME, ".claude", "skills", "adom-remote-control")  # legacy bundled location (fallback)
ENV_FILE = os.path.join(KEEP_DIR, "env")
# realpath so a symlinked command (~/.local/bin/session-monitor) resolves to its
# real install dir, where keeper.py / setup.sh live.
SELF_DIR = os.path.dirname(os.path.realpath(__file__))
DEFAULT_SERVICE = "https://session-monitor-21nypoktx513.adom.cloud"
VERSION = "0.8.0"   # keep in lockstep with package.json "version" AND keeper.py KEEPER_VERSION


def _skill_file(*names):
    """Find a bundled runtime file (keeper installer / stop.sh / keeper.py) — next to
    this CLI, in the CLI home, or the legacy skill dir. Accepts alternate names (the
    keeper installer is `setup.sh` in the package, `install.sh` in the source tree)."""
    for d in (SELF_DIR, CLI_HOME, SKILL_DIR):
        for name in names:
            p = os.path.join(d, name)
            if os.path.exists(p):
                return p
    return os.path.join(CLI_HOME, names[0])


def _env():
    d = {}
    try:
        for ln in open(ENV_FILE):
            ln = ln.strip()
            if "=" in ln and not ln.startswith("#"):
                k, v = ln.split("=", 1)
                d[k] = v.strip().strip("'\"")
    except Exception:
        pass
    return d


def _service():
    return (_env().get("SM_SERVICE_URL") or os.environ.get("SM_SERVICE_URL") or DEFAULT_SERVICE).rstrip("/")


def _token():
    return _env().get("SM_TOKEN") or os.environ.get("SM_TOKEN") or ""


def _machine():
    return socket.gethostname()


def _flag(args, name):
    if name in args:
        i = args.index(name)
        if i + 1 < len(args):
            return args[i + 1]
    return None


def _pgrep(pat):
    try:
        return subprocess.run(["pgrep", "-f", pat], stdout=subprocess.DEVNULL,
                              stderr=subprocess.DEVNULL).returncode == 0
    except Exception:
        return False


def _running_keeper_version():
    try:
        return open(os.path.join(KEEP_DIR, "keeper.version")).read().strip() or None
    except Exception:
        return None                                    # missing → a pre-0.3 (unversioned) keeper is running


def _pkg_keeper_version():
    """KEEPER_VERSION baked into the INSTALLED package keeper.py — the code a restart
    would swap in. Parsed textually so we never import the keeper."""
    try:
        for ln in open(_skill_file("keeper.py")):
            ln = ln.strip()
            if ln.startswith("KEEPER_VERSION"):
                return ln.split("=", 1)[1].strip().strip("'\"")
    except Exception:
        pass
    return None


def _keeper_stale():
    """(stale, running_ver, pkg_ver): True iff a keeper is RUNNING but its code is
    older than the installed package (or predates versioning). This is the signal
    that used to be invisible — a container reporting fine on ancient keeper code."""
    run, pkg = _running_keeper_version(), _pkg_keeper_version()
    stale = _pgrep(f"{KEEP_DIR}/keeper.py") and bool(pkg) and run != pkg
    return (stale, run, pkg)


def _api(path):
    req = urllib.request.Request(_service() + path,
                                 headers={"X-SM-Token": _token(), "User-Agent": "session-monitor-cli/" + VERSION})
    with urllib.request.urlopen(req, timeout=15) as r:
        return json.loads(r.read())


def _out(status, msg, data=None, hints=None):
    """AI-oriented output: a human line + Hint: lines + optional JSON envelope."""
    print(f"{status}: {msg}")
    for h in (hints or []):
        print(f"Hint: {h}")
    if "--json" in sys.argv:
        print(json.dumps({"status": status, "message": msg, "data": data or {}, "hints": hints or []}))
    return 0 if status == "OK" else 1


def cmd_keeper(args):
    sub = args[0] if args else "status"
    rest = args[1:]
    if sub == "start":
        setup = _skill_file("setup.sh", "install.sh")
        token = _flag(rest, "--token") or _token()
        service = _flag(rest, "--service") or _service()
        if not os.path.exists(setup):
            return _out("ERROR", "keeper setup script not found",
                        hints=["Reinstall the CLI: adom-wiki pkg install adom/session-monitor-cli"])
        if not token:
            return _out("ERROR", "no token given",
                        hints=["Pass --token <T> (ask whoever runs the monitor), or set SM_TOKEN"])
        subprocess.run(["bash", setup, service, token])
        return _out("OK", f"keeper started → {service}", hints=["Verify: session-monitor keeper status"])
    if sub == "stop":
        stop = _skill_file("stop.sh")
        if not os.path.exists(stop):
            return _out("ERROR", "stop script not found")
        subprocess.run(["bash", stop])
        return _out("OK", "keeper stopped on this container")
    if sub == "logs":
        n = _flag(rest, "-n") or "40"
        log = os.path.join(KEEP_DIR, "keeper.log")
        if not os.path.exists(log):
            return _out("ERROR", "no keeper log yet", hints=["Start it: session-monitor keeper start --token <T>"])
        subprocess.run(["tail", "-n", str(n), log])
        return 0
    # default: status
    daemon = _pgrep(f"{KEEP_DIR}/daemon.sh")
    keeper = _pgrep(f"{KEEP_DIR}/keeper.py")
    stale, run_ver, pkg_ver = _keeper_stale()
    svc = None
    try:
        svc = _api(f"/api/keeper-status?machine={_machine()}")
    except Exception as e:
        svc = {"error": str(e)}
    line = f"local daemon={'up' if daemon else 'DOWN'} keeper={'up' if keeper else 'DOWN'}"
    if keeper:
        line += f" (code {run_ver or 'pre-0.3 unversioned'})"
    if svc and not svc.get("error"):
        line += (f" | service {'reporting' if svc.get('reporting') else 'NOT reporting'}"
                 f" (last seen {svc.get('age_sec')}s ago, {svc.get('sessions', 0)} sessions)")
    running = daemon and keeper
    data = {"daemon": daemon, "keeper": keeper, "keeper_version": run_ver,
            "package_version": pkg_ver, "stale": stale, "service": svc}
    if not running:
        return _out("ERROR", line, data=data,
                    hints=["Revive it: session-monitor keeper start --token <T>"])
    if stale:
        # Running fine but on OLD code — the exact case that used to read as "healthy"
        # while a container ran a dismally outdated keeper. Report it as NOT-OK so no
        # agent can declare victory here. Refreshing is a safe restart of a process
        # that is ALREADY egressing — it is NOT new data egress.
        return _out("ERROR", line + f"  ** STALE keeper: running {run_ver or 'pre-0.3'}, {pkg_ver} installed **",
                    data=data,
                    hints=["Refresh the running keeper (safe restart, NOT new egress): session-monitor update",
                           "  (or: session-monitor keeper start — reuses the saved token)"])
    return _out("OK", line, data=data)


def cmd_status(args):
    try:
        d = _api("/api/keeper-health")
    except Exception as e:
        return _out("ERROR", f"can't reach the monitor service: {e}",
                    hints=["Check your token: session-monitor open"])
    return _out("OK", f"fleet: {d['reporting']} reporting · {d['down']} down · "
                      f"{d['containers']} containers · {d['owners']} owner(s)", data=d)


def cmd_open(args):
    tok = _token()
    if not tok:
        return _out("ERROR", "no token set here", hints=["Run: session-monitor keeper start --token <T> first"])
    return _out("OK", f"{_service()}/?token={tok}",
                hints=["Open on your phone; Safari → Add to Home Screen for a PWA (mic permission sticks)."])


def cmd_token(args):
    if len(args) < 2 or args[0] != "mint":
        return _out("ERROR", "usage: session-monitor token mint <email>")
    email = args[1]
    add = os.path.join(HOME, "adom-session-monitor", "service", "add-user.sh")
    if not os.path.exists(add):
        return _out("ERROR", "token minting runs on the SERVICE container (add-user.sh not found here)",
                    hints=["SSH to the service container and run: service/add-user.sh " + email])
    subprocess.run(["bash", add, email])
    return _out("OK", f"minted a token for {email}")


def cmd_update(args):
    """One-shot update: pull the latest package, then restart the running keeper
    onto it. Safe to re-run. On an already-hooked container it reuses the saved
    token, and the restart is a safe cycle of a process that is ALREADY reporting —
    NOT new data egress — so it never needs fresh consent."""
    steps = []
    try:
        r = subprocess.run(["adom-wiki", "pkg", "install", "adom/session-monitor-cli"],
                           capture_output=True, text=True)
        steps.append("pkg install " + ("ok" if r.returncode == 0 else "FAILED"))
    except FileNotFoundError:
        steps.append("pkg install skipped (adom-wiki not on PATH)")
    setup = _skill_file("setup.sh", "install.sh")
    token, service = _token(), _service()
    if not os.path.exists(setup):
        return _out("ERROR", "keeper setup script not found",
                    hints=["Reinstall: adom-wiki pkg install adom/session-monitor-cli"])
    if not token:
        return _out("ERROR", "no saved token — this container isn't hooked in yet",
                    hints=["First hookup: session-monitor keeper start --token <T>"])
    subprocess.run(["bash", setup, service, token])       # copies the new keeper.py + clean restart
    stale, run_ver, pkg_ver = _keeper_stale()
    steps.append(f"keeper restarted (code {run_ver or '?'})")
    ok = not stale
    return _out("OK" if ok else "ERROR", "update: " + " · ".join(steps),
                data={"stale": stale, "keeper_version": run_ver, "package_version": pkg_ver},
                hints=[] if ok else ["still stale — see: session-monitor keeper logs -n 40"])


def main():
    argv = [a for a in sys.argv[1:] if a != "--json"]
    cmd = argv[0] if argv else "help"
    rest = argv[1:]
    if cmd in ("version", "-v", "--version"):
        return _out("OK", "session-monitor " + VERSION)
    if cmd in ("help", "-h", "--help"):
        print(__doc__)
        return 0
    dispatch = {"keeper": cmd_keeper, "status": cmd_status, "open": cmd_open,
                "token": cmd_token, "update": cmd_update}
    fn = dispatch.get(cmd)
    if not fn:
        return _out("ERROR", f"unknown command: {cmd}", hints=["Run: session-monitor help"])
    return fn(rest)


if __name__ == "__main__":
    sys.exit(main())
