"""MCP Config Guard — Security scanner for MCP configuration files.

Scans MCP configuration files (claude_desktop_config.json, .claude/settings.json,
etc.) for security misconfigurations: shell injection risks, exposed secrets,
overly permissive settings, and more.

Zero external dependencies. Part of mcpaudit v0.8.0.

Usage:
    mcpaudit guard /path/to/claude_desktop_config.json
    mcpaudit guard /path/to/config.json --format json
    mcpaudit guard /path/to/config.json --format sarif
"""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from .models import Finding, Severity


# --- Guard Rule definitions ---

@dataclass
class GuardRule:
    """A single config guard rule."""

    rule_id: str
    name: str
    description: str
    severity: Severity
    cwe: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "rule_id": self.rule_id,
            "name": self.name,
            "description": self.description,
            "severity": self.severity.value,
            "cwe": self.cwe,
        }


# Patterns for sensitive env var names
_SENSITIVE_ENV_PATTERNS: list[re.Pattern[str]] = [
    re.compile(r"(?i)(api[_-]?key|secret|token|password|passwd|credential|auth)"),
    re.compile(r"(?i)(aws[_-]?access|aws[_-]?secret|private[_-]?key)"),
    re.compile(r"(?i)(database[_-]?url|db[_-]?pass|connection[_-]?string)"),
    re.compile(r"(?i)(github[_-]?token|npm[_-]?token|slack[_-]?token)"),
    re.compile(r"(?i)(openai[_-]?api|anthropic[_-]?api|claude[_-]?api)"),
]

# Trusted binaries that are commonly used and considered safe
_TRUSTED_BINARIES: set[str] = {
    "node", "python", "python3", "npx", "uvx", "uv", "pip", "pip3",
    "docker", "podman", "deno", "bun",
}

# Shell-like commands that indicate shell injection risk
_SHELL_COMMANDS: set[str] = {"bash", "sh", "zsh", "cmd", "cmd.exe", "powershell", "pwsh"}


def _get_all_guard_rules() -> list[GuardRule]:
    """Return all built-in guard rules."""
    return [
        GuardRule(
            rule_id="GUARD-001",
            name="Shell injection risk in command",
            description=(
                "Server uses a shell command (bash -c, sh -c) which allows "
                "arbitrary command injection via argument interpolation."
            ),
            severity=Severity.CRITICAL,
            cwe="CWE-78",
        ),
        GuardRule(
            rule_id="GUARD-002",
            name="Unverified absolute path to binary",
            description=(
                "Server uses an absolute path to a binary that is not in the "
                "standard trusted binary set. Verify the binary is legitimate."
            ),
            severity=Severity.HIGH,
            cwe="CWE-426",
        ),
        GuardRule(
            rule_id="GUARD-003",
            name="Missing authentication configuration",
            description=(
                "Server does not have any authentication token or API key configured. "
                "This may allow unauthorized access."
            ),
            severity=Severity.HIGH,
            cwe="CWE-306",
        ),
        GuardRule(
            rule_id="GUARD-004",
            name="Port exposed on all interfaces",
            description=(
                "Server listens on 0.0.0.0, exposing the port to all network "
                "interfaces without restriction."
            ),
            severity=Severity.HIGH,
            cwe="CWE-668",
        ),
        GuardRule(
            rule_id="GUARD-005",
            name="Sensitive environment variable exposed in args",
            description=(
                "A sensitive environment variable (API key, token, password) "
                "is passed directly in command arguments, risking exposure in "
                "process listings and logs."
            ),
            severity=Severity.CRITICAL,
            cwe="CWE-200",
        ),
        GuardRule(
            rule_id="GUARD-006",
            name="Duplicate server name (shadow config)",
            description=(
                "Multiple servers share the same name. The later definition "
                "silently shadows the earlier one, which may hide malicious "
                "config injection."
            ),
            severity=Severity.HIGH,
            cwe="CWE-694",
        ),
        GuardRule(
            rule_id="GUARD-007",
            name="Overly permissive tool allowlist",
            description=(
                "Server has allowedTools set to wildcard ('*'), granting access "
                "to ALL tools without restriction. Use an explicit allowlist."
            ),
            severity=Severity.CRITICAL,
            cwe="CWE-250",
        ),
        GuardRule(
            rule_id="GUARD-008",
            name="Writable path mounted without restriction",
            description=(
                "Server has a writable host path mounted (e.g., home directory, "
                "/tmp, or root /) that could allow data exfiltration or tampering."
            ),
            severity=Severity.HIGH,
            cwe="CWE-732",
        ),
        GuardRule(
            rule_id="GUARD-009",
            name="Environment variable with inline secret value",
            description=(
                "An environment variable with a sensitive name has its value "
                "hardcoded directly in the config instead of referencing a "
                "secret store or env var."
            ),
            severity=Severity.CRITICAL,
            cwe="CWE-798",
        ),
        GuardRule(
            rule_id="GUARD-010",
            name="Stdio transport with shell glob or pipe",
            description=(
                "Server command args contain shell metacharacters (|, >, <, ;, &, `) "
                "that may cause unexpected shell interpretation."
            ),
            severity=Severity.HIGH,
            cwe="CWE-78",
        ),
    ]


# Lookup by rule_id
_GUARD_RULES_MAP: dict[str, GuardRule] = {r.rule_id: r for r in _get_all_guard_rules()}


# --- Finding generators per rule ---


def _check_shell_injection(server_name: str, server_cfg: dict[str, Any],
                           config_path: str) -> list[Finding]:
    """GUARD-001: Detect shell command usage (bash -c, sh -c, etc.)."""
    findings: list[Finding] = []
    command = server_cfg.get("command", "")
    args = server_cfg.get("args", [])

    # Check if command itself is a shell
    cmd_base = Path(command).name if command else ""
    if cmd_base in _SHELL_COMMANDS:
        # Check for -c flag in args (shell injection pattern)
        if isinstance(args, list) and "-c" in args:
            rule = _GUARD_RULES_MAP["GUARD-001"]
            findings.append(Finding(
                rule_id="GUARD-001",
                severity=rule.severity,
                title=rule.name,
                description=(
                    f"Server '{server_name}' uses {cmd_base} -c with inline script. "
                    f"This allows arbitrary command execution."
                ),
                file=config_path,
                line=0,
                code_snippet=f'command: {command}, args: {args}',
                recommendation="Use a dedicated binary or script instead of shell -c.",
                cwe=rule.cwe,
            ))
        elif isinstance(args, list):
            # Shell without -c is still suspicious
            rule = _GUARD_RULES_MAP["GUARD-001"]
            findings.append(Finding(
                rule_id="GUARD-001",
                severity=Severity.HIGH,
                title=rule.name,
                description=(
                    f"Server '{server_name}' uses shell command '{cmd_base}'. "
                    f"Consider using a direct binary instead."
                ),
                file=config_path,
                line=0,
                code_snippet=f'command: {command}',
                recommendation="Invoke the target binary directly instead of via a shell.",
                cwe=rule.cwe,
            ))

    return findings


def _check_unverified_path(server_name: str, server_cfg: dict[str, Any],
                           config_path: str) -> list[Finding]:
    """GUARD-002: Detect absolute paths to unverified binaries."""
    findings: list[Finding] = []
    command = server_cfg.get("command", "")

    if not command:
        return findings

    # Check if command is an absolute path
    if command.startswith("/") or (len(command) > 2 and command[1] == ":"):
        binary_name = Path(command).stem
        if binary_name not in _TRUSTED_BINARIES:
            rule = _GUARD_RULES_MAP["GUARD-002"]
            findings.append(Finding(
                rule_id="GUARD-002",
                severity=rule.severity,
                title=rule.name,
                description=(
                    f"Server '{server_name}' references absolute path '{command}' "
                    f"which is not in the trusted binary list."
                ),
                file=config_path,
                line=0,
                code_snippet=f'command: {command}',
                recommendation=(
                    "Verify the binary is legitimate and consider using a "
                    "relative name from PATH instead."
                ),
                cwe=rule.cwe,
            ))

    return findings


def _check_missing_auth(server_name: str, server_cfg: dict[str, Any],
                        config_path: str) -> list[Finding]:
    """GUARD-003: Detect servers without authentication configuration."""
    findings: list[Finding] = []

    env = server_cfg.get("env", {})
    args = server_cfg.get("args", [])
    args_str = " ".join(str(a) for a in args) if isinstance(args, list) else str(args)

    # Check if there's any auth-related config
    has_auth = False
    auth_patterns = re.compile(r"(?i)(token|key|auth|secret|password|bearer|credential)")

    # Check env vars
    if isinstance(env, dict):
        for key in env:
            if auth_patterns.search(key):
                has_auth = True
                break

    # Check args
    if not has_auth and auth_patterns.search(args_str):
        has_auth = True

    # Check for URL transport with auth
    url = server_cfg.get("url", "")
    if not has_auth and url and "@" in url:
        has_auth = True

    if not has_auth:
        rule = _GUARD_RULES_MAP["GUARD-003"]
        findings.append(Finding(
            rule_id="GUARD-003",
            severity=rule.severity,
            title=rule.name,
            description=(
                f"Server '{server_name}' has no authentication token or key "
                f"configured. This may allow unauthenticated access."
            ),
            file=config_path,
            line=0,
            code_snippet=f'server: {server_name}',
            recommendation=(
                "Add an authentication token via env vars or args. "
                "Example: env: {{\"API_TOKEN\": \"...\"}}"
            ),
            cwe=rule.cwe,
        ))

    return findings


def _check_exposed_port(server_name: str, server_cfg: dict[str, Any],
                        config_path: str) -> list[Finding]:
    """GUARD-004: Detect ports exposed on all interfaces (0.0.0.0)."""
    findings: list[Finding] = []

    # Serialize entire config to search for 0.0.0.0 references
    cfg_str = json.dumps(server_cfg)

    if "0.0.0.0" in cfg_str:
        rule = _GUARD_RULES_MAP["GUARD-004"]
        findings.append(Finding(
            rule_id="GUARD-004",
            severity=rule.severity,
            title=rule.name,
            description=(
                f"Server '{server_name}' exposes a port on 0.0.0.0 (all interfaces). "
                f"This makes the server accessible from any network."
            ),
            file=config_path,
            line=0,
            code_snippet=f'Detected 0.0.0.0 in server config',
            recommendation=(
                "Bind to 127.0.0.1 (localhost) or a specific interface instead of 0.0.0.0."
            ),
            cwe=rule.cwe,
        ))

    return findings


def _check_sensitive_env_in_args(server_name: str, server_cfg: dict[str, Any],
                                 config_path: str) -> list[Finding]:
    """GUARD-005: Detect sensitive env vars passed in args."""
    findings: list[Finding] = []
    args = server_cfg.get("args", [])

    if not isinstance(args, list):
        return findings

    for arg in args:
        arg_str = str(arg)
        for pattern in _SENSITIVE_ENV_PATTERNS:
            if pattern.search(arg_str):
                rule = _GUARD_RULES_MAP["GUARD-005"]
                findings.append(Finding(
                    rule_id="GUARD-005",
                    severity=rule.severity,
                    title=rule.name,
                    description=(
                        f"Server '{server_name}' passes sensitive value in args: "
                        f"'{arg_str[:60]}...'. This is visible in process listings."
                    ),
                    file=config_path,
                    line=0,
                    code_snippet=f'args contain: {arg_str[:80]}',
                    recommendation=(
                        "Pass secrets via environment variables instead of command args."
                    ),
                    cwe=rule.cwe,
                ))
                break  # One finding per arg

    return findings


def _check_duplicate_servers(servers: dict[str, Any],
                             config_path: str) -> list[Finding]:
    """GUARD-006: Detect duplicate server names."""
    # Note: In standard JSON parsing, duplicate keys are silently overwritten.
    # This check detects duplicates in the raw JSON text.
    # Since we receive parsed dict, duplicates are already resolved.
    # We detect this by reading the raw text if available.
    # For now, we check the parsed dict (no duplicates possible).
    # Real duplicate detection happens in scan_config_text().
    return []


def _check_duplicate_servers_raw(raw_text: str,
                                 config_path: str) -> list[Finding]:
    """GUARD-006: Detect duplicate server names in raw JSON text."""
    findings: list[Finding] = []

    # Find all "mcpServers" keys and their server names
    server_names: list[str] = []
    # Find mcpServers block start, extract only depth-1 keys
    mcp_start = re.search(r'"mcpServers"\s*:\s*\{', raw_text)
    if mcp_start:
        text = raw_text[mcp_start.end():]
        depth = 1
        i = 0
        # Collect characters only at depth 1 (direct children of mcpServers)
        depth1_chars: list[str] = []
        while i < len(text) and depth > 0:
            ch = text[i]
            if ch == '{':
                if depth == 1:
                    depth1_chars.append(ch)
                depth += 1
            elif ch == '}':
                depth -= 1
                if depth == 0:
                    break
            elif depth == 1:
                depth1_chars.append(ch)
            i += 1
        block = ''.join(depth1_chars)
        name_pattern = re.compile(r'"([^"]+)"\s*:\s*\{')
        names = name_pattern.findall(block)
        server_names.extend(names)

    # Check for duplicates
    seen: dict[str, int] = {}
    for name in server_names:
        seen[name] = seen.get(name, 0) + 1

    for name, count in seen.items():
        if count > 1:
            rule = _GUARD_RULES_MAP["GUARD-006"]
            findings.append(Finding(
                rule_id="GUARD-006",
                severity=rule.severity,
                title=rule.name,
                description=(
                    f"Server name '{name}' appears {count} times. "
                    f"Later definitions shadow earlier ones."
                ),
                file=config_path,
                line=0,
                code_snippet=f'Duplicate server: {name} (x{count})',
                recommendation=(
                    "Ensure each server has a unique name to prevent shadow configs."
                ),
                cwe=rule.cwe,
            ))

    return findings


def _check_wildcard_tools(server_name: str, server_cfg: dict[str, Any],
                          config_path: str) -> list[Finding]:
    """GUARD-007: Detect overly permissive tool allowlists."""
    findings: list[Finding] = []

    allowed_tools = server_cfg.get("allowedTools", None)
    if allowed_tools is None:
        # Also check nested settings
        settings = server_cfg.get("settings", {})
        if isinstance(settings, dict):
            allowed_tools = settings.get("allowedTools", None)

    if isinstance(allowed_tools, list):
        for tool in allowed_tools:
            if tool == "*" or tool == "mcp__*" or (isinstance(tool, str) and tool.endswith("*")):
                rule = _GUARD_RULES_MAP["GUARD-007"]
                findings.append(Finding(
                    rule_id="GUARD-007",
                    severity=rule.severity,
                    title=rule.name,
                    description=(
                        f"Server '{server_name}' has wildcard in allowedTools: '{tool}'. "
                        f"This grants access to all matching tools."
                    ),
                    file=config_path,
                    line=0,
                    code_snippet=f'allowedTools: {allowed_tools}',
                    recommendation=(
                        "Use an explicit list of allowed tools instead of wildcards."
                    ),
                    cwe=rule.cwe,
                ))
    elif isinstance(allowed_tools, str) and allowed_tools == "*":
        rule = _GUARD_RULES_MAP["GUARD-007"]
        findings.append(Finding(
            rule_id="GUARD-007",
            severity=rule.severity,
            title=rule.name,
            description=(
                f"Server '{server_name}' has allowedTools set to '*'. "
                f"This grants unrestricted tool access."
            ),
            file=config_path,
            line=0,
            code_snippet=f'allowedTools: "*"',
            recommendation="Specify an explicit list of allowed tools.",
            cwe=rule.cwe,
        ))

    return findings


def _check_writable_paths(server_name: str, server_cfg: dict[str, Any],
                          config_path: str) -> list[Finding]:
    """GUARD-008: Detect writable host paths mounted."""
    findings: list[Finding] = []

    dangerous_paths = {"/", "/home", "/tmp", "/var", "/etc", "/root"}

    # Check args for mounted paths
    args = server_cfg.get("args", [])
    cfg_str = json.dumps(server_cfg)

    for dpath in dangerous_paths:
        # Check if dangerous path appears in config (as volume mount, working dir, etc.)
        if f'"{dpath}"' in cfg_str or f"'{dpath}'" in cfg_str:
            rule = _GUARD_RULES_MAP["GUARD-008"]
            findings.append(Finding(
                rule_id="GUARD-008",
                severity=rule.severity,
                title=rule.name,
                description=(
                    f"Server '{server_name}' references writable path '{dpath}'. "
                    f"This could allow data exfiltration or tampering."
                ),
                file=config_path,
                line=0,
                code_snippet=f'Path reference: {dpath}',
                recommendation=(
                    f"Restrict mounted paths to specific subdirectories "
                    f"instead of '{dpath}'."
                ),
                cwe=rule.cwe,
            ))

    return findings


def _check_hardcoded_secrets(server_name: str, server_cfg: dict[str, Any],
                             config_path: str) -> list[Finding]:
    """GUARD-009: Detect hardcoded secret values in env vars."""
    findings: list[Finding] = []
    env = server_cfg.get("env", {})

    if not isinstance(env, dict):
        return findings

    for key, value in env.items():
        # Check if key looks sensitive
        is_sensitive = False
        for pattern in _SENSITIVE_ENV_PATTERNS:
            if pattern.search(key):
                is_sensitive = True
                break

        if not is_sensitive:
            continue

        value_str = str(value)

        # Check if value looks like a hardcoded secret (not a reference)
        # References look like: ${VAR}, $VAR, env:VAR, etc.
        is_reference = (
            value_str.startswith("${") or
            value_str.startswith("$") or
            value_str.startswith("env:") or
            value_str == "" or
            value_str.startswith("{{")
        )

        if not is_reference and len(value_str) > 3:
            rule = _GUARD_RULES_MAP["GUARD-009"]
            findings.append(Finding(
                rule_id="GUARD-009",
                severity=rule.severity,
                title=rule.name,
                description=(
                    f"Server '{server_name}' has hardcoded value for sensitive "
                    f"env var '{key}'. Store secrets in a vault or env file."
                ),
                file=config_path,
                line=0,
                code_snippet=f'{key}: {"*" * min(len(value_str), 8)}',
                recommendation=(
                    "Use environment variable references (${VAR}) or a secret "
                    "manager instead of hardcoded values."
                ),
                cwe=rule.cwe,
            ))

    return findings


def _check_shell_metacharacters(server_name: str, server_cfg: dict[str, Any],
                                config_path: str) -> list[Finding]:
    """GUARD-010: Detect shell metacharacters in args."""
    findings: list[Finding] = []
    args = server_cfg.get("args", [])

    if not isinstance(args, list):
        return findings

    shell_meta = re.compile(r'[|><;&`$]')

    for arg in args:
        arg_str = str(arg)
        if shell_meta.search(arg_str):
            rule = _GUARD_RULES_MAP["GUARD-010"]
            findings.append(Finding(
                rule_id="GUARD-010",
                severity=rule.severity,
                title=rule.name,
                description=(
                    f"Server '{server_name}' has shell metacharacters in args: "
                    f"'{arg_str[:60]}'. These may cause unexpected shell behavior."
                ),
                file=config_path,
                line=0,
                code_snippet=f'arg: {arg_str[:80]}',
                recommendation=(
                    "Remove shell metacharacters from args or use a wrapper script."
                ),
                cwe=rule.cwe,
            ))

    return findings


# --- ConfigGuard main class ---


@dataclass
class ConfigGuardResult:
    """Result of scanning a config file."""

    file: str
    findings: list[Finding] = field(default_factory=list)
    servers_scanned: int = 0
    parse_error: str = ""

    @property
    def severity_counts(self) -> dict[str, int]:
        """Count findings by severity."""
        counts = {s.value: 0 for s in Severity}
        for f in self.findings:
            counts[f.severity.value] += 1
        return counts

    @property
    def has_critical(self) -> bool:
        return any(f.severity == Severity.CRITICAL for f in self.findings)

    @property
    def has_high(self) -> bool:
        return any(f.severity == Severity.HIGH for f in self.findings)

    @property
    def exit_code(self) -> int:
        """CI/CD exit code: 0=clean, 1=high, 2=critical."""
        if self.has_critical:
            return 2
        if self.has_high:
            return 1
        return 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "file": self.file,
            "servers_scanned": self.servers_scanned,
            "total_findings": len(self.findings),
            "severity_counts": self.severity_counts,
            "exit_code": self.exit_code,
            "findings": [f.to_dict() for f in self.findings],
        }


class ConfigGuard:
    """MCP Configuration security scanner.

    Scans MCP configuration files for security misconfigurations.
    Supports claude_desktop_config.json and similar formats.
    """

    def __init__(self, rules: list[GuardRule] | None = None) -> None:
        """Initialize ConfigGuard.

        Args:
            rules: Optional list of guard rules. If None, all built-in rules.
        """
        self.rules = rules if rules is not None else _get_all_guard_rules()
        self._active_rule_ids = {r.rule_id for r in self.rules}

    def scan_config(self, config_path: Path) -> ConfigGuardResult:
        """Scan an MCP config file for security issues.

        Args:
            config_path: Path to the JSON config file.

        Returns:
            ConfigGuardResult with all findings.
        """
        result = ConfigGuardResult(file=str(config_path))

        try:
            raw_text = config_path.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError) as e:
            result.parse_error = f"Cannot read file: {e}"
            return result

        return self.scan_config_text(raw_text, str(config_path))

    def scan_config_text(self, raw_text: str, config_path: str = "<stdin>") -> ConfigGuardResult:
        """Scan raw config text for security issues.

        Args:
            raw_text: Raw JSON text of the config.
            config_path: Display path for findings.

        Returns:
            ConfigGuardResult with all findings.
        """
        result = ConfigGuardResult(file=config_path)

        try:
            config = json.loads(raw_text)
        except json.JSONDecodeError as e:
            result.parse_error = f"Invalid JSON: {e}"
            return result

        # Extract servers from various config formats
        servers = self._extract_servers(config)
        result.servers_scanned = len(servers)

        # Run per-server checks
        for server_name, server_cfg in servers.items():
            if not isinstance(server_cfg, dict):
                continue
            result.findings.extend(self._check_server(server_name, server_cfg, config_path))

        # Run cross-server checks
        if "GUARD-006" in self._active_rule_ids:
            result.findings.extend(_check_duplicate_servers_raw(raw_text, config_path))

        return result

    def _extract_servers(self, config: dict[str, Any]) -> dict[str, Any]:
        """Extract MCP server definitions from various config formats.

        Supports:
        - claude_desktop_config.json: {"mcpServers": {...}}
        - .claude/settings.json: {"mcpServers": {...}}
        - Generic: {"servers": {...}}
        - Top-level server dict: {"server-name": {"command": ...}}
        """
        # Claude Desktop / Claude Code format
        if "mcpServers" in config:
            servers = config["mcpServers"]
            if isinstance(servers, dict):
                return servers

        # Generic format
        if "servers" in config:
            servers = config["servers"]
            if isinstance(servers, dict):
                return servers

        # Check if top-level keys look like server definitions
        looks_like_servers = all(
            isinstance(v, dict) and ("command" in v or "url" in v)
            for v in config.values()
            if isinstance(v, dict)
        )
        if looks_like_servers and config:
            return config

        return {}

    def _check_server(self, server_name: str, server_cfg: dict[str, Any],
                      config_path: str) -> list[Finding]:
        """Run all active guard rules against a server config."""
        findings: list[Finding] = []

        checks = [
            ("GUARD-001", _check_shell_injection),
            ("GUARD-002", _check_unverified_path),
            ("GUARD-003", _check_missing_auth),
            ("GUARD-004", _check_exposed_port),
            ("GUARD-005", _check_sensitive_env_in_args),
            ("GUARD-007", _check_wildcard_tools),
            ("GUARD-008", _check_writable_paths),
            ("GUARD-009", _check_hardcoded_secrets),
            ("GUARD-010", _check_shell_metacharacters),
        ]

        for rule_id, check_fn in checks:
            if rule_id in self._active_rule_ids:
                findings.extend(check_fn(server_name, server_cfg, config_path))

        return findings


# --- Formatting ---


def format_guard_text(result: ConfigGuardResult) -> str:
    """Format guard results as human-readable text."""
    lines: list[str] = []
    lines.append(f"\n{'=' * 60}")
    lines.append("  MCP Config Guard — Security Check")
    lines.append(f"{'=' * 60}")
    lines.append(f"  Config file: {result.file}")
    lines.append(f"  Servers scanned: {result.servers_scanned}")
    lines.append(f"  Total findings: {len(result.findings)}")

    if result.parse_error:
        lines.append(f"  Parse error: {result.parse_error}")
        lines.append(f"{'=' * 60}")
        return "\n".join(lines)

    counts = result.severity_counts
    lines.append(
        f"  CRITICAL: {counts['CRITICAL']} | HIGH: {counts['HIGH']} | "
        f"MEDIUM: {counts['MEDIUM']} | LOW: {counts['LOW']}"
    )
    lines.append(f"{'=' * 60}\n")

    if not result.findings:
        lines.append("  [OK] No security issues found in config.\n")
        return "\n".join(lines)

    for finding in result.findings:
        icon = finding.severity.icon_plain
        lines.append(f"  {icon} [{finding.rule_id}] {finding.title}")
        lines.append(f"      {finding.description}")
        if finding.code_snippet:
            lines.append(f"      Code: {finding.code_snippet}")
        if finding.recommendation:
            lines.append(f"      Fix:  {finding.recommendation}")
        lines.append("")

    return "\n".join(lines)


def format_guard_json(result: ConfigGuardResult) -> str:
    """Format guard results as JSON."""
    return json.dumps(result.to_dict(), indent=2, ensure_ascii=False)


def format_guard_sarif(result: ConfigGuardResult) -> str:
    """Format guard results as SARIF 2.1.0."""
    from .reporters.sarif_reporter import (
        SARIF_SCHEMA,
        SARIF_VERSION,
        TOOL_NAME,
        TOOL_URI,
        TOOL_VERSION,
        _build_result,
        _build_rule,
    )

    rules_map: dict[str, dict[str, Any]] = {}
    for f in result.findings:
        if f.rule_id not in rules_map:
            rules_map[f.rule_id] = _build_rule(f)

    sarif_results = [_build_result(f) for f in result.findings]

    sarif: dict[str, Any] = {
        "$schema": SARIF_SCHEMA,
        "version": SARIF_VERSION,
        "runs": [
            {
                "tool": {
                    "driver": {
                        "name": TOOL_NAME,
                        "version": TOOL_VERSION,
                        "informationUri": TOOL_URI,
                        "rules": list(rules_map.values()),
                    },
                },
                "results": sarif_results,
            }
        ],
    }

    return json.dumps(sarif, indent=2, ensure_ascii=False)


def get_all_guard_rules() -> list[GuardRule]:
    """Public accessor for all built-in guard rules."""
    return _get_all_guard_rules()
