"""Raw OpenOCD command escape hatch with safety deny-list."""

import logging
import os

from fastmcp import Context
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
from openocd.errors import OpenOCDError

from mcjtag.models import RawCommandResult
from mcjtag.state import SessionState

log = logging.getLogger(__name__)

# C1: Deny-list of destructive OpenOCD commands.
# These must be accessed through the dedicated typed tools which have
# proper validation and safety checks.
_DANGEROUS_PATTERNS = [
    "flash erase",
    "flash write",
    "flash protect",
    "flash fill",
    "mww ",
    "mwh ",
    "mwb ",
    "write_memory ",
    "shutdown",
    "exit",
    "reset",
    "reg ",
    # TCL metacommands that could bypass pattern matching
    "eval ",
    "subst ",
    "uplevel ",
]


def _is_dangerous(command: str) -> str | None:
    """Return the matched dangerous pattern, or None if safe."""
    cmd_lower = command.lower().strip()
    for pattern in _DANGEROUS_PATTERNS:
        if pattern in cmd_lower:
            return pattern
    return None


def _get_state(ctx: Context) -> SessionState:
    return ctx.request_context.lifespan_context


class RawMixin(MCPMixin):
    """Escape hatch for sending arbitrary OpenOCD TCL commands.

    A deny-list blocks destructive commands by default. Override with
    MCJTAG_ALLOW_RAW=true to allow everything (not recommended for
    production use with safety-critical targets).
    """

    @mcp_tool()
    async def raw_command(self, ctx: Context, command: str) -> RawCommandResult:
        """Send a raw OpenOCD TCL command string and return the text response.

        This is an escape hatch for OpenOCD commands not covered by the
        dedicated tools. A deny-list blocks commands that could damage the
        target or disrupt the debug session. This is NOT a security sandbox
        -- TCL metaprogramming (eval, subst, uplevel) could bypass the
        pattern matching, which is why those are also blocked.

        Blocked command patterns and why:
        - "flash erase/write/protect/fill" -- use flash_program instead,
          which validates file format, size, and reports progress.
        - "mww/mwh/mwb", "write_memory" -- use write_memory instead,
          which enforces safe-write-range checks (SRAM-only by default).
        - "reg " -- use read_registers/write_register instead, which
          return structured data.
        - "reset" -- use target_control(action="reset_halt") or
          target_control(action="reset_run") instead.
        - "shutdown", "exit" -- would kill the OpenOCD process and drop
          the debug session. Use disconnect instead.
        - "eval", "subst", "uplevel" -- TCL metacommands that could
          construct blocked commands dynamically.

        To bypass the deny-list, set env var MCJTAG_ALLOW_RAW=true.

        Safe and useful commands (examples):
        - "version" -- OpenOCD version string
        - "adapter speed" -- current clock speed in kHz
        - "adapter name" -- probe/adapter identification
        - "targets" -- list configured targets and their state
        - "flash banks" -- list flash bank topology
        - "flash info 0" -- detailed sector map for flash bank 0
        - "dap info" -- DAP and AP enumeration
        - "poll" -- target poll status
        - "$target_name cget -work-area-size" -- query target config
        - "transport select" -- active transport (swd/jtag)

        Args:
            command: OpenOCD TCL command string. Passed directly to the
                     TCL interpreter. Multi-command TCL scripts work if
                     MCJTAG_ALLOW_RAW is enabled.
        """
        state = _get_state(ctx)
        if not state.session:
            return RawCommandResult(
                command=command,
                response=("Error: Not connected to OpenOCD. Use connect or start_openocd first."),
            )

        # C1: Block destructive commands unless explicitly allowed
        allow_raw = os.environ.get("MCJTAG_ALLOW_RAW", "").lower() in (
            "true",
            "1",
            "yes",
        )
        if not allow_raw:
            matched = _is_dangerous(command)
            if matched:
                return RawCommandResult(
                    command=command,
                    response=(
                        f"Blocked: '{command}' matches destructive pattern "
                        f"'{matched.strip()}'. Use the dedicated tool instead, "
                        f"or set MCJTAG_ALLOW_RAW=true to bypass safety checks."
                    ),
                )

        try:
            response = await state.session.command(command)
        except OpenOCDError as exc:
            response = f"Error: {exc}"

        return RawCommandResult(command=command, response=response)
