"""Budget Guard plugin — sample hook bus consumer.

Demonstrates how a plugin observes and modifies agent behavior via the hook
bus. Registers a `before_tool_call` handler that counts invocations of a few
expensive tools (per-process, in-memory) and vetoes further calls once a soft
budget is hit. The veto goes back to the agent loop as a tool error, so the
LLM sees it and can adapt its plan.

Disabled by default — enable from the /plugins page or via PluginManager API.
This is a reference implementation, not a production budget guard. A real one
would persist counts to SQLite, reset on a schedule, and read its limits from
a YAML config file.
"""

from __future__ import annotations

from pathlib import Path

from agntspace.plugins.base import PluginBase, PluginContext
from agntspace.plugins.hooks import HookContext, HookName


def _load_config() -> dict:
    """Load config.yaml from the same directory as this plugin file.

    Phase 4 / Rule 12 migration: the expensive-tools list and the
    budget value previously lived as Python module-level constants.
    Now they live in config.yaml so users can customize without
    touching code. The YAML is the source of truth; the fallback
    below mirrors it for degraded mode (missing PyYAML or broken file).
    """
    config_path = Path(__file__).parent / "config.yaml"
    if not config_path.is_file():
        return {}
    try:
        import yaml  # type: ignore
        with open(config_path, encoding="utf-8") as f:
            data = yaml.safe_load(f)
        return data if isinstance(data, dict) else {}
    except Exception:
        return {}


# Load once at import time -- the plugin is a long-lived process object
# and the config doesn't change at runtime.
_CFG = _load_config()
_EXPENSIVE_TOOLS: frozenset[str] = frozenset(  # arch:deterministic -- loaded from config.yaml at import time, this is the runtime-resolved set not a hardcoded policy; the YAML is the source of truth
    _CFG.get("expensive_tools", [
        "research_scrape", "deep_memory_recall", "run_wiki_job",
        "scrape_url", "web_search",
    ])
)
_DEFAULT_BUDGET: int = int(_CFG.get("budget", 1000))


class BudgetGuard(PluginBase):
    def __init__(self) -> None:
        self._counts: dict[str, int] = {}
        self._budget = _DEFAULT_BUDGET
        self._log = None

    def init(self, ctx: PluginContext) -> None:
        self._log = ctx.log
        if ctx.hooks is None:
            ctx.log.warning("budget-guard: hook bus not available, plugin inert")
            return
        ctx.hooks.register(
            HookName.BEFORE_TOOL_CALL,
            self._gate,
            plugin_name="budget-guard",
            priority=10,  # run early so cheap plugins can react to the veto
        )
        ctx.log.info(
            "budget-guard armed — %d expensive tools, budget %d/process",
            len(_EXPENSIVE_TOOLS),
            self._budget,
        )

    async def _gate(self, ctx: HookContext) -> None:
        if ctx.tool_name not in _EXPENSIVE_TOOLS:
            return
        n = self._counts.get(ctx.tool_name, 0) + 1
        self._counts[ctx.tool_name] = n
        if n > self._budget:
            ctx.veto = True
            ctx.veto_reason = (
                f"budget-guard: {ctx.tool_name} exceeded {self._budget}/process budget "
                f"(call #{n})"
            )
            if self._log:
                self._log.info(ctx.veto_reason)

    def shutdown(self) -> None:
        # Hook handlers are torn down by PluginManager via hooks.unregister_plugin
        # — nothing to do here. Counts are in-memory and die with the process.
        pass


plugin = BudgetGuard()
