"""Unified exception classification using registry + fingerprinting.

Combines ExceptionRegistry (metadata) and ExceptionFingerprinter (grouping)
to provide comprehensive exception classification.
"""

import re
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Set

from autodebug.detection.exception_fingerprinter import ExceptionFingerprinter
from autodebug.detection.exception_registry import ExceptionMetadata, ExceptionRegistry


@dataclass
class ClassificationResult:
    """Complete classification of an exception."""

    # Basic info
    exception_type: str
    exception_message: str

    # Category and tags
    category: str
    tags: Set[str]

    # Fix guidance
    root_causes: List[str]
    fix_strategies: List[str]
    context_needed: List[str]

    # Severity and recoverability
    severity: int
    is_recoverable: bool

    # Fingerprinting for grouping
    fingerprint: str

    # Extracted semantic info
    semantic_info: Dict

    # Framework detection
    framework: Optional[str]

    # Raw metadata for advanced use
    metadata: ExceptionMetadata = field(repr=False)


class ExceptionClassifier:
    """Unified exception classification using registry + fingerprinting."""

    # Framework detection patterns
    FRAMEWORK_PATTERNS = {
        "django": ["django/", "django.", "/django/"],
        "flask": ["flask/", "flask.", "/flask/", "werkzeug/"],
        "fastapi": ["fastapi/", "fastapi.", "starlette/", "starlette."],
        "sqlalchemy": ["sqlalchemy/", "sqlalchemy."],
        "requests": ["requests/", "requests."],
        "httpx": ["httpx/", "httpx."],
        "pytest": ["pytest/", "_pytest/", "pytest."],
        "unittest": ["unittest/", "unittest."],
        "celery": ["celery/", "celery."],
        "aiohttp": ["aiohttp/", "aiohttp."],
        "tornado": ["tornado/", "tornado."],
        "pydantic": ["pydantic/", "pydantic."],
        "boto3": ["boto3/", "botocore/"],
        "redis": ["redis/", "redis."],
        "psycopg2": ["psycopg2/", "psycopg2."],
    }

    def __init__(self):
        self.registry = ExceptionRegistry()
        self.fingerprinter = ExceptionFingerprinter()

    def classify(
        self,
        exception_type: str,
        message: str,
        stack_trace: Optional[List[str]] = None,
    ) -> ClassificationResult:
        """Classify an exception with full context.

        Args:
            exception_type: The exception class name (e.g., "KeyError")
            message: The exception message
            stack_trace: Optional list of stack frame strings

        Returns:
            ClassificationResult with category, strategies, fingerprint, etc.
        """
        stack_trace = stack_trace or []

        # Get metadata from registry
        metadata = self.registry.get_metadata(exception_type)

        # Generate fingerprint for grouping
        fingerprint = self.fingerprinter.fingerprint(exception_type, message, stack_trace)

        # Extract semantic info from message
        semantic_info = self.fingerprinter.extract_semantic_info(exception_type, message)

        # Detect framework from stack trace
        framework = self._detect_framework(stack_trace)

        # Adjust strategies based on framework
        fix_strategies = self._adjust_strategies_for_framework(
            metadata.fix_strategies, framework, exception_type
        )

        return ClassificationResult(
            exception_type=exception_type,
            exception_message=message,
            category=metadata.category,
            tags=metadata.tags.copy(),
            root_causes=metadata.root_causes.copy(),
            fix_strategies=fix_strategies,
            context_needed=metadata.context_needed.copy(),
            severity=metadata.severity,
            is_recoverable=metadata.is_recoverable,
            fingerprint=fingerprint,
            semantic_info=semantic_info,
            framework=framework,
            metadata=metadata,
        )

    def _detect_framework(self, stack_trace: List[str]) -> Optional[str]:
        """Detect which framework the error originated from.

        Matches path components (preceded by / or start of line) to avoid
        false positives like 'test_fastapi.py' matching 'fastapi'.
        """
        stack_text = "\n".join(stack_trace).lower()

        for framework, patterns in self.FRAMEWORK_PATTERNS.items():
            for p in patterns:
                escaped = re.escape(p.lower())
                if re.search(rf"(?:^|/){escaped}", stack_text, re.MULTILINE):
                    return framework

        return None

    def _adjust_strategies_for_framework(
        self,
        strategies: List[str],
        framework: Optional[str],
        exception_type: str,
    ) -> List[str]:
        """Adjust fix strategies based on detected framework.

        Adds framework-specific strategies when applicable.
        """
        adjusted = strategies.copy()

        if not framework:
            return adjusted

        # Framework-specific additions
        if framework == "django":
            if exception_type in ("KeyError", "AttributeError"):
                adjusted.append("check_template_context")
            if exception_type == "IntegrityError":
                adjusted.append("use_get_or_create")
            if "DoesNotExist" in exception_type:
                adjusted.insert(0, "use_get_or_none")

        elif framework == "flask":
            if exception_type in ("KeyError",):
                adjusted.append("use_request_get")

        elif framework == "fastapi":
            if exception_type == "ValidationError":
                adjusted.insert(0, "fix_pydantic_model")
            if exception_type in ("KeyError", "TypeError"):
                adjusted.append("check_dependency_injection")

        elif framework == "sqlalchemy":
            if exception_type == "IntegrityError":
                adjusted.append("use_session_merge")
            if "NoResultFound" in exception_type:
                adjusted.insert(0, "use_one_or_none")

        elif framework == "pytest":
            if exception_type == "AssertionError":
                adjusted.insert(0, "fix_test_assertion")
                adjusted.append("update_expected_value")

        elif framework == "requests" or framework == "httpx":
            if exception_type in ("ConnectionError", "Timeout"):
                adjusted.insert(0, "add_exponential_backoff")

        elif framework == "celery":
            if exception_type in ("TimeoutError",):
                adjusted.append("adjust_task_timeout")

        return adjusted

    def get_suggested_context_keys(
        self, classification: ClassificationResult
    ) -> List[str]:
        """Get suggested keys to include in error context for LLM.

        Based on exception type and semantic info.
        """
        keys = classification.context_needed.copy()

        # Add based on semantic info
        if "missing_key" in classification.semantic_info:
            keys.extend(["available_keys", "dict_source"])
        if "missing_attribute" in classification.semantic_info:
            keys.extend(["object_type", "similar_attributes"])
        if "object_type" in classification.semantic_info:
            if classification.semantic_info.get("object_type") == "NoneType":
                keys.append("none_source")

        # Add framework-specific context
        if classification.framework == "django":
            keys.extend(["view_name", "url_pattern"])
        elif classification.framework == "fastapi":
            keys.extend(["endpoint", "request_body"])
        elif classification.framework in ("requests", "httpx"):
            keys.extend(["url", "response_status", "response_body"])

        return list(dict.fromkeys(keys))  # Remove duplicates while preserving order

    def is_likely_test_issue(self, classification: ClassificationResult) -> bool:
        """Determine if the issue is likely in test code vs application code.

        Helps decide whether to fix the test or the code.
        """
        # AssertionError in pytest is usually a test issue
        if classification.framework == "pytest" and classification.exception_type == "AssertionError":
            return True

        # Check for test-related tags
        if "test" in classification.tags:
            return True

        return False

    def get_severity_label(self, classification: ClassificationResult) -> str:
        """Get human-readable severity label."""
        labels = {
            1: "critical",
            2: "high",
            3: "medium",
            4: "low",
            5: "minor",
        }
        return labels.get(classification.severity, "unknown")

    def format_for_prompt(self, classification: ClassificationResult) -> str:
        """Format classification for inclusion in LLM prompt.

        Returns a structured string suitable for prompt context.
        """
        lines = [
            f"Exception Type: {classification.exception_type}",
            f"Category: {classification.category}",
            f"Severity: {self.get_severity_label(classification)}",
        ]

        if classification.framework:
            lines.append(f"Framework: {classification.framework}")

        if classification.tags:
            lines.append(f"Tags: {', '.join(classification.tags)}")

        if classification.semantic_info:
            # Format semantic info nicely
            info_lines = []
            for key, value in classification.semantic_info.items():
                if key != "raw_message" and value is not None:
                    info_lines.append(f"  - {key}: {value}")
            if info_lines:
                lines.append("Extracted Info:")
                lines.extend(info_lines)

        lines.append("")
        lines.append("Likely Root Causes:")
        for cause in classification.root_causes:
            lines.append(f"  - {cause}")

        lines.append("")
        lines.append("Recommended Fix Strategies:")
        for strategy in classification.fix_strategies[:5]:  # Top 5
            lines.append(f"  - {strategy}")

        return "\n".join(lines)
