"""Rule definitions and execution for policy evaluation.

This module provides the core rule engine for the MergeGuide Policy Engine.
It defines the data structures and logic for pattern-based code analysis,
supporting both regex and literal string matching against source code
and diff content.

The module implements a hierarchical structure:
- RuleType: Defines how patterns are matched (regex vs literal)
- Severity: Classifies the importance of rule violations
- RuleMatch: Captures individual pattern matches with location info
- RuleResult: Aggregates matches for a single rule execution
- Rule: Defines a complete rule with pattern, message, and metadata
- RuleEngine: Orchestrates rule execution across multiple rules

Example usage:
    >>> from mergeguide.engine.rules import Rule, RuleEngine, Severity, RuleType
    >>> rule = Rule(
    ...     id="no-console",
    ...     name="No Console Statements",
    ...     pattern=r"console\.(log|warn|error)",
    ...     message="Remove console statements before committing",
    ...     severity=Severity.WARNING,
    ...     rule_type=RuleType.REGEX,
    ...     languages=["javascript", "typescript"],
    ... )
    >>> engine = RuleEngine([rule])
    >>> results = engine.check_content("console.log('debug')", language="javascript")
    >>> results[0].passed
    False
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .parser import DiffFile, ParsedFile


class RuleType(Enum):
    """Enumeration of supported rule pattern matching types.

    Determines how the rule pattern is interpreted and matched against
    source code content.

    Attributes:
        REGEX: Pattern is interpreted as a regular expression.
            Supports full Python regex syntax including groups,
            lookahead/lookbehind, and quantifiers.
        LITERAL: Pattern is matched as an exact string.
            Faster for simple string matching but no regex features.
    """

    REGEX = "regex"
    LITERAL = "literal"


class Severity(Enum):
    """Severity level classification for rule violations.

    Used to categorize the importance of rule matches, enabling
    downstream systems to filter or prioritize violations.

    Attributes:
        ERROR: Critical violation that should block PR merges.
            Examples: hardcoded secrets, security vulnerabilities.
        WARNING: Important issue that should be reviewed.
            Examples: deprecated API usage, missing error handling.
        INFO: Informational finding for awareness.
            Examples: TODO comments, code style suggestions.
    """

    ERROR = "error"
    WARNING = "warning"
    INFO = "info"

    @classmethod
    def from_string(cls, value: str) -> Severity:
        """Create a Severity instance from a string value.

        Performs case-insensitive matching against severity names.

        Args:
            value: String representation of severity level.
                Must be one of 'error', 'warning', or 'info' (case-insensitive).

        Returns:
            The corresponding Severity enum member.

        Raises:
            ValueError: If the value does not match any severity level.
        """
        return cls(value.lower())


@dataclass
class RuleMatch:
    """Represents a single pattern match within source content.

    Captures the precise location and context of where a rule pattern
    was found, enabling accurate reporting and inline PR comments.

    Attributes:
        line_no: 1-indexed line number where the match occurred.
        column_start: 1-indexed starting column of the match.
        column_end: 1-indexed ending column (exclusive) of the match.
        matched_text: The actual text that matched the pattern.
        line_content: The full content of the line containing the match.

    Example:
        For content "const x = console.log('test');" on line 5,
        a match for "console.log" would produce:
            RuleMatch(
                line_no=5,
                column_start=11,
                column_end=22,
                matched_text="console.log",
                line_content="const x = console.log('test');",
            )
    """

    line_no: int
    column_start: int
    column_end: int
    matched_text: str
    line_content: str


@dataclass
class RuleResult:
    """Aggregated result of applying a single rule to content.

    Contains the rule identifier, pass/fail status, and all matches
    found during evaluation. Used by the RuleEngine to collect and
    report findings across multiple rules.

    Attributes:
        rule_id: Unique identifier of the rule that was executed.
        passed: True if no matches were found (rule passed), False otherwise.
        matches: List of all RuleMatch instances found during evaluation.
        message: Human-readable message describing the rule violation.
        severity: Severity level of the rule for prioritization.
    """

    rule_id: str
    passed: bool
    matches: list[RuleMatch] = field(default_factory=list)
    message: str = ""
    severity: Severity = Severity.WARNING


@dataclass
class Rule:
    """Defines a pattern-matching rule for code analysis.

    A Rule encapsulates the pattern to match, the languages it applies to,
    and the message and severity to report when violations are found.
    Rules can use either regex or literal string matching.

    Attributes:
        id: Unique identifier for the rule (e.g., "SEC-001", "no-console").
        name: Human-readable name for the rule.
        pattern: The pattern to match (regex or literal string).
        message: Message displayed when the rule is violated.
        severity: How critical the violation is (default: WARNING).
        rule_type: Whether pattern is regex or literal (default: REGEX).
        languages: List of languages this rule applies to. Use ["*"] for all.
        remediation: Guidance on how to fix the violation.
        flags: Regex flags string. Supported: 'i' (case-insensitive),
            'm' (multiline), 's' (dotall - dot matches newline).

    Example:
        >>> rule = Rule(
        ...     id="no-hardcoded-secret",
        ...     name="No Hardcoded Secrets",
        ...     pattern=r"(api_key|password|secret)\s*=\s*['\"][^'\"]+['\"]",
        ...     message="Do not hardcode secrets in source code",
        ...     severity=Severity.ERROR,
        ...     flags="i",  # Case-insensitive matching
        ... )
    """

    id: str
    name: str
    pattern: str
    message: str
    severity: Severity = Severity.WARNING
    rule_type: RuleType = RuleType.REGEX
    languages: list[str] = field(default_factory=lambda: ["*"])
    remediation: str = ""
    flags: str = ""

    def __post_init__(self) -> None:
        """Compile the regex pattern after dataclass initialization.

        For REGEX rule types, compiles the pattern with any specified flags
        for efficient repeated matching. The compiled pattern is stored
        in the private _compiled attribute.

        Supported flags in self.flags string:
            'i': re.IGNORECASE - Case-insensitive matching
            'm': re.MULTILINE - ^ and $ match line boundaries
            's': re.DOTALL - Dot matches newline characters
        """
        self._compiled: re.Pattern[str] | None = None
        if self.rule_type == RuleType.REGEX:
            # Build regex flags from the flags string
            regex_flags = 0
            if "i" in self.flags:
                regex_flags |= re.IGNORECASE
            if "m" in self.flags:
                regex_flags |= re.MULTILINE
            if "s" in self.flags:
                regex_flags |= re.DOTALL
            self._compiled = re.compile(self.pattern, regex_flags)

    def applies_to_language(self, language: str) -> bool:
        """Determine if this rule should be applied to a given language.

        Performs case-insensitive matching against the rule's language list.
        A wildcard "*" in the languages list matches any language.

        Args:
            language: The programming language to check (e.g., "python", "javascript").

        Returns:
            True if the rule applies to the specified language, False otherwise.
        """
        if "*" in self.languages:
            return True
        return language.lower() in [lang.lower() for lang in self.languages]

    def check_content(self, content: str, file_path: str = "") -> RuleResult:
        """Execute this rule against arbitrary text content.

        Scans the content line by line, finding all pattern matches.
        For regex rules, uses the pre-compiled pattern. For literal rules,
        performs exact string matching.

        Args:
            content: The text content to scan for rule violations.
            file_path: Optional file path for context (not used in matching).

        Returns:
            A RuleResult containing all matches found. The result's 'passed'
            attribute is True if no matches were found.
        """
        matches: list[RuleMatch] = []
        lines = content.split("\n")

        if self.rule_type == RuleType.REGEX and self._compiled:
            # Use pre-compiled regex for efficient matching
            for line_no, line in enumerate(lines, start=1):
                for match in self._compiled.finditer(line):
                    matches.append(
                        RuleMatch(
                            line_no=line_no,
                            column_start=match.start() + 1,  # Convert to 1-indexed
                            column_end=match.end() + 1,
                            matched_text=match.group(),
                            line_content=line,
                        )
                    )
        elif self.rule_type == RuleType.LITERAL:
            # Scan for literal string occurrences
            for line_no, line in enumerate(lines, start=1):
                start = 0
                # Find all occurrences of the pattern in this line
                while True:
                    idx = line.find(self.pattern, start)
                    if idx == -1:
                        break
                    matches.append(
                        RuleMatch(
                            line_no=line_no,
                            column_start=idx + 1,  # Convert to 1-indexed
                            column_end=idx + len(self.pattern) + 1,
                            matched_text=self.pattern,
                            line_content=line,
                        )
                    )
                    start = idx + 1  # Continue searching after this match

        return RuleResult(
            rule_id=self.id,
            passed=len(matches) == 0,
            matches=matches,
            message=self.message,
            severity=self.severity,
        )

    def check_file(self, parsed_file: ParsedFile) -> RuleResult:
        """Execute this rule against a parsed source file.

        Convenience method that extracts content from a ParsedFile
        and delegates to check_content.

        Args:
            parsed_file: A ParsedFile instance containing the file content and path.

        Returns:
            A RuleResult with all matches found in the file.
        """
        return self.check_content(parsed_file.content, parsed_file.path)

    def check_diff_additions(self, diff_file: DiffFile) -> RuleResult:
        """Execute this rule against only the added lines in a diff.

        This is the primary method for PR analysis, as it focuses only on
        new code being introduced. Deleted or unchanged lines are ignored.

        Args:
            diff_file: A DiffFile instance containing parsed diff information.

        Returns:
            A RuleResult with matches found only in added lines.
            Line numbers correspond to the new file's line numbers.
        """
        matches: list[RuleMatch] = []

        # Iterate only over lines that were added in this diff
        for line_no, content in diff_file.added_lines():
            if self.rule_type == RuleType.REGEX and self._compiled:
                for match in self._compiled.finditer(content):
                    matches.append(
                        RuleMatch(
                            line_no=line_no,
                            column_start=match.start() + 1,
                            column_end=match.end() + 1,
                            matched_text=match.group(),
                            line_content=content,
                        )
                    )
            elif self.rule_type == RuleType.LITERAL:
                start = 0
                while True:
                    idx = content.find(self.pattern, start)
                    if idx == -1:
                        break
                    matches.append(
                        RuleMatch(
                            line_no=line_no,
                            column_start=idx + 1,
                            column_end=idx + len(self.pattern) + 1,
                            matched_text=self.pattern,
                            line_content=content,
                        )
                    )
                    start = idx + 1

        return RuleResult(
            rule_id=self.id,
            passed=len(matches) == 0,
            matches=matches,
            message=self.message,
            severity=self.severity,
        )


class RuleEngine:
    """Orchestrates execution of multiple rules against code content.

    The RuleEngine is the primary interface for running policy checks.
    It manages a collection of rules and provides methods to check content,
    files, or diffs against all applicable rules based on language.

    The engine handles:
    - Language detection from file extensions
    - Filtering rules by applicable languages
    - Aggregating results from multiple rules

    Example:
        >>> engine = RuleEngine()
        >>> engine.add_rule(Rule(
        ...     id="no-print",
        ...     name="No Print Statements",
        ...     pattern=r"\\bprint\\(",
        ...     message="Use logging instead of print",
        ...     languages=["python"],
        ... ))
        >>> results = engine.check_content("print('hello')", language="python")
        >>> [r for r in results if not r.passed]  # Get violations
        [RuleResult(rule_id='no-print', passed=False, ...)]

    Attributes:
        rules: List of Rule instances managed by this engine.
    """

    def __init__(self, rules: list[Rule] | None = None) -> None:
        """Initialize the RuleEngine with an optional list of rules.

        Args:
            rules: Initial list of rules to add to the engine.
                If None, the engine starts with an empty rule set.
        """
        self.rules = rules or []

    def add_rule(self, rule: Rule) -> None:
        """Add a single rule to the engine's rule set.

        Args:
            rule: The Rule instance to add.
        """
        self.rules.append(rule)

    def check_content(
        self,
        content: str,
        file_path: str = "",
        language: str = "*",
    ) -> list[RuleResult]:
        """Execute all applicable rules against text content.

        Filters rules by language and runs each matching rule against
        the provided content, collecting all results.

        Args:
            content: The text content to check.
            file_path: Optional file path for context in results.
            language: Programming language for rule filtering.
                Use "*" to apply all rules regardless of language.

        Returns:
            List of RuleResult instances, one per applicable rule.
            Both passing and failing results are included.
        """
        results = []
        for rule in self.rules:
            if rule.applies_to_language(language):
                result = rule.check_content(content, file_path)
                results.append(result)
        return results

    def check_file(
        self,
        parsed_file: ParsedFile,
        language: str | None = None,
    ) -> list[RuleResult]:
        """Execute all applicable rules against a parsed file.

        Automatically detects the language from the file extension
        if not explicitly provided.

        Args:
            parsed_file: A ParsedFile instance containing file content and path.
            language: Optional language override. If None, detected from file path.

        Returns:
            List of RuleResult instances, one per applicable rule.
        """
        lang = language or self._detect_language(parsed_file.path)
        results = []
        for rule in self.rules:
            if rule.applies_to_language(lang):
                result = rule.check_file(parsed_file)
                results.append(result)
        return results

    def check_diff(
        self,
        diff_file: DiffFile,
        language: str | None = None,
    ) -> list[RuleResult]:
        """Execute all applicable rules against added lines in a diff.

        This is the primary method for PR analysis workflows. Only newly
        added lines are checked, allowing focus on new code being introduced.

        Args:
            diff_file: A DiffFile instance containing parsed diff information.
            language: Optional language override. If None, detected from file path.

        Returns:
            List of RuleResult instances, one per applicable rule.
            Results contain only matches from added lines.
        """
        lang = language or self._detect_language(diff_file.path)
        results = []
        for rule in self.rules:
            if rule.applies_to_language(lang):
                result = rule.check_diff_additions(diff_file)
                results.append(result)
        return results

    def _detect_language(self, path: str) -> str:
        """Detect programming language from a file path's extension.

        Uses a built-in mapping of common file extensions to language
        identifiers. Returns "*" for unrecognized extensions, which
        will match rules with wildcard language filters.

        Args:
            path: File path to analyze (only the extension is used).

        Returns:
            Language identifier string (e.g., "python", "javascript"),
            or "*" if the extension is not recognized.
        """
        # Mapping of file extensions to language identifiers
        ext_map = {
            ".py": "python",
            ".js": "javascript",
            ".ts": "typescript",
            ".tsx": "typescript",
            ".jsx": "javascript",
            ".java": "java",
            ".go": "go",
            ".rs": "rust",
            ".rb": "ruby",
            ".php": "php",
            ".cs": "csharp",
            ".cpp": "cpp",
            ".c": "c",
            ".h": "c",
            ".hpp": "cpp",
            ".swift": "swift",
            ".kt": "kotlin",
            ".scala": "scala",
            ".sh": "bash",
            ".bash": "bash",
            ".zsh": "bash",
            ".yaml": "yaml",
            ".yml": "yaml",
            ".json": "json",
            ".xml": "xml",
            ".html": "html",
            ".css": "css",
            ".scss": "scss",
            ".less": "less",
            ".md": "markdown",
            ".sql": "sql",
        }
        for ext, lang in ext_map.items():
            if path.endswith(ext):
                return lang
        return "*"
