#!/usr/bin/env python3
"""
AgentsRoom team-run reminder hook for Claude Code.

Wired as a UserPromptSubmit hook in `.claude/settings.local.json` for agents
spawned as part of a team run. Before each user message is sent to Claude,
this script runs and — only when there is genuinely new team activity —
prints a SHORT reminder to stdout that gets prepended as context.

Why this exists
---------------
System-prompt instructions about the team (your role, current cycle, NOTES.md)
get summarised away by Claude's context compaction on long runs. A
UserPromptSubmit hook is re-injected EVERY turn, so it survives compaction.
We keep the output small (~100 tokens worst case) and skip output entirely
when nothing has changed since the agent's last turn — preserves the
input-token budget without hiding relevant updates.

Contract
--------
  1. NEVER raise. Any exception = silent exit 0 (Claude proceeds normally).
  2. NEVER block > 1s. Pure local file I/O, no network.
  3. NEVER print unless there's new info worth telling the agent about.
  4. If env vars are missing (not a team run) → silent exit 0.
  5. Output is plain text, no markdown/ANSI — Claude treats it as
     additional context prepended to the user message.
"""

import json
import os
import sys
import time


def _quiet_exit() -> None:
    sys.exit(0)


def _read_json(path: str):
    try:
        with open(path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception:
        return None


def _atomic_write_json(path: str, obj) -> None:
    """Write-tmp + rename. Same invariant as teamRunState.ts atomicWriteJson."""
    try:
        d = os.path.dirname(path)
        if d and not os.path.isdir(d):
            os.makedirs(d, exist_ok=True)
        tmp = f"{path}.tmp.{os.getpid()}.{int(time.time() * 1000)}"
        with open(tmp, 'w', encoding='utf-8') as f:
            json.dump(obj, f, indent=2)
            try:
                f.flush()
                os.fsync(f.fileno())
            except Exception:
                pass
        os.replace(tmp, path)
    except Exception:
        pass


def _count_unread_inbox(inbox_dir: str, my_role: str, since_seq: int) -> int:
    """Count messages addressed to us (or broadcast) with seq > since_seq."""
    try:
        entries = sorted(os.listdir(inbox_dir))
    except Exception:
        return 0
    count = 0
    for name in entries:
        if not name.endswith('.json'):
            continue
        msg = _read_json(os.path.join(inbox_dir, name))
        if not msg:
            continue
        seq = msg.get('seq')
        if not isinstance(seq, int) or seq <= since_seq:
            continue
        to_role = msg.get('toRole') or ''
        if to_role not in (my_role, 'any', '*'):
            continue
        count += 1
    return count


def main() -> None:
    run_id = os.environ.get('AGENTSROOM_TEAM_RUN_ID')
    node_id = os.environ.get('AGENTSROOM_TEAM_NODE_ID')
    agent_id = os.environ.get('AGENTSROOM_TEAM_AGENT_ID')
    workspace = os.environ.get('AGENTSROOM_TEAM_WORKSPACE')
    role = os.environ.get('AGENTSROOM_TEAM_ROLE') or 'agent'

    if not (run_id and node_id and agent_id and workspace):
        # Not a team-run agent — no reminder to add.
        return

    # Drain stdin silently. UserPromptSubmit sends a JSON payload; we don't
    # use it, but Claude waits on the pipe being closed before continuing,
    # so read + discard.
    try:
        sys.stdin.read()
    except Exception:
        pass

    run_dir = os.path.join(workspace, '.agentsroom', 'team-runs', run_id)
    state_path = os.path.join(run_dir, 'state.json')
    inbox_dir = os.path.join(run_dir, 'inbox')

    state = _read_json(state_path)
    if not state:
        # Run state missing — run was cleaned up, nothing to remind about.
        return

    # Parallel fan-out: a branch agent's identity lives in parallelSteps
    # (keyed by its own agentId, env-reliable since branch agents never
    # morph). currentStep tracks the PRIMARY agent's branch only.
    parallel_steps = state.get('parallelSteps') or {}
    my_branch = parallel_steps.get(agent_id) or {}
    if my_branch.get('nodeId'):
        current_step = my_branch
    else:
        current_step = state.get('currentStep') or {}
    cur_node = current_step.get('nodeId')
    cur_agent = current_step.get('agentId')
    cycle = current_step.get('cycle') or 1
    status = state.get('status') or 'running'

    # If the active step isn't ours, stay silent — the agent may be a zombie
    # from a previous cycle whose terminal the user re-opened. We don't want
    # to flood the context with "you're not active". Branch agents matched
    # their own parallelSteps entry above, so this guard only bites zombies.
    if not my_branch.get('nodeId'):
        if cur_node and cur_node != node_id:
            return
        if cur_agent and cur_agent != agent_id:
            return

    note_count = state.get('noteCount') or 0
    inbox_count = state.get('inboxCount') or 0
    cursors = state.get('agentCursors') or {}
    my_cursor = cursors.get(agent_id) or {}
    last_note = my_cursor.get('lastSeenNoteCount') or 0
    last_inbox_seq = my_cursor.get('lastSeenInboxCount') or 0

    new_notes = max(0, note_count - last_note)
    unread_inbox = _count_unread_inbox(inbox_dir, role, last_inbox_seq) if inbox_count > last_inbox_seq else 0

    # If nothing is new and this isn't the very first turn, stay silent.
    is_first_turn = not my_cursor  # no cursor persisted yet
    if not is_first_turn and new_notes == 0 and unread_inbox == 0:
        return

    # Build the shortest useful reminder.
    lines = []
    if is_first_turn:
        lines.append(
            f"[TEAM] run={run_id} role={role} cycle={cycle} status={status}"
        )
        lines.append(
            "[TEAM] Call team_get_context() first (cheap), then team_read_notes() "
            "and team_read_inbox() to catch up. Signal completion with "
            "team_complete_step({featureSummary, risks, testHints, flags})."
        )
    else:
        parts = []
        if new_notes > 0:
            parts.append(f"{new_notes} new note{'s' if new_notes > 1 else ''} from teammates")
        if unread_inbox > 0:
            parts.append(f"{unread_inbox} unread inbox message{'s' if unread_inbox > 1 else ''}")
        if parts:
            lines.append(f"[TEAM] cycle={cycle} | " + " | ".join(parts))
            if new_notes > 0:
                lines.append("[TEAM] Read them with team_read_notes(). "
                             "Acknowledge or act on anything relevant before continuing.")
            if unread_inbox > 0:
                lines.append("[TEAM] Read inbox with team_read_inbox() — teammates are waiting on answers.")

    if not lines:
        return

    # Advance our cursor so we don't re-remind on the next turn.
    try:
        # Re-read to avoid stomping on a recent main-process update; merge.
        fresh = _read_json(state_path) or state
        cursors = dict(fresh.get('agentCursors') or {})
        cursors[agent_id] = {
            'lastSeenNoteCount': note_count,
            'lastSeenInboxCount': inbox_count,
            'lastRemindedAt': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
        }
        fresh['agentCursors'] = cursors
        _atomic_write_json(state_path, fresh)
    except Exception:
        # Cursor update is best-effort. Worst case: we remind again next turn.
        pass

    # Emit the reminder. Claude prepends stdout to the user message.
    print('\n'.join(lines))


if __name__ == '__main__':
    try:
        main()
    except BaseException:
        # Last-ditch safety net — never let anything reach Claude's terminal.
        pass
