"""Security rules for MCP audit scanner.

Each rule module implements the Rule base class.
Rules receive (ast_tree, source) and return list[Finding].
"""

from __future__ import annotations

import ast
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..models import Finding, Severity


class Rule(ABC):
    """Base class for all security rules."""

    @property
    @abstractmethod
    def rule_id(self) -> str:
        """Unique rule identifier (e.g. PATH-001)."""

    @property
    @abstractmethod
    def name(self) -> str:
        """Human-readable rule name."""

    @property
    @abstractmethod
    def severity(self) -> Severity:
        """Default severity level."""

    @property
    @abstractmethod
    def description(self) -> str:
        """Description of what this rule detects."""

    @property
    def cwe(self) -> str:
        """CWE identifier (e.g. 'CWE-78'). Override in subclasses."""
        return ""

    @property
    def detailed_description(self) -> str:
        """Extended description with examples and remediation guidance.

        Override in subclasses to provide richer --explain output.
        Falls back to the short description by default.
        """
        return self.description

    @abstractmethod
    def check(self, tree: ast.Module, source: str) -> list[Finding]:
        """Run the rule against parsed source code.

        Args:
            tree: Parsed AST of the source file.
            source: Raw source code string.

        Returns:
            List of findings detected by this rule.
        """

    def _get_lines(self, source: str) -> list[str]:
        """Split source into lines for snippet extraction."""
        return source.splitlines()

    def _get_snippet(self, source: str, lineno: int) -> str:
        """Get a code snippet for the given line number."""
        lines = self._get_lines(source)
        if 1 <= lineno <= len(lines):
            return lines[lineno - 1].strip()
        return ""

    def _get_func_name(self, node: ast.Call) -> str:
        """Extract the full function name from a Call node."""
        if isinstance(node.func, ast.Name):
            return node.func.id
        if isinstance(node.func, ast.Attribute):
            parts: list[str] = []
            current: ast.expr = node.func
            while isinstance(current, ast.Attribute):
                parts.append(current.attr)
                current = current.value
            if isinstance(current, ast.Name):
                parts.append(current.id)
            return ".".join(reversed(parts))
        return ""

    def _find_parent_function(
        self, tree: ast.Module, target_node: ast.AST
    ) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
        """Find the enclosing function definition for a node."""
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                for child in ast.walk(node):
                    if child is target_node:
                        return node
        return None


def get_all_rules() -> list[Rule]:
    """Return instances of all available rules."""
    from .path_traversal import PathTraversalRule
    from .command_injection import CommandInjectionShellRule, CommandInjectionDynamicRule
    from .hardcoded_secrets import HardcodedSecretsRule
    from .absolute_paths import AbsolutePathsRule
    from .input_validation import MissingInputValidationRule
    from .sql_injection import SqlInjectionRule
    from .unsafe_deser import UnsafeDeserializationRule
    from .info_disclosure import InformationDisclosureRule
    from .missing_auth import MissingAuthRule
    from .cors_misconfig import CorsMisconfigurationRule
    from .unsafe_temp import UnsafeTempFilesRule
    from .excessive_perms import ExcessivePermissionsRule
    from .redos import ReDosRule
    from .rate_limiting import MissingRateLimitingRule
    from .ssrf import SSRFRule
    from .sensitive_logging import SensitiveLoggingRule
    from .unsafe_file_write import UnsafeFileWriteRule
    from .error_handling import UnhandledToolErrorRule
    from .resource_exhaustion import ResourceExhaustionRule
    from .mcp_tool_safety import MCPToolDescriptionRule, MCPEnvAccessRule
    from .confused_deputy import ConfusedDeputyRule
    from .prompt_injection import PromptInjectionRule
    from .idor import IDORRule
    from .privilege_escalation import PrivilegeEscalationRule
    from .missing_tool_authz import MissingToolAuthzRule

    return [
        PathTraversalRule(),
        CommandInjectionShellRule(),
        CommandInjectionDynamicRule(),
        HardcodedSecretsRule(),
        AbsolutePathsRule(),
        MissingInputValidationRule(),
        SqlInjectionRule(),
        UnsafeDeserializationRule(),
        InformationDisclosureRule(),
        MissingAuthRule(),
        CorsMisconfigurationRule(),
        UnsafeTempFilesRule(),
        ExcessivePermissionsRule(),
        ReDosRule(),
        MissingRateLimitingRule(),
        SSRFRule(),
        SensitiveLoggingRule(),
        UnsafeFileWriteRule(),
        UnhandledToolErrorRule(),
        ResourceExhaustionRule(),
        MCPToolDescriptionRule(),
        MCPEnvAccessRule(),
        ConfusedDeputyRule(),
        PromptInjectionRule(),
        IDORRule(),
        PrivilegeEscalationRule(),
        MissingToolAuthzRule(),
    ]
