"""The `register(tau)` entry point: wires bridge tools, guarded built-in
shadows, the /sdlc command, and the real (verified-live) Tau hooks.

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

import os
import sys

from tau.hooks import InputEventResult

from .bridge import _BridgeTool
from .capability import TIER_UNIVERSAL, detect_tool_call_tier
from .commands import _make_sdlc_command, _sdlc_argument_completions
from .config import _CONFIG_ENV
from .context import _fetch_context
from .gates import _resolve_gates
from .guarded_tools import _GUARDED_BUILTINS, _GuardedBuiltinTool
from .hub import _launch_business_hub
from .observe import _observe_bg
from .params import _TOOLS
from .tool_gate import _make_tool_call_gate


def register(tau) -> None:
    # G-08 (#552): the companion Hub launch runs HERE, not at module import —
    # register() is the real "a Tau session loaded this extension" signal (Tau
    # has no session-start hook). Best-effort; RSTACK_NO_BUSINESS_HUB=1 or CI
    # skips it entirely. Worker pools importing the adapter never launch it.
    _launch_business_hub()

    cfg = tau.config or {}
    config_env = {
        env: str(cfg[key]) for key, env in _CONFIG_ENV.items() if cfg.get(key) is not None
    }

    for name, (description, model) in _TOOLS.items():
        tau.register_tool(_BridgeTool(name, description, model, config_env))

    # Opt-in quality gates (#256): a `quality_gates` extension setting is merged
    # into RSTACK_TAU_GATES so `_resolve_gates()` (env-driven) sees it. OFF
    # unless configured — the default returns [] and _GuardedBuiltinTool.execute
    # runs guard only, exactly as before.
    _gates_setting = cfg.get("quality_gates")
    if _gates_setting is not None:
        if isinstance(_gates_setting, (list, tuple)):
            os.environ["RSTACK_TAU_GATES"] = ",".join(str(g) for g in _gates_setting)
        else:
            os.environ["RSTACK_TAU_GATES"] = str(_gates_setting)
    enabled_gates = _resolve_gates()

    # Enforcement, layer 1 of 2: shadow the three built-ins the destructive-action
    # gate needs to see — read/glob/grep/ls stay the real built-ins, unshadowed,
    # since they're read-only and never routed through guard. These stay
    # registered on EVERY Tau, including one whose tool_call hook works: a hook
    # handler that raises is swallowed upstream and read as consent, so the
    # shadows are never traded away for it (#575).
    for guard_tool_name, guard_field, tau_param_field, inner_factory in _GUARDED_BUILTINS.values():
        tau.register_tool(_GuardedBuiltinTool(
            inner_factory(), guard_tool_name, guard_field, tau_param_field, lambda: enabled_gates,
        ))

    tau.register_command(
        "sdlc",
        "Run an RStack SDLC subcommand (start, plan, status, approve, ...).",
        _make_sdlc_command(config_env),
        get_argument_completions=_sdlc_argument_completions,
        argument_hint="<subcommand> [text | {json}]",
    )

    # Enforcement, layer 2 of 2: the UNIVERSAL gate. Tau's `tool_call` hook
    # fires for every tool the engine executes, so this is the only mechanism
    # that can see an MCP server's tools or another extension's tools — the G-07
    # residue #552 recorded as "outside the guard until upstream ships T-01".
    #
    # Installed ONLY when the running Tau actually honors an extension's block.
    # On released 0.9.2 the return value is discarded, and a gate whose verdict
    # is thrown away is worse than no gate: it looks like enforcement. The tier
    # is probed behaviorally because no version string can answer it — HEAD and
    # the v0.9.2 tag both declare 0.9.2 (see capability.py).
    tier = detect_tool_call_tier()
    if tier == TIER_UNIVERSAL:
        tau.on("tool_call", _make_tool_call_gate(lambda: enabled_gates))
    else:
        print(
            f"[rstack] Tau tool_call gate NOT installed (capability: {tier}). Enforcement is "
            "the three built-in shadows only — MCP and extension tools run UNGUARDED. "
            "Upgrade to a Tau release that honors an extension's tool_call block.",
            file=sys.stderr,
        )

    _register_hooks(tau)


def _register_hooks(tau) -> None:
    """Wire the real (verified-live) Tau hooks: observability + context."""

    @tau.on("tool_result")
    async def _rstack_observe_result(event, ctx):
        # Post-execution observability (#251): Tau exposes a real tool_result
        # hook, so we record the outcome (source="tau") exactly like Pi's
        # tool_result event. Fire-and-forget (#253) — off the critical path, so
        # the result is returned immediately and a slow write adds no latency.
        _observe_bg(
            {
                "tool_name": event.tool_name,
                "hook_event_name": "PostToolUse",
                "content": event.content,
                "is_error": bool(getattr(event, "is_error", False)),
            },
            str(ctx.cwd),
        )
        return None

    @tau.on("tool_execution_failure")
    async def _rstack_observe_failure(event, ctx):
        # Tool crashed (uncaught exception, distinct from a returned error): record
        # an error tool_result so failures show in the Business Hub feed. (#255)
        # Fire-and-forget — off the critical path, never disrupts the session.
        _observe_bg(
            {
                "tool_name": getattr(event, "tool_name", "") or "",
                "hook_event_name": "PostToolUseFailure",
                "content": getattr(event, "error", "") or "",
                "is_error": True,
            },
            str(ctx.cwd),
        )
        return None

    @tau.on("before_compaction")
    async def _rstack_observe_compaction(event, ctx):
        # Context is about to be trimmed — record a context_preserved event
        # (ties to the context-pressure work). trigger mirrors Claude Code's
        # PreCompact "manual"/"auto". (#255) Fire-and-forget.
        trigger = "manual" if bool(getattr(event, "manual", False)) else "auto"
        _observe_bg(
            {"hook_event_name": "PreCompact", "trigger": trigger},
            str(ctx.cwd),
        )
        return None

    @tau.on("input")
    async def _rstack_inject_context(event, ctx):
        # Context injection (#255) — via the real `input` hook, not the dead
        # `before_agent_start` (see the package docstring). `input` fires for
        # every submitted prompt and a transform result genuinely replaces the
        # text the agent receives, verified live against upstream Tau. This is
        # the practical analog of Claude Code's UserPromptSubmit injection.
        # Restricted to interactive/rpc sources — a real external actor
        # issuing a new instruction — so a delegated subagent or an internal
        # cron/goal/queue turn never gets a duplicate/nested packet prepended.
        # Best-effort + hard-timeout-bounded (see _fetch_context): any failure
        # returns no transform and the turn proceeds with the original text.
        # This hook can NEVER block a turn — it only augments the prompt text.
        source = getattr(event, "source", None)
        if source not in ("interactive", "rpc"):
            return None
        try:
            packet = await _fetch_context(str(ctx.cwd))
        except Exception:
            return None
        if not packet:
            return None
        base = getattr(event, "text", "") or ""
        combined = f"{packet}\n\n{base}" if base else packet
        return InputEventResult(action="transform", text=combined)
