"""Host-owned cancellation for agents created deep inside synchronous work.

A host that runs a blocking command on a worker thread (Hermes Console) never sees
the ``AIAgent`` a CLI subcommand forks inside it, so it cannot call ``interrupt()``
when the user cancels. The host binds an :class:`InterruptScope` around the work;
every ``run_conversation()`` under that scope registers its agent, and
``scope.cancel()`` hard-interrupts them from any thread. Agents registering after
the cancel are interrupted immediately, so a cancel never loses the race with a
turn that has not started yet (#106179).
"""

from __future__ import annotations

import threading
from contextlib import contextmanager, nullcontext
from contextvars import ContextVar
from typing import Any, Iterator, Optional

from agent.interrupt_compat import request_hard_interrupt

_ACTIVE_SCOPE: ContextVar[Optional["InterruptScope"]] = ContextVar("hermes_interrupt_scope", default=None)


_TOOL_REASON_HOST_CANCELLED = "host cancelled the command"


class InterruptScope:
    def __init__(self) -> None:
        self._lock = threading.Lock()
        self._agents: list[Any] = []
        self.reason: Optional[str] = None
        self._tool_reason: Optional[str] = _TOOL_REASON_HOST_CANCELLED

    def cancel(self, reason: str, *, tool_reason: Optional[str] = _TOOL_REASON_HOST_CANCELLED) -> None:
        """Latch ``reason`` and hard-interrupt every agent running under this scope.

        ``tool_reason`` names the system issuer; pass ``None`` for a human stop so the turn is
        attributed to the user rather than to the host (#112647)."""
        with self._lock:
            self.reason = reason
            self._tool_reason = tool_reason
            agents = list(self._agents)
        for agent in agents:
            request_hard_interrupt(agent, reason, tool_reason=tool_reason)

    @contextmanager
    def track(self, agent: Any) -> Iterator[None]:
        with self._lock:
            self._agents.append(agent)
            reason, tool_reason = self.reason, self._tool_reason
        if reason is not None:
            request_hard_interrupt(agent, reason, tool_reason=tool_reason)
        try:
            yield
        finally:
            with self._lock:
                self._agents.remove(agent)


@contextmanager
def bind_interrupt_scope(scope: Optional[InterruptScope]) -> Iterator[None]:
    """Make ``scope`` the owner of every agent turn started in this context."""
    token = _ACTIVE_SCOPE.set(scope)
    try:
        yield
    finally:
        _ACTIVE_SCOPE.reset(token)


def track_in_interrupt_scope(agent: Any):
    """Register ``agent`` with the bound scope for the duration of its turn (no-op without one)."""
    scope = _ACTIVE_SCOPE.get()
    return nullcontext() if scope is None else scope.track(agent)
