#!/usr/bin/env python3
"""uxclaude.py - Claude Code texts you when it needs your input ("alertme").

Say "alertme on" in any Claude Code session and, from then on, that session
sends you a UserAlertX alert whenever Claude is waiting for your permission
or your next instruction. Say "alertme off" to stop; "alertme" alone tells
you whether it's on. Other sessions are unaffected.

Install (once):
  1. Save this file as ~/.claude/uxclaude.py
  2. Save your API key: run uxsend.py once and let it save the key, or put
     the key in ~/.useralertx_key, or set USERALERTX_API_KEY.
  3. Add these hooks to ~/.claude/settings.json (merge into any existing
     "hooks" section):

  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [ { "type": "command", "command": "python3 ~/.claude/uxclaude.py prompt" } ] }
    ],
    "Stop": [
      { "hooks": [ { "type": "command", "command": "python3 ~/.claude/uxclaude.py stop" } ] }
    ],
    "Notification": [
      { "matcher": "permission_prompt",
        "hooks": [ { "type": "command", "command": "python3 ~/.claude/uxclaude.py notify" } ] }
    ]
  }

Claude Code notices settings changes on its own - no restart needed.
When Claude finishes a reply, the "stop" hook waits WAIT seconds (60) in
the background and alerts you only if you haven't typed anything since.
A permission request alerts right away (Claude Code raises it after ~6s).
Test without sending: USERALERTX_DRYRUN=1. Uses only Python's standard library.
"""
import json
import os
import re
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

URL = os.environ.get("USERALERTX_URL", "https://useralertx.com/cgi-bin/itmessages.cgi")
KEYFILE = os.path.join(os.path.expanduser("~"), ".useralertx_key")
STATE = os.path.join(os.path.expanduser("~"), ".claude", "useralertx-alertme")
MIN_GAP = 60  # seconds between alerts for one session
WAIT = int(os.environ.get("USERALERTX_WAIT", "60"))  # idle time before a "done" alert


def read_hook_input():
    try:
        return json.load(sys.stdin)
    except ValueError:
        return {}


def flag_path(session_id):
    safe = re.sub(r"[^A-Za-z0-9_-]", "_", session_id or "unknown")
    return os.path.join(STATE, safe)


def cleanup():
    """Forget sessions untouched for a week."""
    try:
        for n in os.listdir(STATE):
            p = os.path.join(STATE, n)
            if time.time() - os.path.getmtime(p) > 7 * 86400:
                os.remove(p)
    except OSError:
        pass


def send_alert(text):
    if os.environ.get("USERALERTX_DRYRUN"):
        print(f"[dry run] would send: {text}", file=sys.stderr)
        return
    key = os.environ.get("USERALERTX_API_KEY", "").strip()
    if not key and os.path.exists(KEYFILE):
        key = open(KEYFILE).read().strip()
    if not key:
        print("uxclaude: no UserAlertX API key (see install notes)", file=sys.stderr)
        return
    req = urllib.request.Request(
        URL,
        data=urllib.parse.urlencode({"body": text}).encode(),
        headers={"X-Api-Key": key, "User-Agent": "UserAlertX-claude-hook/1.0"},
        method="POST",
    )
    try:
        urllib.request.urlopen(req, timeout=15).read()
    except (urllib.error.URLError, OSError) as e:
        print(f"uxclaude: alert not sent ({e})", file=sys.stderr)


def on_prompt(data):
    """UserPromptSubmit: "alertme on" / "alertme off" switch alerts for this
    session; a bare "alertme" just reports the current state. Whatever this
    prints is added to Claude's context, so Claude confirms it to the user."""
    prompt = (data.get("prompt") or "").lower()
    if not re.search(r"\balertme\b", prompt):
        touch_state(data.get("session_id"))  # you're here, cancels a pending "done" alert
        return
    os.makedirs(STATE, exist_ok=True)
    path = flag_path(data.get("session_id"))
    m = re.search(r"\balertme\s+(on|off)\b", prompt)
    if m and m.group(1) == "on":
        with open(path, "w") as f:
            f.write(json.dumps({"cwd": data.get("cwd", ""), "since": int(time.time()), "last_sent": 0}))
        print("UserAlertX alertme is now ON for this session: the user will get a text/email alert "
              "whenever you are waiting for their permission or their next instruction. "
              "Briefly confirm this to the user (they can say 'alertme off' to stop).")
    elif m and m.group(1) == "off":
        if os.path.exists(path):
            os.remove(path)
        print("UserAlertX alertme is now OFF for this session - no more alerts when you need input. "
              "Briefly confirm this to the user.")
    else:
        state = "ON" if os.path.exists(path) else "OFF"
        print(f"UserAlertX alertme is currently {state} for this session. If the user is asking about it, "
              "tell them: say 'alertme on' to get a text when you need their input, 'alertme off' to stop.")
    cleanup()


def load_state(path):
    try:
        return json.load(open(path))
    except (OSError, ValueError):
        return {}


def save_state(path, state):
    with open(path, "w") as f:
        f.write(json.dumps(state))


def touch_state(session_id):
    path = flag_path(session_id)
    if os.path.exists(path):
        state = load_state(path)
        state["last_prompt"] = time.time()
        save_state(path, state)


def alert_once(path, state, data, msg):
    now = int(time.time())
    if now - int(state.get("last_sent", 0)) < MIN_GAP:
        return
    project = os.path.basename((data.get("cwd") or state.get("cwd") or "").rstrip("/")) or "Claude"
    send_alert(f"Claude needs you ({project}): {msg}"[:300])
    state["last_sent"] = now
    save_state(path, state)


def on_stop(data):
    """Stop: Claude finished a reply. Wait in the background, then alert
    only if the user still hasn't typed anything."""
    path = flag_path(data.get("session_id"))
    if not os.path.exists(path):
        return
    # hand off to a detached copy of this script so Claude Code isn't held up
    data = {k: data.get(k) for k in ("session_id", "cwd")}  | {"stopped_at": time.time(),
            "last_assistant_message": (data.get("last_assistant_message") or "")[:400]}
    kw = {"stdin": subprocess.DEVNULL, "stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL,
          "env": dict(os.environ, UXCLAUDE_DATA=json.dumps(data))}
    if os.name == "nt":
        kw["creationflags"] = 0x00000008 | 0x00000200  # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
    else:
        kw["start_new_session"] = True
    subprocess.Popen([sys.executable, os.path.abspath(__file__), "wait"], **kw)


def on_wait(data):
    """Background half of on_stop."""
    path = flag_path(data.get("session_id"))
    stopped = float(data.get("stopped_at", 0))
    time.sleep(WAIT)
    if not os.path.exists(path):
        return  # alertme turned off meanwhile
    state = load_state(path)
    if float(state.get("last_prompt", 0)) >= stopped:
        return  # user already answered
    last = " ".join((data.get("last_assistant_message") or "").split())
    msg = "finished and is waiting for you" + (f' - "{last[:160]}"' if last else "")
    alert_once(path, state, data, msg)


def on_notify(data):
    """Notification (permission_prompt / idle_prompt): alert if this session opted in."""
    path = flag_path(data.get("session_id"))
    if not os.path.exists(path):
        return
    msg = (data.get("message") or "is waiting for your input").strip()
    alert_once(path, load_state(path), data, msg)


if __name__ == "__main__":
    mode = sys.argv[1] if len(sys.argv) > 1 else ""
    data = json.loads(os.environ.pop("UXCLAUDE_DATA", "{}")) if mode == "wait" else read_hook_input()
    if os.environ.get("UXCLAUDE_DEBUG"):
        try:
            with open(os.path.join(os.path.expanduser("~"), ".claude", "uxclaude-debug.log"), "a") as f:
                f.write(time.strftime("%Y%m%d %H%M%S ") + mode + " " + json.dumps({k: data.get(k) for k in ("hook_event_name", "notification_type", "message")}) + "\n")
        except OSError:
            pass
    if mode == "prompt":
        on_prompt(data)
    elif mode == "notify":
        on_notify(data)
    elif mode == "stop":
        on_stop(data)
    elif mode == "wait":
        on_wait(data)
    else:
        print(__doc__)
    sys.exit(0)  # never block Claude Code, whatever happens
