#!/usr/bin/env python3
"""Genius presence heartbeat (amarre v2, Phase 2) — CONSERVATIVE.

Emits one presence ping (~every PING_INTERVAL) to the Genius capture/presence endpoint
ONLY when there is REAL recent activity, so it binds worked-time to the consultant's
machine clock without counting an idle-open VSCode:

  ACTIVE  := a workspace SOURCE file was edited in the last IDLE_WINDOW   (human work)
          OR the Claude transcript / agent files grew in the last IDLE_WINDOW
             (a /loop or Workflow is executing — counts even with no human interaction)
  IDLE    := neither -> NO ping (VSCode open but nothing running is NOT counted)

Metadata only: workspace path + git branch (for project classification + retroactive
attribution) + machine-clock timestamp + tz. No transcript content. Never blocks, always
exit 0. Single-instance per machine (lockfile). Self-terminates when the Claude session
is gone (transcript dir idle > STOP_IDLE) or after MAX_LIFETIME.
"""
import json
import os
import re
import subprocess
import sys
import time
import urllib.request

HOME = os.path.expanduser("~")
CFG = os.path.join(HOME, ".genius", "config.json")
LOCK = os.path.join(HOME, ".genius", "presence.lock")
STOP_FLAG = os.path.join(HOME, ".genius", "presence.stop")
PROJECTS = os.path.join(HOME, ".claude", "projects")

PING_INTERVAL = 300        # 5 min between ticks
IDLE_WINDOW = 600          # a file touched in the last 10 min => active this tick
STOP_IDLE = 2700           # Claude dir quiet 45 min => session over, exit
MAX_LIFETIME = 18 * 3600   # hard stop
SKIP_DIRS = {".git", "node_modules", "__pycache__", ".next", "dist", "build",
             ".venv", "venv", ".mypy_cache", ".pytest_cache", "outbox"}


def _cfg():
    try:
        with open(CFG) as fh:
            return json.load(fh)
    except Exception:
        return {}


def _recent(path, window_s, budget_s=2.0):
    """True if any non-skipped file under path has mtime within window_s. Bounded by a
    small wall-clock budget so a huge tree never stalls a tick. Early-returns."""
    cutoff = time.time() - window_s
    deadline = time.time() + budget_s
    try:
        for root, dirs, files in os.walk(path):
            dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
            for f in files:
                try:
                    if os.path.getmtime(os.path.join(root, f)) >= cutoff:
                        return True
                except OSError:
                    pass
            if time.time() > deadline:
                break
    except OSError:
        pass
    return False


def _project_dir(cwd):
    """Best-effort: the ~/.claude/projects/<slug> dir for this cwd, else the most
    recently-modified project dir (the active session)."""
    slug = re.sub(r"[^A-Za-z0-9]+", "-", cwd)
    cand = os.path.join(PROJECTS, slug)
    if os.path.isdir(cand):
        return cand
    # fallback: most-recently-touched project dir
    try:
        best, best_m = None, 0
        for d in os.listdir(PROJECTS):
            p = os.path.join(PROJECTS, d)
            if os.path.isdir(p):
                m = os.path.getmtime(p)
                if m > best_m:
                    best, best_m = p, m
        return best
    except OSError:
        return None


def _git_branch(cwd):
    try:
        out = subprocess.run(["git", "-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"],
                             capture_output=True, text=True, timeout=3)
        b = (out.stdout or "").strip()
        return b if b and b != "HEAD" else ""
    except Exception:
        return ""


def _post(cfg, payload):
    base = (cfg.get("base_url") or "").rstrip("/")
    token = cfg.get("token") or ""
    if not base or not token:
        return False
    url = base + "/xma/genius/v1/capture/presence"
    body = json.dumps({"jsonrpc": "2.0", "method": "call",
                       "params": payload, "id": 1}).encode()
    req = urllib.request.Request(url, data=body, method="POST")
    req.add_header("Content-Type", "application/json")
    req.add_header("Authorization", "Bearer " + token)
    req.add_header("User-Agent", "GeniusPresence/1.0")  # WAF blocks default Python-urllib UA
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            json.loads(resp.read().decode("utf-8", "replace") or "{}")
        return True
    except Exception:
        return False


def _work_mode(cwd, claude_dir):
    """Label HOW the work is happening (owner: loop/workflow vs short interactions).
    A workspace edit = the human is present -> interactive. Otherwise, if a Workflow/
    subagent run is active -> autonomous (Claude working alone, e.g. overnight). Default
    interactive (a normal Claude turn)."""
    if _recent(cwd, IDLE_WINDOW):
        return "interactive"
    if claude_dir:
        for sub in ("subagents", os.path.join("subagents", "workflows")):
            if _recent(os.path.join(claude_dir, sub), IDLE_WINDOW, budget_s=1.5):
                return "autonomous"
    return "interactive"


def _ping(cfg, cwd, session_ref, claude_dir):
    payload = {
        "session_ref": session_ref,
        "occurred_at_epoch": time.time(),
        "utc_offset_minutes": -int(time.timezone // 60) if not time.daylight
                              else -int(time.altzone // 60),
        "workspace": cwd[:256],
        "git_branch": _git_branch(cwd)[:128],
        "surface": "vscode",
        "work_mode": _work_mode(cwd, claude_dir),
    }
    return _post(cfg, payload)


def _is_active(cwd, claude_dir):
    """Conservative: real work signal in the last IDLE_WINDOW — human edit OR Claude
    execution (loop/workflow). Idle-open VSCode touches no file -> not active."""
    if _recent(cwd, IDLE_WINDOW):
        return True
    if claude_dir and _recent(claude_dir, IDLE_WINDOW):
        return True
    return False


def _claude_idle_too_long(claude_dir):
    return not (claude_dir and _recent(claude_dir, STOP_IDLE, budget_s=2.0))


def main():
    args = sys.argv[1:]
    once = "--once" in args
    cwd = os.getcwd()
    session_ref = "vscode:%s" % time.strftime("%Y-%m-%d")
    for i, a in enumerate(args):
        if a == "--cwd" and i + 1 < len(args):
            cwd = args[i + 1]
        if a == "--session" and i + 1 < len(args):
            session_ref = args[i + 1]
    cfg = _cfg()
    claude_dir = _project_dir(cwd)

    if once:
        active = _is_active(cwd, claude_dir)
        ok = _ping(cfg, cwd, session_ref, claude_dir) if active else False
        print(json.dumps({"active": active, "posted": ok, "cwd": cwd,
                          "claude_dir": claude_dir, "session_ref": session_ref}))
        return

    # single-instance lock
    try:
        if os.path.exists(LOCK):
            with open(LOCK) as fh:
                pid = int((fh.read() or "0").strip() or 0)
            if pid:
                try:
                    os.kill(pid, 0)   # alive?
                    return            # another daemon owns it
                except OSError:
                    pass
        with open(LOCK, "w") as fh:
            fh.write(str(os.getpid()))
    except Exception:
        pass

    start = time.time()
    try:
        while True:
            if os.path.exists(STOP_FLAG):
                break
            if time.time() - start > MAX_LIFETIME:
                break
            claude_dir = _project_dir(cwd)  # refresh (slug may resolve later)
            if _is_active(cwd, claude_dir):
                _ping(cfg, cwd, session_ref, claude_dir)
            elif _claude_idle_too_long(claude_dir):
                break  # session truly over
            time.sleep(PING_INTERVAL)
    finally:
        try:
            if os.path.exists(LOCK):
                os.remove(LOCK)
        except OSError:
            pass


if __name__ == "__main__":
    try:
        main()
    except Exception:
        pass
    sys.exit(0)
