"""Opt-in quality gates (#256): plan-gate / tdd-gate / scope-guard presets run
via `rstack-agents gate <name>` on the write/edit shadows, AFTER guard. OFF by
default; only tdd-gate ever blocks, and it is always overridable
(RSTACK_ALLOW_NO_TESTS=1 or an audited approval), so a gate can never dead-end
a session.

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

import asyncio
import json
import os
import sys

from .config import _timeout_from_env
from .proc import _communicate_or_kill, _own_process_group_kwargs
from .resolve import _resolve_bin_argv

# Opt-in quality gates (#256). OFF by default; enabled via the extension setting
# `quality_gates` (comma string or list of plan-gate|tdd-gate|scope-guard) or the
# RSTACK_TAU_GATES env. Only file-write/edit tools are gated (not `terminal`),
# and both happen to use "path" as both the gate field and Tau's own param name.
_GATE_TOOL_MAP = {
    "write": ("Write", "path", "path"),
    "edit": ("Edit", "path", "path"),
}
_KNOWN_GATES = ("plan-gate", "tdd-gate", "scope-guard")


def _resolve_gates() -> list[str]:
    """Ordered, de-duped list of enabled quality-gate presets. Env/settings both
    accepted; unknown names dropped. Returns [] when gates are off (the default)."""
    raw = os.environ.get("RSTACK_TAU_GATES", "")
    wanted = {g.strip().lower() for g in raw.split(",") if g.strip()}
    # Normalize short aliases (tdd → tdd-gate, scope → scope-guard).
    normalized: set[str] = set()
    for g in wanted:
        if g in _KNOWN_GATES:
            normalized.add(g)
        elif g == "scope":
            normalized.add("scope-guard")
        elif g in ("plan", "tdd"):
            normalized.add(f"{g}-gate")
    return [g for g in _KNOWN_GATES if g in normalized]


# Gates may run a real test lookup (tdd-gate), so they get a looser bound
# than guard while staying far under the bridge's control-plane bound.
_GATE_TIMEOUT_MS = 30_000


def _gate_timeout_s() -> float:
    """Hard bound on a single opt-in quality-gate invocation (#487).
    RSTACK_GATE_TIMEOUT_MS to override, default 30s."""
    return _timeout_from_env("RSTACK_GATE_TIMEOUT_MS", _GATE_TIMEOUT_MS)


async def _run_gate(gate_name: str, guard_tool_name: str, tool_input: dict, cwd: str) -> tuple[bool, str]:
    """Run one OPT-IN quality gate via `rstack-agents gate <name>` (#256).

    Same contract as _run_guard: exit 2 blocks (returns False + reason), any
    other exit allows. Only tdd-gate ever blocks; it is always overridable
    (RSTACK_ALLOW_NO_TESTS=1 or an audited approval), so this can never
    dead-end a session. Fails OPEN when npx is unreachable.
    """
    # G-09 (#552): local binary preferred over `npx --yes` — see resolve.py.
    # Gates keep their documented fail-OPEN posture when nothing resolves.
    argv_prefix, _needs_network = _resolve_bin_argv("rstack-agents", cwd)
    if argv_prefix is None:
        return True, ""
    payload = json.dumps({"tool_name": guard_tool_name, "tool_input": tool_input}).encode("utf-8")
    try:
        proc = await asyncio.create_subprocess_exec(
            *argv_prefix, "gate", gate_name, "--project", cwd,
            stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
            env=os.environ, cwd=cwd,
            **_own_process_group_kwargs(),
        )
    except OSError as exc:
        # #487 (CodeRabbit review): a spawn failure is the same "cannot
        # determine" class as no-npx / a timeout above — fail open, matching
        # this function's documented contract, instead of propagating.
        print(f"[rstack] gate '{gate_name}' could not spawn ({exc}) — allowing (gates fail open)", file=sys.stderr)
        return True, ""
    # #487 (audit finding, verbatim): "_run_gate — invoked once per enabled
    # quality-gate preset... same unbounded communicate()." A gate is
    # opt-in and already documented to fail OPEN when npx is unreachable
    # (see the docstring above) — a genuine timeout (npx present but hung)
    # is the same "cannot determine" class, so it fails open too, exactly
    # like the no-npx case, rather than introducing a new blocking behavior
    # this function was never designed to have.
    try:
        out, err = await _communicate_or_kill(proc, payload, _gate_timeout_s())
    except asyncio.TimeoutError:
        print(f"[rstack] gate '{gate_name}' timed out after {_gate_timeout_s():.0f}s (RSTACK_GATE_TIMEOUT_MS) — allowing (gates fail open, matching the no-npx behavior)", file=sys.stderr)
        return True, ""
    if proc.returncode == 2:
        reason = (
            err.decode("utf-8", "replace").strip()
            or out.decode("utf-8", "replace").strip()
            or f"RStack {gate_name} blocked this tool call."
        )
        return False, reason
    return True, ""
