"""Enforcement via built-in shadowing (see the package docstring for why the
documented `tool_call` hook is dead in upstream Tau): `_GuardedBuiltinTool`
wraps the real write/terminal/edit built-ins and runs `rstack-agents guard`
(and opt-in quality gates) BEFORE delegating to the real tool.

owner: RStack developed by Richardson Gunde; Tau adapter contributed by Jeomon
"""
from __future__ import annotations

import os
from typing import Optional

from tau.builtins.tools import create_edit_tool, create_terminal_tool, create_write_tool
from tau.tool.types import Tool, ToolContext, ToolInvocation, ToolResult

from .gates import _GATE_TOOL_MAP, _run_gate
from .guard import _run_guard
from .observe import _observe_bg

# Tau built-in tool name → (guard tool_name, guard payload field, Tau's OWN
# param field carrying the same value, factory for a fresh instance of the
# real tool to delegate to). The guard's field name and Tau's own schema
# field name are NOT always the same word for the same value — Tau's
# TerminalParams calls it `cmd`, but `rstack-agents guard` (matching Claude
# Code's own Bash tool_input shape) expects `command`; a live smoke test
# against the real installed tau-coding-agent package caught this exact
# mismatch (guard silently saw an empty string and allowed everything).
# `read`, `glob`, `grep`, `ls` are read-only and are not shadowed/guarded.
_GUARDED_BUILTINS = {
    "terminal": ("Bash", "command", "cmd", create_terminal_tool),
    "write": ("Write", "path", "path", create_write_tool),
    "edit": ("Edit", "path", "path", create_edit_tool),
}


class _GuardedBuiltinTool(Tool):
    """Shadows a Tau built-in tool (write/terminal/edit) to enforce
    `rstack-agents guard` (and opt-in quality gates) BEFORE execution.

    This is the real enforcement mechanism (see the package docstring for why
    the documented `tool_call` hook is dead in upstream Tau). Extension tools
    with the same name as a built-in shadow it while loaded — this class
    wraps a fresh instance of the real tool (via the `tau.builtins.tools`
    `create_*_tool()` factories) and forwards every render/prompt hint so the
    TUI experience is byte-for-byte the same as the un-shadowed built-in;
    only the pre-execution gate is added.
    """

    def __init__(
        self,
        inner: Tool,
        guard_tool_name: str,
        guard_field: str,
        tau_param_field: str,
        enabled_gates_getter,
    ):
        super().__init__(
            name=inner.name,
            description=inner.description,
            schema=inner.schema,
            kind=inner.kind,
            execution_mode=inner.execution_mode,
            render_call=inner.render_call,
            render_result=inner.render_result,
            render_shell=inner.render_shell,
            result_expandable=inner.result_expandable,
            result_preview_lines=inner.result_preview_lines,
            prompt_snippet=inner.prompt_snippet,
            prompt_guidelines=inner.prompt_guidelines,
            prepare_arguments=inner.prepare_arguments,
        )
        self._inner = inner
        self._guard_tool_name = guard_tool_name
        self._guard_field = guard_field
        self._tau_param_field = tau_param_field
        self._enabled_gates_getter = enabled_gates_getter

    async def execute(
        self,
        invocation: ToolInvocation,
        tool_execution_update_callback=None,
        signal=None,
        context: Optional[ToolContext] = None,
    ) -> ToolResult:
        cwd = str(getattr(context, "cwd", None) or os.getcwd())
        params = invocation.params or {}
        # Read the value using TAU's own param field name, but key it under
        # the GUARD's expected field name when building its payload — these
        # differ for terminal (Tau: `cmd`, guard: `command`).
        target_value = params.get(self._tau_param_field, "")
        guard_input = {self._guard_field: target_value}

        # Observability (#251): emit the INTENT even when the call is later
        # blocked, so it still shows in the dashboard. Fire-and-forget (#253)
        # — off the critical path, never adds latency to the real tool call.
        _observe_bg(
            {"tool_name": self.name, "tool_input": guard_input, "hook_event_name": "PreToolUse"},
            cwd,
        )

        allowed, reason = await _run_guard(self._guard_tool_name, guard_input, cwd)
        if not allowed:
            return ToolResult.error(invocation.id, reason)

        gate_mapping = _GATE_TOOL_MAP.get(self.name)
        enabled_gates = self._enabled_gates_getter()
        if enabled_gates and gate_mapping is not None:
            gate_tool_name, gate_field, gate_tau_field = gate_mapping
            gate_input = {gate_field: params.get(gate_tau_field, "")}
            for gate_name in enabled_gates:
                ok, gate_reason = await _run_gate(gate_name, gate_tool_name, gate_input, cwd)
                if not ok:
                    return ToolResult.error(invocation.id, gate_reason)

        return await self._inner.execute(invocation, tool_execution_update_callback, signal, context)
