"""Failure analysis for autonomous debugging.

This module categorizes failures and extracts rich context
for LLM hypothesis generation.

Uses the exception classification system for comprehensive
coverage of Python exceptions and domain-specific errors.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional

from autodebug.detection import ClassificationResult, ExceptionClassifier


@dataclass
class FailureCategory:
    """A category of failure with associated fix strategies.

    Note: This is maintained for backward compatibility.
    New code should use ClassificationResult from the detection module.
    """

    name: str
    pattern: str
    context_needed: List[str]
    fix_strategies: List[str]

    def matches(self, exception_type: str, message: str) -> bool:
        """Check if this category matches the failure."""
        full_text = f"{exception_type}: {message}"
        return bool(re.search(self.pattern, full_text, re.IGNORECASE))


# Standard failure categories
FAILURE_CATEGORIES = [
    FailureCategory(
        name="assertion_error",
        pattern=r"AssertionError",
        context_needed=["assertion_line", "compared_values"],
        fix_strategies=["adjust_assertion", "fix_value_source"],
    ),
    FailureCategory(
        name="attribute_error",
        pattern=r"AttributeError.*has no attribute",
        context_needed=["object_type", "missing_attr", "similar_attrs"],
        fix_strategies=["add_null_check", "fix_typo", "add_attribute"],
    ),
    FailureCategory(
        name="key_error",
        pattern=r"KeyError",
        context_needed=["dict_source", "expected_key", "available_keys"],
        fix_strategies=["use_get_default", "fix_key_name", "ensure_key_exists"],
    ),
    FailureCategory(
        name="type_error",
        pattern=r"TypeError",
        context_needed=["expected_type", "actual_type", "function_signature"],
        fix_strategies=["add_type_conversion", "fix_argument_order"],
    ),
    FailureCategory(
        name="value_error",
        pattern=r"ValueError",
        context_needed=["expected_value", "actual_value", "validation_context"],
        fix_strategies=["fix_input_validation", "add_default_value", "handle_edge_case"],
    ),
    FailureCategory(
        name="index_error",
        pattern=r"IndexError",
        context_needed=["list_length", "requested_index", "list_source"],
        fix_strategies=["add_bounds_check", "fix_index_calculation", "handle_empty_list"],
    ),
    FailureCategory(
        name="name_error",
        pattern=r"NameError.*not defined",
        context_needed=["undefined_name", "similar_names", "scope_context"],
        fix_strategies=["fix_typo", "add_import", "define_variable"],
    ),
    FailureCategory(
        name="import_error",
        pattern=r"(ImportError|ModuleNotFoundError)",
        context_needed=["module_name", "available_modules"],
        fix_strategies=["fix_import_path", "install_dependency", "fix_typo"],
    ),
    FailureCategory(
        name="http_error",
        pattern=r"(HTTPError|status.*(4\d\d|5\d\d)|Response.*[45]\d\d)",
        context_needed=["endpoint", "request_body", "response_body", "status_code"],
        fix_strategies=["fix_request", "handle_error", "mock_response", "fix_auth"],
    ),
    FailureCategory(
        name="sql_error",
        pattern=r"(IntegrityError|ProgrammingError|DataError|OperationalError)",
        context_needed=["query", "params", "table_schema"],
        fix_strategies=["fix_query", "add_migration", "handle_constraint"],
    ),
    FailureCategory(
        name="connection_error",
        pattern=r"(ConnectionError|ConnectionRefused|TimeoutError)",
        context_needed=["host", "port", "service"],
        fix_strategies=["add_retry", "fix_connection_params", "add_timeout_handling"],
    ),
    FailureCategory(
        name="json_error",
        pattern=r"(JSONDecodeError|json\.decoder)",
        context_needed=["raw_input", "expected_format"],
        fix_strategies=["fix_json_format", "add_validation", "handle_empty_response"],
    ),
]


@dataclass
class StackFrame:
    """A single frame in a stack trace."""

    file: str
    line: int
    function: str
    code: Optional[str] = None

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> StackFrame:
        return cls(
            file=data.get("file", ""),
            line=data.get("line", 0),
            function=data.get("function", ""),
            code=data.get("code"),
        )


@dataclass
class FailureAnalysis:
    """Complete analysis of a failure."""

    exception_type: str
    exception_message: str
    stack_trace: List[StackFrame]
    category: Optional[FailureCategory]
    context: Dict[str, Any]
    recommended_strategies: List[str]

    # Enhanced classification (Plan 51)
    classification: Optional[ClassificationResult] = None
    fingerprint: Optional[str] = None
    semantic_info: Optional[Dict[str, Any]] = None
    framework: Optional[str] = None

    @property
    def failure_location(self) -> str:
        """Get the location of the failure."""
        if self.stack_trace:
            frame = self.stack_trace[0]
            return f"{frame.file}:{frame.line}"
        return "unknown"

    @property
    def summary(self) -> str:
        """Get a one-line summary of the failure."""
        return f"{self.exception_type}: {self.exception_message[:100]}"

    @property
    def category_name(self) -> str:
        """Get category name from either old or new classification."""
        if self.classification:
            return self.classification.category
        if self.category:
            return self.category.name
        return "unknown"

    @property
    def root_causes(self) -> List[str]:
        """Get root causes from classification."""
        if self.classification:
            return self.classification.root_causes
        return []

    @property
    def is_recoverable(self) -> bool:
        """Check if the failure is typically recoverable."""
        if self.classification:
            return self.classification.is_recoverable
        return True


def categorize_failure(
    exception_type: str,
    message: str,
) -> Optional[FailureCategory]:
    """Categorize a failure based on exception type and message.

    Args:
        exception_type: The exception class name
        message: The exception message

    Returns:
        Matching FailureCategory or None
    """
    for category in FAILURE_CATEGORIES:
        if category.matches(exception_type, message):
            return category
    return None


def _get_source_lines(file_path: str, line_number: int, context: int = 5) -> List[str]:
    """Get source lines around a location.

    Args:
        file_path: Path to source file
        line_number: Line number (1-indexed)
        context: Number of lines before and after

    Returns:
        List of source lines with line numbers
    """
    try:
        path = Path(file_path)
        if not path.exists():
            import logging as _logging
            _logging.getLogger(__name__).warning(
                "Source file not found while extracting context: %s", file_path
            )
            return []

        lines = path.read_text().splitlines()
        start = max(0, line_number - context - 1)
        end = min(len(lines), line_number + context)

        result = []
        for i in range(start, end):
            marker = ">>>" if i == line_number - 1 else "   "
            result.append(f"{marker} {i + 1:4d} | {lines[i]}")
        return result
    except Exception:
        return []


def _get_function_source(file_path: str, function_name: str, line_number: int) -> str:
    """Extract the source of a function containing the failure.

    Uses AST-based extraction (L-02) for reliable function boundary
    detection. Falls back to string-based heuristic if AST parsing fails
    (e.g., syntax errors in the file being debugged).

    Args:
        file_path: Path to source file
        function_name: Name of the function
        line_number: Line number within the function

    Returns:
        Function source code
    """
    try:
        import ast as _ast

        path = Path(file_path)
        if not path.exists():
            return ""

        source = path.read_text()

        # Try AST-based extraction first (more reliable for nested classes,
        # decorators, and multiline signatures)
        try:
            tree = _ast.parse(source)
        except SyntaxError:
            # File has syntax errors (common when debugging) -- fall back
            return _get_function_source_fallback(source, function_name, line_number)

        # Collect all function/method definitions, including nested ones
        candidates = []
        for node in _ast.walk(tree):
            if isinstance(node, (_ast.FunctionDef, _ast.AsyncFunctionDef)):
                candidates.append(node)

        if not candidates:
            return "\n".join(_get_source_lines(file_path, line_number, 10))

        # Find the best matching function:
        # 1. Exact name match containing the failure line
        # 2. Any function containing the failure line
        best = None
        for node in candidates:
            end_line = getattr(node, "end_lineno", node.lineno + 1)
            if node.name == function_name and node.lineno <= line_number <= end_line:
                best = node
                break
        if best is None:
            for node in candidates:
                end_line = getattr(node, "end_lineno", node.lineno + 1)
                if node.lineno <= line_number <= end_line:
                    best = node
                    break
        if best is None:
            # Name match without line containment (function may have moved)
            for node in candidates:
                if node.name == function_name:
                    best = node
                    break

        if best is not None:
            segment = _ast.get_source_segment(source, best)
            if segment:
                return segment
            # Fallback: use line numbers from AST node
            lines = source.splitlines()
            end_line = getattr(best, "end_lineno", best.lineno + 20)
            return "\n".join(lines[best.lineno - 1:end_line])

        return "\n".join(_get_source_lines(file_path, line_number, 10))
    except Exception:
        return ""


def _get_function_source_fallback(
    source: str, function_name: str, line_number: int
) -> str:
    """String-based fallback for function extraction when AST parsing fails.

    Args:
        source: File source text
        function_name: Name of the function
        line_number: Line number within the function

    Returns:
        Function source code
    """
    lines = source.splitlines()
    func_start = None
    indent_level = None

    for i in range(min(line_number - 1, len(lines) - 1), -1, -1):
        line = lines[i]
        stripped = line.lstrip()
        if stripped.startswith(("def ", "async def ")):
            func_start = i
            indent_level = len(line) - len(stripped)
            break

    if func_start is None:
        start = max(0, line_number - 11)
        end = min(len(lines), line_number + 10)
        return "\n".join(lines[start:end])

    func_end = func_start + 1
    for i in range(func_start + 1, len(lines)):
        line = lines[i]
        if line.strip() == "":
            continue
        current_indent = len(line) - len(line.lstrip())
        if current_indent <= indent_level and line.strip():
            break
        func_end = i + 1

    return "\n".join(lines[func_start:func_end])


def _extract_imports(file_path: str) -> List[str]:
    """Extract import statements from a file.

    Args:
        file_path: Path to source file

    Returns:
        List of import statements
    """
    try:
        path = Path(file_path)
        if not path.exists():
            return []

        imports = []
        for line in path.read_text().splitlines():
            stripped = line.strip()
            if stripped.startswith(("import ", "from ")):
                imports.append(stripped)
            elif imports and not stripped.startswith("#") and stripped:
                # Stop after first non-import line
                break
        return imports
    except Exception:
        return []


def extract_context(
    exception: Dict[str, Any],
    causal_slice: Optional[Dict[str, Any]] = None,
    manifest: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """Extract rich context for LLM hypothesis generation.

    Args:
        exception: Exception info with type, message, stack
        causal_slice: Optional causal slice with last deps
        manifest: Optional run manifest

    Returns:
        Context dictionary for LLM
    """
    exc_type = exception.get("exc_type", exception.get("type", "Unknown"))
    message = exception.get("message", "")
    stack = exception.get("stack", [])

    # Parse stack frames
    frames = [StackFrame.from_dict(f) for f in stack]

    context = {
        # Failure info
        "exception_type": exc_type,
        "exception_message": message,
        "stack_trace": stack,
    }

    # Add source context from top frame
    if frames:
        top_frame = frames[0]
        context["failure_file"] = top_frame.file
        context["failure_line"] = top_frame.line
        context["failure_function"] = top_frame.function
        context["failure_code"] = "\n".join(
            _get_source_lines(top_frame.file, top_frame.line, 5)
        )
        context["failing_function_source"] = _get_function_source(
            top_frame.file, top_frame.function, top_frame.line
        )
        context["imports"] = _extract_imports(top_frame.file)

    # Add related function sources
    related_sources = []
    for frame in frames[1:5]:  # Next 4 frames
        source = _get_function_source(frame.file, frame.function, frame.line)
        if source:
            related_sources.append({
                "file": frame.file,
                "function": frame.function,
                "source": source,
            })
    context["related_functions"] = related_sources

    # Add causal slice info
    if causal_slice:
        context["last_http"] = causal_slice.get("last_http")
        context["last_sql"] = causal_slice.get("last_sql")
        context["related_deps"] = causal_slice.get("related_deps", [])

    # Add manifest info
    if manifest:
        context["test_command"] = manifest.get("entry_cmd", {}).get("argv", [])
        context["env_vars_accessed"] = list(
            manifest.get("env_fingerprint", {}).get("env_vars_read", {}).keys()
        )
        context["python_version"] = manifest.get("env_fingerprint", {}).get("python_version")

    return context


# Global classifier instance (lazy initialization)
_classifier: Optional[ExceptionClassifier] = None


def _get_classifier() -> ExceptionClassifier:
    """Get or create the global exception classifier."""
    global _classifier
    if _classifier is None:
        _classifier = ExceptionClassifier()
    return _classifier


def analyze_failure(
    exception: Dict[str, Any],
    causal_slice: Optional[Dict[str, Any]] = None,
    manifest: Optional[Dict[str, Any]] = None,
) -> FailureAnalysis:
    """Analyze a failure and extract all relevant information.

    Uses the enhanced exception classification system for comprehensive
    coverage and intelligent fix strategy recommendations.

    Args:
        exception: Exception info with type, message, stack
        causal_slice: Optional causal slice with last deps
        manifest: Optional run manifest

    Returns:
        FailureAnalysis with category, context, and strategies
    """
    exc_type = exception.get("exc_type", exception.get("type", "Unknown"))
    message = exception.get("message", "")
    stack = exception.get("stack", [])

    # Parse stack frames
    frames = [StackFrame.from_dict(f) for f in stack]

    # Convert stack frames to strings for classifier
    stack_strings = [
        f'File "{f.file}", line {f.line}, in {f.function}'
        for f in frames
    ]

    # Use enhanced classifier (Plan 51)
    classifier = _get_classifier()
    classification = classifier.classify(exc_type, message, stack_strings)

    # Legacy categorization for backward compatibility
    category = categorize_failure(exc_type, message)

    # Extract context
    context = extract_context(exception, causal_slice, manifest)

    # Add classification info to context
    context["classification"] = {
        "category": classification.category,
        "tags": list(classification.tags),
        "root_causes": classification.root_causes,
        "fingerprint": classification.fingerprint,
        "semantic_info": classification.semantic_info,
        "framework": classification.framework,
        "severity": classification.severity,
        "is_recoverable": classification.is_recoverable,
    }

    # Use classification strategies (enhanced) with fallback
    if classification.fix_strategies:
        strategies = classification.fix_strategies.copy()
    elif category:
        strategies = category.fix_strategies.copy()
    else:
        strategies = ["fix_the_code", "add_error_handling", "fix_the_test"]

    return FailureAnalysis(
        exception_type=exc_type,
        exception_message=message,
        stack_trace=frames,
        category=category,
        context=context,
        recommended_strategies=strategies,
        # Enhanced fields (Plan 51)
        classification=classification,
        fingerprint=classification.fingerprint,
        semantic_info=classification.semantic_info,
        framework=classification.framework,
    )


def format_analysis_report(analysis: FailureAnalysis) -> str:
    """Format failure analysis as human-readable report.

    Args:
        analysis: FailureAnalysis to format

    Returns:
        Formatted report string
    """
    lines = [
        "",
        "Failure Analysis Report",
        "=" * 50,
        "",
        f"Exception: {analysis.exception_type}",
        f"Message: {analysis.exception_message[:200]}",
        f"Location: {analysis.failure_location}",
        "",
    ]

    # Enhanced classification info (Plan 51)
    if analysis.classification:
        lines.append(f"Category: {analysis.classification.category}")
        lines.append(f"Fingerprint: {analysis.fingerprint}")
        if analysis.classification.tags:
            lines.append(f"Tags: {', '.join(analysis.classification.tags)}")
        if analysis.framework:
            lines.append(f"Framework: {analysis.framework}")
        lines.append(f"Severity: {analysis.classification.severity}/5")
        lines.append(f"Recoverable: {'Yes' if analysis.classification.is_recoverable else 'No'}")

        # Semantic info
        if analysis.semantic_info:
            relevant_info = {
                k: v for k, v in analysis.semantic_info.items()
                if k != "raw_message" and v is not None
            }
            if relevant_info:
                lines.append("")
                lines.append("Extracted Information:")
                for key, value in relevant_info.items():
                    lines.append(f"  {key}: {value}")

        # Root causes
        if analysis.classification.root_causes:
            lines.append("")
            lines.append("Likely root causes:")
            for cause in analysis.classification.root_causes:
                lines.append(f"  - {cause}")
    elif analysis.category:
        lines.append(f"Category: {analysis.category.name}")
        lines.append(f"Context needed: {', '.join(analysis.category.context_needed)}")
    else:
        lines.append("Category: uncategorized")

    lines.append("")
    lines.append("Recommended fix strategies:")
    for strategy in analysis.recommended_strategies:
        lines.append(f"  - {strategy}")

    if analysis.stack_trace:
        lines.append("")
        lines.append("Stack trace:")
        for frame in analysis.stack_trace[:5]:
            lines.append(f"  {frame.file}:{frame.line} in {frame.function}")

    return "\n".join(lines)
