"""Plugin hook bus — six choke points where plugins can observe or modify agent behavior.

Engine, not strategy (Rule #12). Handlers register from PluginBase.init() and run
in priority order. Every hook inherits the current correlation_scope so plugin
activity shows up in /decisions under the same chain as the tool call that
triggered it.

Four hooks in v1, deliberately chosen:
    before_tool_call    — rate limit, transform args, veto
    after_tool_call     — logging, result rewriting, redaction
    before_agent_reply  — content filters, PII scrubbing, post-processing
    message_received    — channel-level preprocessing (translate, drop spam)

Reserved for a later pass (not yet fired by any call site, deliberately
omitted from the enum so plugins can't silently register dead handlers):
session_start, session_end — need a clean teardown point in chat flow
that doesn't currently exist.

Failure policy is per-hook. fail_closed hooks (before_tool_call,
before_agent_reply) abort on handler error. fail_open hooks tolerate handler
errors and continue the chain.

The bus does NOT expose prompt building. before_prompt_build / before_agent_start
are deliberately excluded — those are prompt-injection vectors and the right
answer is to not expose the surface, not to gate it behind opt-in flags.
"""

from __future__ import annotations

import logging
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Literal

log = logging.getLogger(__name__)


class HookName(str, Enum):
    BEFORE_TOOL_CALL = "before_tool_call"
    AFTER_TOOL_CALL = "after_tool_call"
    BEFORE_AGENT_REPLY = "before_agent_reply"
    MESSAGE_RECEIVED = "message_received"


FailurePolicy = Literal["fail_closed", "fail_open"]

_POLICY: dict[HookName, FailurePolicy] = {
    HookName.BEFORE_TOOL_CALL: "fail_closed",
    HookName.AFTER_TOOL_CALL: "fail_open",
    HookName.BEFORE_AGENT_REPLY: "fail_closed",
    HookName.MESSAGE_RECEIVED: "fail_open",
}


@dataclass
class HookContext:
    """Mutable context passed to every hook handler.

    Handlers modify the relevant fields in-place. The bus reads them back after
    the chain completes and returns the (possibly mutated) ctx to the caller.
    """

    hook: HookName
    plugin_name: str = ""  # set by the bus before each handler runs

    # ── tool-call hooks ─────────────────────────────────
    tool_name: str = ""
    tool_input: dict = field(default_factory=dict)
    tool_result: dict = field(default_factory=dict)

    # ── reply hook ──────────────────────────────────────
    reply_text: str = ""

    # ── channel message hook ────────────────────────────
    message_text: str = ""
    channel: str = ""
    sender: str = ""
    message_metadata: dict = field(default_factory=dict)

    # ── reply hook context ──────────────────────────────
    conversation_id: str = ""

    # ── mutable outputs every hook can set ──────────────
    veto: bool = False
    veto_reason: str = ""

    # ── free-form scratch space for plugins ─────────────
    metadata: dict = field(default_factory=dict)


HookHandler = Callable[[HookContext], Awaitable[None]]


class HookBus:
    """Async hook dispatcher. One instance per process, injected into PluginContext."""

    def __init__(self, activity_logger: Any | None = None) -> None:
        self._handlers: dict[HookName, list[tuple[int, str, HookHandler]]] = {
            h: [] for h in HookName
        }
        self._activity = activity_logger

    def register(
        self,
        hook: HookName,
        handler: HookHandler,
        *,
        plugin_name: str,
        priority: int = 50,
    ) -> None:
        """Register a handler. Lower priority runs first (0 = earliest, 100 = latest)."""
        self._handlers[hook].append((priority, plugin_name, handler))
        self._handlers[hook].sort(key=lambda t: t[0])
        log.debug(
            "hook registered: %s ← %s (priority=%d)",
            hook.value,
            plugin_name,
            priority,
        )

    def unregister_plugin(self, plugin_name: str) -> int:
        """Remove every handler registered by `plugin_name`. Returns count removed."""
        removed = 0
        for hook in HookName:
            before = len(self._handlers[hook])
            self._handlers[hook] = [
                (p, n, h) for (p, n, h) in self._handlers[hook] if n != plugin_name
            ]
            removed += before - len(self._handlers[hook])
        if removed:
            log.debug("hook handlers removed for %s: %d", plugin_name, removed)
        return removed

    def handler_count(self, hook: HookName | None = None) -> int:
        """Total registered handlers, or count for one hook if specified."""
        if hook is None:
            return sum(len(h) for h in self._handlers.values())
        return len(self._handlers[hook])

    async def fire(self, hook: HookName, ctx: HookContext) -> HookContext:
        """Run every handler for `hook` in priority order. Returns the (possibly mutated) context.

        - Handlers see each other's mutations (chain semantics, not parallel).
        - veto=True short-circuits the chain.
        - fail_closed handler exception sets veto + veto_reason.
        - fail_open handler exception is logged + activity-recorded, chain continues.
        """
        ctx.hook = hook
        policy = _POLICY[hook]
        for _priority, plugin_name, handler in self._handlers[hook]:
            ctx.plugin_name = plugin_name
            try:
                await handler(ctx)
            except Exception as e:
                log.warning(
                    "hook handler failed: %s / %s: %s",
                    hook.value,
                    plugin_name,
                    e,
                )
                if self._activity is not None:
                    try:
                        self._activity.log(
                            category="system",
                            action="hook_handler_failed",
                            summary=f"{plugin_name} failed in {hook.value}",
                            details={
                                "hook": hook.value,
                                "plugin": plugin_name,
                                "error": str(e),
                                "error_type": type(e).__name__,
                            },
                            importance="medium",
                        )
                    except Exception:
                        pass
                if policy == "fail_closed":
                    ctx.veto = True
                    ctx.veto_reason = (
                        f"hook handler error in {plugin_name}: {type(e).__name__}: {e}"
                    )
                    return ctx
            if ctx.veto:
                return ctx
        return ctx
