"""AST-based complexity analysis for YARA rules."""

from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any

from yaraast.ast.strings import HexString, PlainString, RegexString
from yaraast.visitor import ASTVisitor
from yaraast.parser import Parser
from yaraast.ast.base import ASTNode, YaraFile
from yaraast.ast.conditions import ForExpression, ForOfExpression, OfExpression
from yaraast.ast.expressions import BinaryExpression, UnaryExpression
from yaraast.ast.rules import Rule

if TYPE_CHECKING:
    pass


@dataclass
class ComplexityMetrics:
    """Complexity metrics for YARA rules."""

    # File-level metrics
    total_rules: int = 0
    total_imports: int = 0
    total_includes: int = 0

    # Rule-level metrics
    rules_with_strings: int = 0
    rules_with_meta: int = 0
    rules_with_tags: int = 0
    private_rules: int = 0
    global_rules: int = 0

    # String complexity
    total_strings: int = 0
    plain_strings: int = 0
    hex_strings: int = 0
    regex_strings: int = 0
    strings_with_modifiers: int = 0

    # Condition complexity
    max_condition_depth: int = 0
    avg_condition_depth: float = 0.0
    total_binary_ops: int = 0
    total_unary_ops: int = 0
    for_expressions: int = 0
    for_of_expressions: int = 0
    of_expressions: int = 0

    # Pattern complexity
    hex_wildcards: int = 0
    hex_jumps: int = 0
    hex_alternatives: int = 0
    regex_groups: int = 0
    regex_quantifiers: int = 0

    # Quality metrics
    unused_strings: list[str] = field(default_factory=list)
    complex_rules: list[str] = field(default_factory=list)  # Rules exceeding thresholds
    cyclomatic_complexity: dict[str, int] = field(default_factory=dict)

    # Dependencies
    string_dependencies: dict[str, set[str]] = field(default_factory=dict)
    module_usage: dict[str, int] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        """Convert metrics to dictionary for serialization."""
        return {
            "file_metrics": {
                "total_rules": self.total_rules,
                "total_imports": self.total_imports,
                "total_includes": self.total_includes,
            },
            "rule_metrics": {
                "rules_with_strings": self.rules_with_strings,
                "rules_with_meta": self.rules_with_meta,
                "rules_with_tags": self.rules_with_tags,
                "private_rules": self.private_rules,
                "global_rules": self.global_rules,
            },
            "string_metrics": {
                "total_strings": self.total_strings,
                "plain_strings": self.plain_strings,
                "hex_strings": self.hex_strings,
                "regex_strings": self.regex_strings,
                "strings_with_modifiers": self.strings_with_modifiers,
            },
            "condition_metrics": {
                "max_condition_depth": self.max_condition_depth,
                "avg_condition_depth": self.avg_condition_depth,
                "total_binary_ops": self.total_binary_ops,
                "total_unary_ops": self.total_unary_ops,
                "for_expressions": self.for_expressions,
                "for_of_expressions": self.for_of_expressions,
                "of_expressions": self.of_expressions,
            },
            "pattern_metrics": {
                "hex_wildcards": self.hex_wildcards,
                "hex_jumps": self.hex_jumps,
                "hex_alternatives": self.hex_alternatives,
                "regex_groups": self.regex_groups,
                "regex_quantifiers": self.regex_quantifiers,
            },
            "quality_metrics": {
                "unused_strings": self.unused_strings,
                "complex_rules": self.complex_rules,
                "cyclomatic_complexity": self.cyclomatic_complexity,
            },
            "dependencies": {
                "string_dependencies": {k: list(v) for k, v in self.string_dependencies.items()},
                "module_usage": self.module_usage,
            },
        }

    def get_quality_score(self) -> float:
        """Calculate overall quality score (0-100)."""
        score = 100.0

        # Deduct for complexity issues
        if self.max_condition_depth > 8:
            score -= 20
        elif self.max_condition_depth > 5:
            score -= 10

        # Deduct for unused strings
        if self.unused_strings:
            score -= min(20, len(self.unused_strings) * 5)

        # Deduct for very complex rules
        if self.complex_rules:
            score -= min(25, len(self.complex_rules) * 10)

        # Bonus for good practices
        if self.rules_with_meta / max(1, self.total_rules) > 0.8:
            score += 5

        return max(0.0, score)

    def get_complexity_grade(self) -> str:
        """Get letter grade for complexity."""
        score = self.get_quality_score()
        if score >= 90:
            return "A"
        if score >= 80:
            return "B"
        if score >= 70:
            return "C"
        if score >= 60:
            return "D"
        return "F"


class ComplexityAnalyzer(ASTVisitor[None]):
    """Analyzes AST complexity metrics."""

    def __init__(self):
        self.metrics = ComplexityMetrics()
        self._current_rule: Rule | None = None
        self._condition_depths: list[int] = []
        self._current_depth = 0
        self._string_usage: dict[str, set[str]] = defaultdict(set)  # string_id -> rule_names
        self._rule_strings: dict[str, set[str]] = defaultdict(set)  # rule_name -> string_ids

    def analyze(self, ast: YaraFile) -> ComplexityMetrics:
        """Analyze AST and return complexity metrics."""
        self.metrics = ComplexityMetrics()
        self._condition_depths.clear()
        self._current_depth = 0
        self._string_usage.clear()
        self._rule_strings.clear()

        # File-level metrics
        self.metrics.total_rules = len(ast.rules)
        self.metrics.total_imports = len(ast.imports)
        self.metrics.total_includes = len(ast.includes)

        # Module usage from imports
        for imp in ast.imports:
            self.metrics.module_usage[imp.module] = self.metrics.module_usage.get(imp.module, 0) + 1

        # Analyze each rule
        for rule in ast.rules:
            self._analyze_rule(rule)

        # Post-analysis calculations
        self._calculate_derived_metrics()

        return self.metrics

    def _analyze_rule(self, rule: Rule) -> None:
        """Analyze a single rule."""
        self._current_rule = rule

        # Rule modifiers
        if "private" in rule.modifiers:
            self.metrics.private_rules += 1
        if "global" in rule.modifiers:
            self.metrics.global_rules += 1

        # Rule sections
        if rule.strings:
            self.metrics.rules_with_strings += 1
            self._analyze_strings(rule)

        if rule.meta:
            self.metrics.rules_with_meta += 1

        if rule.tags:
            self.metrics.rules_with_tags += 1

        # Condition analysis
        if rule.condition:
            self._current_depth = 0
            self.visit(rule.condition)
            rule_max_depth = max(self._condition_depths) if self._condition_depths else 0
            self.metrics.max_condition_depth = max(self.metrics.max_condition_depth, rule_max_depth)

            # Calculate cyclomatic complexity for this rule
            self.metrics.cyclomatic_complexity[rule.name] = self._calculate_cyclomatic_complexity()

            # Check if rule is complex
            if rule_max_depth > 6 or self.metrics.cyclomatic_complexity[rule.name] > 10:
                self.metrics.complex_rules.append(rule.name)

    def _analyze_strings(self, rule: Rule) -> None:
        """Analyze string definitions in a rule."""
        for string_def in rule.strings:
            self.metrics.total_strings += 1
            self._rule_strings[rule.name].add(string_def.identifier)

            if isinstance(string_def, PlainString):
                self.metrics.plain_strings += 1
                if string_def.modifiers:
                    self.metrics.strings_with_modifiers += 1

            elif isinstance(string_def, HexString):
                self.metrics.hex_strings += 1
                if string_def.modifiers:
                    self.metrics.strings_with_modifiers += 1

                # Analyze hex tokens
                self._analyze_hex_tokens(string_def.tokens)

            elif isinstance(string_def, RegexString):
                self.metrics.regex_strings += 1
                if string_def.modifiers:
                    self.metrics.strings_with_modifiers += 1

                # Analyze regex complexity
                self._analyze_regex_complexity(string_def.regex)

    def _analyze_hex_tokens(self, tokens: list) -> None:
        """Analyze hex string tokens."""
        from yaraast.ast.strings import HexAlternative, HexJump, HexWildcard

        for token in tokens:
            if isinstance(token, HexWildcard):
                self.metrics.hex_wildcards += 1
            elif isinstance(token, HexJump):
                self.metrics.hex_jumps += 1
            elif isinstance(token, HexAlternative):
                self.metrics.hex_alternatives += 1

    def _analyze_regex_complexity(self, regex: str) -> None:
        """Analyze regex pattern complexity."""
        import re

        # Count groups
        self.metrics.regex_groups += len(re.findall(r"\([^?]", regex))

        # Count quantifiers
        quantifiers = r"[*+?{]"
        self.metrics.regex_quantifiers += len(re.findall(quantifiers, regex))

    def _calculate_cyclomatic_complexity(self) -> int:
        """Calculate cyclomatic complexity for current rule."""
        # Start with 1 (linear path)
        complexity = 1

        # Add complexity for each decision point
        complexity += self.metrics.total_binary_ops  # Each binary op is a decision
        complexity += self.metrics.for_expressions
        complexity += self.metrics.for_of_expressions
        complexity += self.metrics.of_expressions

        return complexity

    def _calculate_derived_metrics(self) -> None:
        """Calculate derived metrics after analysis."""
        # Average condition depth
        if self._condition_depths:
            self.metrics.avg_condition_depth = sum(self._condition_depths) / len(
                self._condition_depths
            )

        # Find unused strings
        for rule_name, string_ids in self._rule_strings.items():
            used_strings = self._string_usage.get(rule_name, set())
            unused = string_ids - used_strings
            for unused_string in unused:
                self.metrics.unused_strings.append(f"{rule_name}:{unused_string}")

        # String dependencies
        for rule_name, string_ids in self._string_usage.items():
            if string_ids:
                self.metrics.string_dependencies[rule_name] = string_ids

    # Visitor methods
    def visit_binary_expression(self, node: BinaryExpression) -> None:
        """Visit binary expression and track complexity."""
        self._current_depth += 1
        self._condition_depths.append(self._current_depth)
        self.metrics.total_binary_ops += 1

        self.visit(node.left)
        self.visit(node.right)
        self._current_depth -= 1

    def visit_unary_expression(self, node: UnaryExpression) -> None:
        """Visit unary expression."""
        self.metrics.total_unary_ops += 1
        self.visit(node.operand)

    def visit_for_expression(self, node: ForExpression) -> None:
        """Visit for expression."""
        self._current_depth += 1
        self._condition_depths.append(self._current_depth)
        self.metrics.for_expressions += 1

        self.visit(node.iterable)
        self.visit(node.body)
        self._current_depth -= 1

    def visit_for_of_expression(self, node: ForOfExpression) -> None:
        """Visit for-of expression."""
        self._current_depth += 1
        self._condition_depths.append(self._current_depth)
        self.metrics.for_of_expressions += 1

        self.visit(node.string_set)
        if node.condition:
            self.visit(node.condition)
        self._current_depth -= 1

    def visit_of_expression(self, node: OfExpression) -> None:
        """Visit of expression."""
        self.metrics.of_expressions += 1

        if hasattr(node.quantifier, "accept"):
            self.visit(node.quantifier)
        if hasattr(node.string_set, "accept"):
            self.visit(node.string_set)

    def visit_string_identifier(self, node) -> None:
        """Track string usage."""
        if self._current_rule:
            self._string_usage[self._current_rule.name].add(node.name)

    # Required visitor methods (minimal implementations)
    def visit_yara_file(self, node) -> None:
        pass

    def visit_import(self, node) -> None:
        pass

    def visit_include(self, node) -> None:
        pass

    def visit_rule(self, node) -> None:
        pass

    def visit_tag(self, node) -> None:
        pass

    def visit_string_definition(self, node) -> None:
        pass

    def visit_plain_string(self, node) -> None:
        pass

    def visit_hex_string(self, node) -> None:
        pass

    def visit_regex_string(self, node) -> None:
        pass

    def visit_string_modifier(self, node) -> None:
        pass

    def visit_hex_token(self, node) -> None:
        pass

    def visit_hex_byte(self, node) -> None:
        pass

    def visit_hex_wildcard(self, node) -> None:
        pass

    def visit_hex_jump(self, node) -> None:
        pass

    def visit_hex_alternative(self, node) -> None:
        pass

    def visit_hex_nibble(self, node) -> None:
        pass

    def visit_expression(self, node) -> None:
        pass

    def visit_identifier(self, node) -> None:
        pass

    def visit_string_count(self, node) -> None:
        pass

    def visit_string_offset(self, node) -> None:
        pass

    def visit_string_length(self, node) -> None:
        pass

    def visit_integer_literal(self, node) -> None:
        pass

    def visit_double_literal(self, node) -> None:
        pass

    def visit_string_literal(self, node) -> None:
        pass

    def visit_regex_literal(self, node) -> None:
        pass

    def visit_boolean_literal(self, node) -> None:
        pass

    def visit_parentheses_expression(self, node) -> None:
        self.visit(node.expression)

    def visit_set_expression(self, node) -> None:
        for elem in node.elements:
            self.visit(elem)

    def visit_range_expression(self, node) -> None:
        self.visit(node.low)
        self.visit(node.high)

    def visit_function_call(self, node) -> None:
        for arg in node.arguments:
            self.visit(arg)

    def visit_array_access(self, node) -> None:
        self.visit(node.array)
        self.visit(node.index)

    def visit_member_access(self, node) -> None:
        self.visit(node.object)

    def visit_condition(self, node) -> None:
        pass

    def visit_at_expression(self, node) -> None:
        self.visit(node.offset)

    def visit_in_expression(self, node) -> None:
        self.visit(node.range)

    def visit_meta(self, node) -> None:
        pass

    def visit_module_reference(self, node) -> None:
        pass

    def visit_dictionary_access(self, node) -> None:
        self.visit(node.object)

    def visit_comment(self, node) -> None:
        pass

    def visit_comment_group(self, node) -> None:
        pass

    def visit_defined_expression(self, node) -> None:
        self.visit(node.expression)

    def visit_string_operator_expression(self, node) -> None:
        self.visit(node.left)
        self.visit(node.right)

    # Add missing abstract methods
    def visit_extern_import(self, node) -> None:
        pass

    def visit_extern_namespace(self, node) -> None:
        pass

    def visit_extern_rule(self, node) -> None:
        pass

    def visit_extern_rule_reference(self, node) -> None:
        pass

    def visit_in_rule_pragma(self, node) -> None:
        pass

    def visit_pragma(self, node) -> None:
        pass

    def visit_pragma_block(self, node) -> None:
        pass


@dataclass
class ComplexityReport:
    """Report of complexity analysis for a rule."""
    
    rule_name: str
    total_complexity: int
    cyclomatic_complexity: int
    cognitive_complexity: int
    string_complexity: int
    condition_complexity: int


class ComplexityCalculator(ASTVisitor[int]):
    """Calculate complexity scores for AST nodes."""
    
    def __init__(self):
        self._cognitive_depth = 0
        self._in_logical_op = False
    
    def calculate(self, node: ASTNode) -> int:
        """Calculate complexity for an AST node."""
        if node is None:
            return 0
        return node.accept(self)
    
    # Simple expressions - base complexity 1
    def visit_boolean_literal(self, node) -> int:
        return 1
    
    def visit_integer_literal(self, node) -> int:
        return 1
    
    def visit_double_literal(self, node) -> int:
        return 1
    
    def visit_string_literal(self, node) -> int:
        return 1
    
    def visit_regex_literal(self, node) -> int:
        return 2  # Regex slightly more complex
    
    def visit_identifier(self, node) -> int:
        return 1
    
    def visit_string_identifier(self, node) -> int:
        return 1
    
    # Binary expressions - add complexity for operators
    def visit_binary_expression(self, node: BinaryExpression) -> int:
        complexity = 1  # Base
        
        # Logical operators add more complexity
        if node.operator in ("and", "or"):
            complexity += 2
            self._in_logical_op = True
        else:
            complexity += 1
        
        # Add child complexity
        complexity += self.calculate(node.left)
        complexity += self.calculate(node.right)
        
        self._in_logical_op = False
        return complexity
    
    # Unary expressions
    def visit_unary_expression(self, node: UnaryExpression) -> int:
        return 2 + self.calculate(node.operand)
    
    # Function calls
    def visit_function_call(self, node) -> int:
        complexity = 2  # Base function call
        for arg in node.arguments:
            complexity += self.calculate(arg)
        return complexity
    
    # String operations
    def visit_string_count(self, node) -> int:
        return 2
    
    def visit_string_offset(self, node) -> int:
        complexity = 2
        if node.index:
            complexity += self.calculate(node.index)
        return complexity
    
    def visit_string_length(self, node) -> int:
        complexity = 2
        if node.index:
            complexity += self.calculate(node.index)
        return complexity
    
    # Complex expressions
    def visit_for_expression(self, node: ForExpression) -> int:
        self._cognitive_depth += 1
        complexity = 5  # High base for loops
        complexity += self.calculate(node.iterable)
        complexity += self.calculate(node.body)
        self._cognitive_depth -= 1
        return complexity
    
    def visit_for_of_expression(self, node: ForOfExpression) -> int:
        self._cognitive_depth += 1
        complexity = 5  # High base for loops
        if hasattr(node.string_set, "accept"):
            complexity += self.calculate(node.string_set)
        else:
            complexity += len(node.string_set) if isinstance(node.string_set, list) else 1
        if node.condition:
            complexity += self.calculate(node.condition)
        self._cognitive_depth -= 1
        return complexity
    
    def visit_of_expression(self, node: OfExpression) -> int:
        complexity = 4  # Base for of expressions
        if hasattr(node.string_set, "accept"):
            complexity += self.calculate(node.string_set)
        elif isinstance(node.string_set, list):
            complexity += len(node.string_set)
        return complexity
    
    # Container expressions
    def visit_set_expression(self, node) -> int:
        complexity = 1
        for elem in node.elements:
            complexity += self.calculate(elem)
        return complexity
    
    def visit_range_expression(self, node) -> int:
        return 1 + self.calculate(node.low) + self.calculate(node.high)
    
    def visit_array_access(self, node) -> int:
        return 1 + self.calculate(node.array) + self.calculate(node.index)
    
    def visit_member_access(self, node) -> int:
        return 1 + self.calculate(node.object)
    
    def visit_parentheses_expression(self, node) -> int:
        return self.calculate(node.expression)
    
    # Other expressions
    def visit_at_expression(self, node) -> int:
        return 2 + self.calculate(node.offset)
    
    def visit_in_expression(self, node) -> int:
        return 2 + self.calculate(node.range)
    
    def visit_defined_expression(self, node) -> int:
        return 1 + self.calculate(node.expression)
    
    def visit_string_operator_expression(self, node) -> int:
        return 2 + self.calculate(node.left) + self.calculate(node.right)
    
    def visit_dictionary_access(self, node) -> int:
        complexity = 1 + self.calculate(node.object)
        if hasattr(node.key, "accept"):
            complexity += self.calculate(node.key)
        return complexity
    
    # Default/unused visitor methods
    def visit_yara_file(self, node) -> int:
        return 0
    
    def visit_import(self, node) -> int:
        return 0
    
    def visit_include(self, node) -> int:
        return 0
    
    def visit_rule(self, node) -> int:
        return 0
    
    def visit_tag(self, node) -> int:
        return 0
    
    def visit_string_definition(self, node) -> int:
        return 0
    
    def visit_plain_string(self, node) -> int:
        return 0
    
    def visit_hex_string(self, node) -> int:
        return 0
    
    def visit_regex_string(self, node) -> int:
        return 0
    
    def visit_string_modifier(self, node) -> int:
        return 0
    
    def visit_hex_token(self, node) -> int:
        return 0
    
    def visit_hex_byte(self, node) -> int:
        return 0
    
    def visit_hex_wildcard(self, node) -> int:
        return 0
    
    def visit_hex_jump(self, node) -> int:
        return 0
    
    def visit_hex_alternative(self, node) -> int:
        return 0
    
    def visit_hex_nibble(self, node) -> int:
        return 0
    
    def visit_expression(self, node) -> int:
        return 0
    
    def visit_condition(self, node) -> int:
        return 0
    
    def visit_meta(self, node) -> int:
        return 0
    
    def visit_module_reference(self, node) -> int:
        return 0
    
    def visit_comment(self, node) -> int:
        return 0
    
    def visit_comment_group(self, node) -> int:
        return 0
    
    # Add missing abstract methods
    def visit_extern_import(self, node) -> int:
        return 0
    
    def visit_extern_namespace(self, node) -> int:
        return 0
    
    def visit_extern_rule(self, node) -> int:
        return 0
    
    def visit_extern_rule_reference(self, node) -> int:
        return 0
    
    def visit_in_rule_pragma(self, node) -> int:
        return 0
    
    def visit_pragma(self, node) -> int:
        return 0
    
    def visit_pragma_block(self, node) -> int:
        return 0


# Convenience functions
def calculate_rule_complexity(rule: Rule) -> int:
    """Calculate total complexity for a rule."""
    calc = ComplexityCalculator()
    
    # Base complexity
    complexity = 1
    
    # Add string complexity
    if rule.strings:
        complexity += len(rule.strings)
        for string in rule.strings:
            if isinstance(string, RegexString):
                complexity += 2
            elif isinstance(string, HexString):
                complexity += 1
    
    # Add condition complexity
    if rule.condition:
        complexity += calc.calculate(rule.condition)
    
    # Add modifier complexity
    if "private" in rule.modifiers:
        complexity += 1
    if "global" in rule.modifiers:
        complexity += 1
    
    return complexity


def calculate_expression_complexity(expr: ASTNode) -> int:
    """Calculate complexity for an expression."""
    calc = ComplexityCalculator()
    return calc.calculate(expr)


def calculate_cyclomatic_complexity(expr: ASTNode) -> int:
    """Calculate cyclomatic complexity (branching)."""
    # Simple approximation based on decision points
    complexity = 1
    
    if hasattr(expr, "operator") and expr.operator in ("and", "or"):
        complexity += 1
    
    # Recursively check children
    if hasattr(expr, "left"):
        complexity += calculate_cyclomatic_complexity(expr.left) - 1
    if hasattr(expr, "right"):
        complexity += calculate_cyclomatic_complexity(expr.right) - 1
    if hasattr(expr, "operand"):
        complexity += calculate_cyclomatic_complexity(expr.operand) - 1
    
    return complexity


def calculate_cognitive_complexity(expr: ASTNode) -> int:
    """Calculate cognitive complexity (mental effort)."""
    calc = ComplexityCalculator()
    base_complexity = calc.calculate(expr)
    
    # Add extra complexity for nesting
    if isinstance(expr, (ForExpression, ForOfExpression)):
        return base_complexity + 3
    
    return base_complexity


def generate_complexity_report(ast: YaraFile) -> dict[str, Any]:
    """Generate a comprehensive complexity report for a YARA file."""
    analyzer = ComplexityAnalyzer()
    metrics = analyzer.analyze(ast)
    
    rules_data = []
    for rule in ast.rules:
        complexity = calculate_rule_complexity(rule)
        cyclomatic = metrics.cyclomatic_complexity.get(rule.name, 1)
        cognitive = calculate_cognitive_complexity(rule.condition) if rule.condition else 0
        
        rules_data.append({
            "name": rule.name,
            "total_complexity": complexity,
            "cyclomatic_complexity": cyclomatic,
            "cognitive_complexity": cognitive,
            "strings": len(rule.strings) if rule.strings else 0,
            "modifiers": len(rule.modifiers),
        })
    
    return {
        "rules": rules_data,
        "summary": {
            "total_rules": len(ast.rules),
            "avg_complexity": sum(r["total_complexity"] for r in rules_data) / max(1, len(rules_data)),
            "max_complexity": max((r["total_complexity"] for r in rules_data), default=0),
            "quality_score": metrics.get_quality_score(),
            "quality_grade": metrics.get_complexity_grade(),
        },
        "metrics": metrics.to_dict(),
    }


def analyze_file_complexity(file_path: str | Path) -> dict[str, Any]:
    """Analyze complexity of a YARA file."""
    parser = Parser()
    
    with open(file_path, 'r') as f:
        content = f.read()
    
    ast = parser.parse(content)
    report = generate_complexity_report(ast)
    
    return {
        "file": str(file_path),
        "complexity": report,
    }
