#!/usr/bin/env python3
"""
Hermes Agent pre_llm_call hook — inject DashClaw governance context.

This is the only Hermes hook whose return value rewrites the current
turn's user message. We use it to surface governance state the model
should be aware of: pending approvals on this session, recent blocks,
and active policies.

Context is sourced from a cache file written by the on_session_start
hook (refreshed every 5 minutes on subsequent turns). We avoid a
network round-trip on every turn — important because pre_llm_call sits
in the hot path.

Injected text is bounded to ~500 chars to stay polite with the
conversation context budget.
"""

from __future__ import annotations

import os
import sys
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from dashclaw_common import (  # noqa: E402
    AGENT_ID,
    api_request,
    emit_context,
    emit_noop,
    read_cache,
    read_stdin_json,
    write_cache,
)

CACHE_TTL_SECONDS = int(os.environ.get("DASHCLAW_PRELLM_REFRESH") or "300")
MAX_CONTEXT_CHARS = 500
MAX_HANDOFF_CHARS = 1500

# Module-level set tracks which session_ids have seen the first-turn injection.
# Hooks are short-lived processes, but Hermes may keep them resident; this is
# a defensive guard against double-injection within the same Python process.
_first_turn_seen: set[str] = set()


def _is_first_turn(session_id: str) -> bool:
    if not session_id:
        return False
    if session_id in _first_turn_seen:
        return False
    _first_turn_seen.add(session_id)
    return True


def _format_handoff(handoff: dict) -> str:
    bundle = handoff.get("bundle") or {}
    summary = (bundle.get("summary") or "").strip()
    loops = bundle.get("open_loops") or []
    decisions = bundle.get("decisions_made") or []
    if not summary and not loops and not decisions:
        return ""

    lines = ["[DashClaw handoff from previous session]"]
    if summary:
        lines.append(f"Summary: {summary}")
    if loops:
        lines.append("Open loops you committed to:")
        for loop in loops[:10]:
            if isinstance(loop, dict):
                desc = loop.get("description") or loop.get("title") or "(unnamed loop)"
                lines.append(f"  - {desc}")
    if decisions:
        lines.append("Recent decisions:")
        for d in decisions[:10]:
            if isinstance(d, dict):
                desc = d.get("description") or d.get("declared_goal") or "(unnamed decision)"
                lines.append(f"  - {desc}")
    return "\n".join(lines)[:MAX_HANDOFF_CHARS]


def _refresh_state(session_id: str) -> dict:
    """Pull a lightweight governance snapshot from DashClaw."""
    summary = api_request("GET", "/api/setup/summary", timeout=3) or {}
    approvals = api_request("GET", "/api/approvals?status=pending&limit=5", timeout=3) or {}
    pending = approvals.get("approvals") or approvals.get("items") or []
    counts = summary.get("counts") or {}
    return {
        "fetched_at": int(time.time()),
        "pending_approvals_count": len(pending),
        "pending_approvals_first": [a.get("id") for a in pending[:3] if isinstance(a, dict)],
        "policies_active": counts.get("policies_active", 0),
        "actions_24h": counts.get("actions_24h", 0),
    }


def _format_context(state: dict) -> str:
    pending = state.get("pending_approvals_count", 0)
    policies = state.get("policies_active", 0)
    actions = state.get("actions_24h", 0)

    # Skip injection entirely when there is nothing meaningful to add.
    # Empty `context` would still cost tokens; better to return no-op.
    if not pending and not policies:
        return ""

    parts = [f"[DashClaw] agent={AGENT_ID}"]
    if policies:
        parts.append(f"active_policies={policies}")
    if pending:
        ids = ", ".join(state.get("pending_approvals_first") or []) or "see /dashclaw-approvals"
        parts.append(f"pending_approvals={pending} (IDs: {ids})")
    if actions:
        parts.append(f"actions_today={actions}")
    parts.append("Call `dashclaw_guard` from MCP before destructive or network actions; record outcomes via `dashclaw_record_action`.")
    return " | ".join(parts)[:MAX_CONTEXT_CHARS]


def main() -> int:
    data = read_stdin_json()
    session_id = data.get("session_id") or ""
    if not session_id:
        emit_noop()
        return 0

    state = read_cache(session_id, suffix="prellm")
    now = int(time.time())
    if not state or (now - int(state.get("fetched_at", 0))) > CACHE_TTL_SECONDS:
        try:
            state = _refresh_state(session_id)
            write_cache(session_id, state, suffix="prellm")
        except Exception:
            # On any failure, fall back to the (possibly stale) cache or no-op.
            state = state or {}

    context = _format_context(state) if state else ""

    # On first turn of this session, prepend the previous-session handoff if
    # on_session_start cached one. Bounded to MAX_HANDOFF_CHARS so we don't
    # blow the context budget.
    if _is_first_turn(session_id):
        handoff = read_cache(session_id, suffix="handoff")
        handoff_text = _format_handoff(handoff) if handoff else ""
        if handoff_text:
            context = (handoff_text + ("\n\n" + context if context else ""))

    if context:
        emit_context(context)
    else:
        emit_noop()
    return 0


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