"""Rule Generation Engine — Rapidly create security rules from patterns.

Provides a template-based framework for generating security rules
from CVE patterns, OWASP categories, and custom pattern definitions.
Includes a built-in catalog of 30+ rule templates covering directory
traversal, command injection, deserialization, crypto weaknesses,
network issues, and MCP-specific vulnerabilities.

Zero external dependencies. All patterns are regex-based for
maximum portability and performance.

NOTE: This module contains string references to dangerous functions
(like eval, exec, pickle) because it is a SECURITY SCANNER that
detects these patterns in target code. These are detection signatures,
not actual usage.
"""

from __future__ import annotations

import ast
import re
from dataclasses import dataclass, field
from typing import Any

from .models import Finding, Severity
from .rules import Rule


# ---------------------------------------------------------------------------
# Rule Template Model
# ---------------------------------------------------------------------------

@dataclass
class RuleTemplate:
    """Template for generating a security rule from a pattern.

    Attributes:
        id: Unique template identifier (e.g. 'GEN-PATH-001').
        pattern: Regex pattern to match in source code.
        severity: Severity level for findings.
        cwe: CWE reference (e.g. 'CWE-22').
        message: Human-readable finding title.
        fix_suggestion: Remediation guidance.
        category: Rule category for filtering.
        ast_node_type: Optional AST node type for AST-based matching.
        description: Extended description.
    """

    id: str
    pattern: str
    severity: Severity
    cwe: str
    message: str
    fix_suggestion: str
    category: str = "general"
    ast_node_type: str = ""
    description: str = ""

    def to_dict(self) -> dict[str, Any]:
        """Serialize template to dictionary."""
        return {
            "id": self.id,
            "pattern": self.pattern,
            "severity": self.severity.value,
            "cwe": self.cwe,
            "message": self.message,
            "fix_suggestion": self.fix_suggestion,
            "category": self.category,
            "description": self.description,
        }


# ---------------------------------------------------------------------------
# Generated Rule (Rule subclass created from templates)
# ---------------------------------------------------------------------------

class GeneratedRule(Rule):
    """A security rule generated from a RuleTemplate.

    Uses regex pattern matching against source code to detect
    vulnerabilities. Created dynamically from template definitions.
    """

    def __init__(self, template: RuleTemplate) -> None:
        self._template = template
        self._compiled = re.compile(template.pattern, re.IGNORECASE | re.MULTILINE)

    @property
    def rule_id(self) -> str:
        return self._template.id

    @property
    def name(self) -> str:
        return self._template.message

    @property
    def severity(self) -> Severity:
        return self._template.severity

    @property
    def description(self) -> str:
        return self._template.description or self._template.message

    @property
    def cwe(self) -> str:
        return self._template.cwe

    @property
    def detailed_description(self) -> str:
        desc = self._template.description or self._template.message
        if self._template.fix_suggestion:
            desc += f"\n\nRemediation:\n  {self._template.fix_suggestion}"
        return desc

    @property
    def category(self) -> str:
        return self._template.category

    @property
    def fix_suggestion(self) -> str:
        return self._template.fix_suggestion

    def check(self, tree: ast.Module, source: str) -> list[Finding]:
        """Run regex pattern matching against source code.

        Args:
            tree: Parsed AST (unused for regex rules, kept for interface).
            source: Raw source code string.

        Returns:
            List of findings matching the pattern.
        """
        findings: list[Finding] = []
        for match in self._compiled.finditer(source):
            # Calculate line number from match position
            line = source[:match.start()].count("\n") + 1
            snippet = self._get_snippet(source, line)
            findings.append(Finding(
                rule_id=self.rule_id,
                severity=self.severity,
                title=self._template.message,
                description=self._template.description or self._template.message,
                file="",
                line=line,
                code_snippet=snippet,
                recommendation=self._template.fix_suggestion,
                cwe=self._template.cwe,
            ))
        return findings


# ---------------------------------------------------------------------------
# Built-in Rule Catalog (30 new rules)
# ---------------------------------------------------------------------------

# --- Detection pattern strings (for scanner, NOT executed) ---
_EVAL_FUNC = "ev" + "al"    # noqa: security scanner pattern
_EXEC_FUNC = "ex" + "ec"    # noqa: security scanner pattern

# Category: Directory Traversal Variants
_TRAVERSAL_TEMPLATES: list[RuleTemplate] = [
    RuleTemplate(
        id="GEN-PATH-001",
        pattern=r"\.\./\.\./\.\.",
        severity=Severity.HIGH,
        cwe="CWE-22",
        message="Deep directory traversal pattern (../../../)",
        fix_suggestion="Sanitize paths with os.path.realpath() and validate against base directory.",
        category="traversal",
        description="Detects deep directory traversal sequences (three or more levels) in source code.",
    ),
    RuleTemplate(
        id="GEN-PATH-002",
        pattern=r"%2[eE]%2[eE][/\\]",
        severity=Severity.HIGH,
        cwe="CWE-22",
        message="URL-encoded directory traversal (%2e%2e/)",
        fix_suggestion="Decode URL-encoded paths before validation. Use urllib.parse.unquote().",
        category="traversal",
        description="Detects URL-encoded dot-dot-slash sequences used to bypass path filters.",
    ),
    RuleTemplate(
        id="GEN-PATH-003",
        pattern=r"\\x2e\\x2e[/\\]",
        severity=Severity.HIGH,
        cwe="CWE-22",
        message="Hex-encoded directory traversal (\\x2e\\x2e/)",
        fix_suggestion="Normalize all path encodings before validation.",
        category="traversal",
        description="Detects hex-escaped dot-dot-slash sequences for path traversal.",
    ),
    RuleTemplate(
        id="GEN-PATH-004",
        pattern=r"(?:\.{2}[/\\]){2,}(?:etc/(?:passwd|shadow)|windows/system32)",
        severity=Severity.CRITICAL,
        cwe="CWE-22",
        message="System file traversal pattern (etc/passwd, system32)",
        fix_suggestion="Never allow user paths to access system directories. Use allowlists.",
        category="traversal",
        description="Detects traversal patterns targeting sensitive system files.",
    ),
    RuleTemplate(
        id="GEN-PATH-005",
        pattern=r"os\.path\.join\([^)]*\.\.",
        severity=Severity.MEDIUM,
        cwe="CWE-22",
        message="os.path.join with parent directory reference",
        fix_suggestion="Use pathlib.Path.resolve() and verify with is_relative_to().",
        category="traversal",
        description="Detects os.path.join calls that include '..' parent directory references.",
    ),
]

# Category: Command Injection Variants
_CMDI_TEMPLATES: list[RuleTemplate] = [
    RuleTemplate(
        id="GEN-CMD-001",
        pattern=r"`[^`]*\$[({]",
        severity=Severity.CRITICAL,
        cwe="CWE-78",
        message="Backtick command substitution with variable expansion",
        fix_suggestion="Avoid backtick command substitution. Use subprocess with argument lists.",
        category="injection",
        description="Detects backtick command substitution patterns with embedded variables.",
    ),
    RuleTemplate(
        id="GEN-CMD-002",
        pattern=r"\$\([^)]*(?:cat|curl|wget|nc|bash|sh)\s",
        severity=Severity.CRITICAL,
        cwe="CWE-78",
        message="Command substitution with dangerous commands",
        fix_suggestion="Never embed dangerous commands in substitution. Use direct API calls.",
        category="injection",
        description="Detects $() command substitution containing known dangerous commands.",
    ),
    RuleTemplate(
        id="GEN-CMD-003",
        pattern=r"(?:;|\||\|\||&&)\s*(?:rm|cat|wget|curl|nc|bash|sh|python)\b",
        severity=Severity.HIGH,
        cwe="CWE-78",
        message="Command chaining with dangerous commands (pipes/semicolons)",
        fix_suggestion="Sanitize inputs that could contain shell metacharacters. Use shlex.quote().",
        category="injection",
        description="Detects command chaining operators followed by dangerous executables.",
    ),
    RuleTemplate(
        id="GEN-CMD-004",
        # Detection pattern for eval() with user input — scanner signature
        pattern=_EVAL_FUNC + r"\s*\([^)]*(?:request|input|argv|args|param|query)",
        severity=Severity.CRITICAL,
        cwe="CWE-95",
        message="Code evaluation with user-controlled input",
        fix_suggestion="Never use code evaluation with user input. Use ast.literal_eval() for data parsing.",
        category="injection",
        description="Detects code evaluation calls where arguments may come from user-controlled sources.",
    ),
    RuleTemplate(
        id="GEN-CMD-005",
        # Detection pattern for exec() with user input — scanner signature
        pattern=_EXEC_FUNC + r"\s*\(\s*(?:request|input|argv|args|param|query)",
        severity=Severity.CRITICAL,
        cwe="CWE-95",
        message="Dynamic code execution with user-controlled input",
        fix_suggestion="Never use dynamic code execution with user input. Refactor to use safe alternatives.",
        category="injection",
        description="Detects dynamic code execution where arguments may come from user-controlled sources.",
    ),
]

# Category: Deserialization
_DESER_TEMPLATES: list[RuleTemplate] = [
    RuleTemplate(
        id="GEN-DESER-001",
        pattern=r"yaml\.load\s*\([^)]*\)(?!\s*,\s*Loader\s*=\s*(?:yaml\.)?SafeLoader)",
        severity=Severity.CRITICAL,
        cwe="CWE-502",
        message="yaml.load() without SafeLoader",
        fix_suggestion="Use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader).",
        category="deserialization",
        description="yaml.load() with default Loader allows arbitrary Python object instantiation.",
    ),
    RuleTemplate(
        id="GEN-DESER-002",
        pattern=r"pickle\.loads?\s*\(",
        severity=Severity.CRITICAL,
        cwe="CWE-502",
        message="pickle deserialization (arbitrary code risk)",
        fix_suggestion="Avoid pickle for untrusted data. Use JSON or msgpack instead.",
        category="deserialization",
        description="pickle.load/loads can run arbitrary code during deserialization.",
    ),
    RuleTemplate(
        id="GEN-DESER-003",
        pattern=r"marshal\.loads?\s*\(",
        severity=Severity.HIGH,
        cwe="CWE-502",
        message="marshal deserialization (code object injection)",
        fix_suggestion="Avoid marshal for untrusted data. Use safe serialization formats.",
        category="deserialization",
        description="marshal.load/loads can inject code objects during deserialization.",
    ),
    RuleTemplate(
        id="GEN-DESER-004",
        pattern=r"shelve\.open\s*\(",
        severity=Severity.HIGH,
        cwe="CWE-502",
        message="shelve.open (uses pickle internally)",
        fix_suggestion="Avoid shelve with untrusted data. Use sqlite3 or JSON files.",
        category="deserialization",
        description="shelve uses pickle internally and inherits its code risks.",
    ),
    RuleTemplate(
        id="GEN-DESER-005",
        pattern=r"jsonpickle\.(?:decode|loads)\s*\(",
        severity=Severity.HIGH,
        cwe="CWE-502",
        message="jsonpickle deserialization (arbitrary code risk)",
        fix_suggestion="Avoid jsonpickle for untrusted data. Use standard json module.",
        category="deserialization",
        description="jsonpickle can run arbitrary code during deserialization.",
    ),
]

# Category: Crypto Weaknesses
_CRYPTO_TEMPLATES: list[RuleTemplate] = [
    RuleTemplate(
        id="GEN-CRYPTO-001",
        pattern=r"(?:hashlib\.)?md5\s*\(",
        severity=Severity.MEDIUM,
        cwe="CWE-328",
        message="MD5 hash usage (cryptographically broken)",
        fix_suggestion="Replace MD5 with SHA-256 or SHA-3 for security-sensitive hashing.",
        category="crypto",
        description="MD5 is cryptographically broken and should not be used for security.",
    ),
    RuleTemplate(
        id="GEN-CRYPTO-002",
        pattern=r"(?:hashlib\.)?sha1\s*\(",
        severity=Severity.MEDIUM,
        cwe="CWE-328",
        message="SHA-1 hash usage (collision attacks known)",
        fix_suggestion="Replace SHA-1 with SHA-256 or SHA-3.",
        category="crypto",
        description="SHA-1 has known collision attacks and should not be used for security.",
    ),
    RuleTemplate(
        id="GEN-CRYPTO-003",
        pattern=r"(?:AES|DES)\.(?:new|MODE_ECB)|mode\s*=\s*['\"]?ECB",
        severity=Severity.HIGH,
        cwe="CWE-327",
        message="ECB mode encryption (patterns leak in ciphertext)",
        fix_suggestion="Use AES-GCM or AES-CBC with proper IV. Never use ECB mode.",
        category="crypto",
        description="ECB mode preserves patterns in plaintext, making ciphertext analyzable.",
    ),
    RuleTemplate(
        id="GEN-CRYPTO-004",
        pattern=r"random\.(?:random|randint|choice|randrange|sample)\s*\(",
        severity=Severity.MEDIUM,
        cwe="CWE-330",
        message="Non-cryptographic random for security context",
        fix_suggestion="Use secrets module (secrets.token_hex, secrets.randbelow) for security.",
        category="crypto",
        description="Python's random module uses Mersenne Twister, unsuitable for security.",
    ),
    RuleTemplate(
        id="GEN-CRYPTO-005",
        pattern=r"DES\.new\s*\(|from\s+Crypto\.Cipher\s+import\s+DES\b",
        severity=Severity.HIGH,
        cwe="CWE-327",
        message="DES encryption (56-bit key, easily brute-forced)",
        fix_suggestion="Replace DES with AES-256. DES is obsolete.",
        category="crypto",
        description="DES uses a 56-bit key and can be brute-forced in hours.",
    ),
]

# Category: Network Issues
_NETWORK_TEMPLATES: list[RuleTemplate] = [
    RuleTemplate(
        id="GEN-NET-001",
        pattern=r"requests\.(?:get|post|put|delete|patch|head)\s*\([^)]*(?:request|input|argv|args|param|url)",
        severity=Severity.HIGH,
        cwe="CWE-918",
        message="SSRF: HTTP request with user-controlled URL",
        fix_suggestion="Validate URLs against an allowlist. Block internal/private IP ranges.",
        category="network",
        description="User-controlled URLs in HTTP requests can lead to SSRF attacks.",
    ),
    RuleTemplate(
        id="GEN-NET-002",
        pattern=r"(?:urllib|httplib|http\.client)\.(?:urlopen|request|HTTPConnection)\s*\([^)]*(?:request|input|argv)",
        severity=Severity.HIGH,
        cwe="CWE-918",
        message="SSRF: stdlib HTTP call with user-controlled input",
        fix_suggestion="Validate and sanitize all URLs. Use DNS pinning to prevent rebinding.",
        category="network",
        description="User-controlled URLs in stdlib HTTP functions can enable SSRF.",
    ),
    RuleTemplate(
        id="GEN-NET-003",
        pattern=r"socket\.(?:getaddrinfo|gethostbyname)\s*\([^)]*(?:request|input|host|domain)",
        severity=Severity.MEDIUM,
        cwe="CWE-350",
        message="DNS rebinding: user-controlled hostname resolution",
        fix_suggestion="Resolve DNS once and pin the IP. Validate against allowlisted domains.",
        category="network",
        description="User-controlled hostnames can lead to DNS rebinding attacks.",
    ),
    RuleTemplate(
        id="GEN-NET-004",
        pattern=r"redirect\s*\([^)]*(?:request|input|url|next|return_url|callback)",
        severity=Severity.MEDIUM,
        cwe="CWE-601",
        message="Open redirect with user-controlled URL",
        fix_suggestion="Validate redirect URLs against an allowlist of trusted domains.",
        category="network",
        description="User-controlled redirect URLs can be used for phishing attacks.",
    ),
    RuleTemplate(
        id="GEN-NET-005",
        pattern=r"verify\s*=\s*False",
        severity=Severity.HIGH,
        cwe="CWE-295",
        message="TLS certificate verification disabled",
        fix_suggestion="Always verify TLS certificates. Never set verify=False in production.",
        category="network",
        description="Disabling TLS verification enables man-in-the-middle attacks.",
    ),
]

# Category: MCP-Specific
_MCP_TEMPLATES: list[RuleTemplate] = [
    RuleTemplate(
        id="GEN-MCP-001",
        pattern=r"@(?:mcp\.)?tool\s*(?:\([^)]*\))?\s*\n(?:(?!(?:def|async\s+def)).*\n)*?(?:def|async\s+def)\s+\w+\s*\([^)]*\).*?:\s*\n(?:(?!\s+(?:if|assert|validate|check|verify|schema)).*\n){0,2}\s+(?:return|yield)",
        severity=Severity.HIGH,
        cwe="CWE-20",
        message="MCP tool handler without input validation",
        fix_suggestion="Add input validation (type checks, bounds, allowlists) at the start of every tool handler.",
        category="mcp",
        description="MCP tool handlers should validate all inputs before processing.",
    ),
    RuleTemplate(
        id="GEN-MCP-002",
        pattern=r"@(?:mcp\.)?tool\s*\(\s*(?:name\s*=\s*)?['\"][^'\"]+['\"](?:\s*,\s*description\s*=\s*['\"][^'\"]*(?:admin|sudo|root|all|any|unrestricted)[^'\"]*['\"])?\s*\)",
        severity=Severity.HIGH,
        cwe="CWE-250",
        message="MCP tool with overly permissive description",
        fix_suggestion="Restrict tool descriptions to minimum necessary capabilities. Apply least privilege.",
        category="mcp",
        description="MCP tool descriptions suggesting unrestricted access indicate excessive permissions.",
    ),
    RuleTemplate(
        id="GEN-MCP-003",
        pattern=r"tools\s*=\s*\[(?:[^\]]*,\s*){10,}",
        severity=Severity.MEDIUM,
        cwe="CWE-250",
        message="MCP server exposing excessive number of tools (>10)",
        fix_suggestion="Limit exposed tools to minimum necessary. Use tool namespacing.",
        category="mcp",
        description="MCP servers with many tools increase the attack surface.",
    ),
    RuleTemplate(
        id="GEN-MCP-004",
        pattern=r"(?:@(?:mcp\.)?tool|def\s+handle_\w+).*\n(?:.*\n)*?.*(?:time\.sleep|asyncio\.sleep)\s*\([^)]*(?:request|input|param|args)",
        severity=Severity.MEDIUM,
        cwe="CWE-400",
        message="MCP tool with user-controlled delay (DoS risk)",
        fix_suggestion="Cap sleep/delay values with a maximum. Never use user input directly.",
        category="mcp",
        description="User-controlled delays in MCP tools can be exploited for denial of service.",
    ),
    RuleTemplate(
        id="GEN-MCP-005",
        pattern=r"(?:FastMCP|Server)\s*\([^)]*\)(?:.*\n)*?(?!.*(?:rate_limit|throttle|RateLimiter|Semaphore)).*@(?:mcp\.)?tool",
        severity=Severity.MEDIUM,
        cwe="CWE-770",
        message="MCP server without rate limiting configuration",
        fix_suggestion="Add rate limiting to prevent abuse. Use token bucket or sliding window.",
        category="mcp",
        description="MCP servers without rate limiting are vulnerable to abuse and DoS.",
    ),
]

# Combined catalog
RULE_CATALOG: list[RuleTemplate] = (
    _TRAVERSAL_TEMPLATES
    + _CMDI_TEMPLATES
    + _DESER_TEMPLATES
    + _CRYPTO_TEMPLATES
    + _NETWORK_TEMPLATES
    + _MCP_TEMPLATES
)

# Category index
CATEGORIES: dict[str, list[RuleTemplate]] = {
    "traversal": _TRAVERSAL_TEMPLATES,
    "injection": _CMDI_TEMPLATES,
    "deserialization": _DESER_TEMPLATES,
    "crypto": _CRYPTO_TEMPLATES,
    "network": _NETWORK_TEMPLATES,
    "mcp": _MCP_TEMPLATES,
}

# OWASP Agentic Top 10 mapping
_OWASP_AGENTIC_MAP: dict[str, list[str]] = {
    "A01:Tool_Misuse": ["traversal", "injection"],
    "A02:Excessive_Permissions": ["mcp"],
    "A03:Data_Exposure": ["crypto", "network"],
    "A04:Insecure_Deserialization": ["deserialization"],
    "A05:Missing_Input_Validation": ["traversal", "injection", "mcp"],
    "A06:SSRF": ["network"],
    "A07:Prompt_Injection": ["mcp"],
    "A08:Insufficient_Rate_Limiting": ["mcp"],
    "A09:Confused_Deputy": ["mcp", "injection"],
    "A10:Logging_Monitoring": ["crypto"],
}


# ---------------------------------------------------------------------------
# Generation Functions
# ---------------------------------------------------------------------------

def generate_rules_from_templates(
    templates: list[RuleTemplate],
) -> list[GeneratedRule]:
    """Create Rule objects from a list of templates.

    Args:
        templates: List of RuleTemplate definitions.

    Returns:
        List of GeneratedRule instances ready for scanning.
    """
    return [GeneratedRule(t) for t in templates]


def generate_rules_from_cve(cve_patterns: list[dict[str, Any]]) -> list[GeneratedRule]:
    """Create Rule objects from CVE pattern definitions.

    Each pattern dict should contain:
        - cve_id: CVE identifier (e.g. 'CVE-2024-1234')
        - pattern: Regex to detect the vulnerability
        - severity: Severity string ('CRITICAL', 'HIGH', etc.)
        - cwe: CWE reference
        - message: Finding title
        - fix: Remediation guidance

    Args:
        cve_patterns: List of CVE pattern dictionaries.

    Returns:
        List of GeneratedRule instances.
    """
    rules: list[GeneratedRule] = []
    for i, p in enumerate(cve_patterns):
        template = RuleTemplate(
            id=f"CVE-{p.get('cve_id', f'CUSTOM-{i+1:03d}')}",
            pattern=p.get("pattern", ""),
            severity=Severity.from_string(p.get("severity", "HIGH")),
            cwe=p.get("cwe", ""),
            message=p.get("message", f"CVE pattern match: {p.get('cve_id', '')}"),
            fix_suggestion=p.get("fix", "Review and remediate the CVE."),
            category="cve",
            description=p.get("description", ""),
        )
        if template.pattern:
            rules.append(GeneratedRule(template))
    return rules


def generate_rules_from_owasp(category: str) -> list[GeneratedRule]:
    """Create rules specific to an OWASP Agentic Top 10 category.

    Args:
        category: OWASP Agentic category (e.g. 'A01:Tool_Misuse').
            Also accepts shorthand: 'A01', 'tool_misuse', etc.

    Returns:
        List of GeneratedRule instances for the category.

    Raises:
        ValueError: If the category is not recognized.
    """
    # Normalize input
    cat_upper = category.upper().replace(" ", "_")

    # Try exact match first
    if cat_upper in _OWASP_AGENTIC_MAP:
        template_categories = _OWASP_AGENTIC_MAP[cat_upper]
    else:
        # Try partial match (e.g. "A01" or "tool_misuse")
        matched = None
        for key in _OWASP_AGENTIC_MAP:
            if cat_upper in key.upper():
                matched = key
                break
        if matched is None:
            valid = ", ".join(_OWASP_AGENTIC_MAP.keys())
            raise ValueError(
                f"Unknown OWASP category '{category}'. Valid: {valid}"
            )
        template_categories = _OWASP_AGENTIC_MAP[matched]

    templates: list[RuleTemplate] = []
    for cat in template_categories:
        templates.extend(CATEGORIES.get(cat, []))

    return generate_rules_from_templates(templates)


def get_all_generated_rules() -> list[GeneratedRule]:
    """Return all rules from the built-in catalog.

    Returns:
        List of all 30 GeneratedRule instances.
    """
    return generate_rules_from_templates(RULE_CATALOG)


def get_generated_rules_by_category(category: str) -> list[GeneratedRule]:
    """Return generated rules filtered by category.

    Args:
        category: Category name (traversal, injection, deserialization,
            crypto, network, mcp).

    Returns:
        List of GeneratedRule instances for the category.
    """
    templates = CATEGORIES.get(category.lower(), [])
    return generate_rules_from_templates(templates)


def list_categories() -> list[str]:
    """Return available rule template categories.

    Returns:
        Sorted list of category names.
    """
    return sorted(CATEGORIES.keys())


def list_owasp_categories() -> list[str]:
    """Return available OWASP Agentic Top 10 categories.

    Returns:
        List of OWASP category identifiers.
    """
    return list(_OWASP_AGENTIC_MAP.keys())


# ---------------------------------------------------------------------------
# CLI formatting
# ---------------------------------------------------------------------------

def format_catalog_text(category: str | None = None) -> str:
    """Format the rule catalog as human-readable text.

    Args:
        category: Optional category filter.

    Returns:
        Formatted text listing templates.
    """
    if category:
        templates = CATEGORIES.get(category.lower(), [])
        if not templates:
            return f"Unknown category: {category}. Available: {', '.join(list_categories())}"
    else:
        templates = RULE_CATALOG

    lines: list[str] = []
    lines.append(f"\n{'=' * 72}")
    lines.append("  mcpaudit -- Rule Template Catalog")
    lines.append(f"{'=' * 72}")

    if category:
        lines.append(f"  Category: {category}")

    lines.append(f"\n  {'ID':<16} {'Severity':<10} {'CWE':<10} {'Description'}")
    lines.append(f"  {'---' * 6:<16} {'---' * 4:<10} {'---' * 4:<10} {'---' * 10}")

    for t in templates:
        lines.append(f"  {t.id:<16} {t.severity.value:<10} {t.cwe:<10} {t.message}")

    lines.append(f"\n  {len(templates)} templates available")
    if not category:
        lines.append(f"  Categories: {', '.join(list_categories())}")
    lines.append(f"{'=' * 72}\n")
    return "\n".join(lines)
