# backend/performance_testing/python_bindings/django_analyzer.py - Django Performance Analysis Engine
# Provides deep analysis of Django-specific performance patterns, including N+1 query detection.

# --- Standard Library Imports ---
import re
import logging
import threading
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass

# --- Third-Party Imports ---
from django.db import connection
from django.test.utils import override_settings

# --- Local Imports ---
try:
    from .colors import colors, get_status_icon, EduLiteColorScheme
except ImportError:
    from colors import colors, get_status_icon, EduLiteColorScheme

# --- Data Classes for Analysis ---


@dataclass
class QueryAnalysis:
    """
    A data class holding the analysis of a single database query.

    Attributes:
        sql (str): The raw SQL of the query.
        duration (float): The execution time in milliseconds.
        table (str): The primary table being operated on.
        operation (str): The SQL operation (e.g., SELECT, INSERT).
        is_select_related (bool): True if the query likely uses `select_related`.
        is_prefetch_related (bool): True if the query likely uses `prefetch_related`.
        potentially_problematic (bool): True if the query shows signs of being inefficient.
    """

    sql: str
    duration: float
    table: str
    operation: str
    is_select_related: bool
    is_prefetch_related: bool
    potentially_problematic: bool


@dataclass
class NplusOneDetection:
    """
    A data class representing the results of an N+1 query pattern detection.

    Attributes:
        detected (bool): True if an N+1 pattern was detected.
        pattern_type (str): The type of N+1 pattern identified.
        queries (List[QueryAnalysis]): A list of the queries involved in the pattern.
        suggested_fix (str): A human-readable suggestion for fixing the issue.
        severity (str): The severity of the issue (e.g., 'low', 'critical').
        affected_tables (List[str]): A list of database tables affected by the pattern.
    """

    detected: bool
    pattern_type: str
    queries: List[QueryAnalysis]
    suggested_fix: str
    severity: str
    affected_tables: List[str]


# --- Query Pattern Recognition ---


class QueryPattern:
    """
    Represents a detected pattern of related database queries.

    This class is used to identify potential N+1 issues by grouping a base query
    with a series of subsequent, similar queries.
    """

    def __init__(self, base_query: str, related_queries: List[str]) -> None:
        """
        Initializes a QueryPattern instance.

        Args:
            base_query (str): The initial query that is suspected of causing an N+1 problem.
            related_queries (List[str]): A list of subsequent queries that may be part of the N+1 pattern.
        """
        self.base_query = base_query
        self.related_queries = related_queries
        self.count = len(related_queries)

    def is_n_plus_one(self) -> bool:
        """
        Determines if this query pattern constitutes an N+1 problem.

        Returns:
            bool: True if the pattern is a likely N+1 scenario, False otherwise.
        """
        if self.count < 2 or not self.related_queries:
            return False

        first_query = self.related_queries[0]
        table_match = re.search(r'FROM ["`]?(\w+)["`]?', first_query, re.IGNORECASE)
        if not table_match:
            return False

        table_name = table_match.group(1)
        same_table_count = sum(
            1
            for query in self.related_queries
            if table_name.lower() in query.lower()
            and re.search(r'WHERE.*\b\w*id\s*=\s*["\\]?\d+["\\]?', query, re.IGNORECASE)
        )

        return same_table_count >= len(self.related_queries) * 0.8


# --- Query Logging ---


class DjangoQueryLogger:
    """
    A thread-safe logger for capturing Django database queries for analysis.
    """

    def __init__(self) -> None:
        """Initializes the query logger."""
        self.queries: List[Dict[str, Any]] = []
        self.is_logging = False
        self._lock = threading.Lock()
        self._original_level = None
        self._handler = None

    def start_logging(self) -> None:
        """Starts logging database queries by attaching a custom handler."""
        with self._lock:
            if self.is_logging:
                return
            self.queries.clear()
            self.is_logging = True
            logger = logging.getLogger("django.db.backends")
            self._original_level = logger.level
            logger.setLevel(logging.DEBUG)
            self._handler = QueryHandler(self)
            logger.addHandler(self._handler)

    def stop_logging(self) -> List[Dict[str, Any]]:
        """
        Stops logging and returns the list of captured queries.

        Returns:
            List[Dict[str, Any]]: A list of captured query data.
        """
        with self._lock:
            if not self.is_logging:
                return []
            self.is_logging = False
            logger = logging.getLogger("django.db.backends")
            if self._original_level is not None:
                logger.setLevel(self._original_level)
            if self._handler:
                logger.removeHandler(self._handler)
            return self.queries.copy()

    def add_query(self, query_data: Dict[str, Any]) -> None:
        """
        Adds a captured query to the internal log.

        Args:
            query_data (Dict[str, Any]): The query data to add.
        """
        with self._lock:
            if self.is_logging:
                self.queries.append(query_data)


class QueryHandler(logging.Handler):
    """
    A custom logging handler that directs Django query logs to our logger instance.
    """

    def __init__(self, logger_instance: DjangoQueryLogger) -> None:
        """
        Initializes the handler.

        Args:
            logger_instance (DjangoQueryLogger): The logger instance to which queries will be sent.
        """
        super().__init__()
        self.logger_instance = logger_instance

    def emit(self, record: logging.LogRecord) -> None:
        """
        Captures a query from a log record and adds it to the logger instance.

        Args:
            record (logging.LogRecord): The log record to process.
        """
        if hasattr(record, "sql"):
            self.logger_instance.add_query(
                {
                    "sql": record.sql,
                    "duration": getattr(record, "duration", 0),
                    "params": getattr(record, "params", []),
                    "timestamp": record.created,
                }
            )


# --- Analysis Engine ---


class DjangoAnalysisEngine:
    """
    An engine for advanced, Django-specific performance analysis.
    """

    def __init__(self) -> None:
        """Initializes the analysis engine."""
        self.query_logger = DjangoQueryLogger()

    def analyze_queries(self, queries: List[Dict[str, Any]]) -> List[QueryAnalysis]:
        """
        Analyzes a list of raw query data and returns structured analysis.

        Args:
            queries (List[Dict[str, Any]]): A list of query data dictionaries.

        Returns:
            List[QueryAnalysis]: A list of structured query analysis objects.
        """
        return [
            QueryAnalysis(
                sql=(sql := query_data.get("sql", "")),
                duration=float(duration := query_data.get("duration", 0)),
                table=self._extract_table_name(sql),
                operation=self._extract_operation(sql),
                is_select_related=self._has_joins(sql),
                is_prefetch_related=False,  # Context-dependent, hard to detect here
                potentially_problematic=self._is_potentially_problematic(sql, duration),
            )
            for query_data in queries
        ]

    def detect_n_plus_one_queries(self, queries: List[Dict[str, Any]]) -> List[NplusOneDetection]:
        """
        Detects N+1 query patterns from a list of queries.

        Args:
            queries (List[Dict[str, Any]]): The list of queries to analyze.

        Returns:
            List[NplusOneDetection]: A list of detected N+1 patterns.
        """
        if len(queries) < 3:
            return []

        query_analyses = self.analyze_queries(queries)
        patterns = self._group_queries_by_pattern(query_analyses)
        return [
            self._create_n_plus_one_detection(p, query_analyses)
            for p in patterns
            if p.is_n_plus_one()
        ]

    def _group_queries_by_pattern(self, analyses: List[QueryAnalysis]) -> List[QueryPattern]:
        """Groups queries into patterns to facilitate N+1 detection."""
        patterns = []
        base_queries = [
            a for a in analyses if a.operation == "SELECT" and not self._is_single_row_lookup(a.sql)
        ]

        for i, base_query in enumerate(base_queries):
            related_queries = []
            for subsequent_query in analyses[i + 1 :]:
                if subsequent_query.operation == "SELECT":
                    if self._is_single_row_lookup(
                        subsequent_query.sql
                    ) and self._queries_likely_related(base_query, subsequent_query):
                        related_queries.append(subsequent_query.sql)
                    else:
                        break  # Stop if we hit another broad query
            if related_queries:
                patterns.append(QueryPattern(base_query.sql, related_queries))
        return patterns

    def _is_single_row_lookup(self, sql: str) -> bool:
        """Checks if a query is a single-row lookup, a key indicator of N+1 issues."""
        if sql is None:
            return False

        patterns = [
            r'WHERE.*\b\w*id\s*=\s*["\\]?\w+["\\]?',
            r'WHERE.*\bpk\s*=\s*["\\]?\w+["\\]?',
            r'WHERE.*\s+IN\s*\(["\\]?\w+["\\]?\)',  # Single item IN clause
        ]
        return any(re.search(p, sql, re.IGNORECASE) for p in patterns)

    def _queries_likely_related(
        self, base_query: QueryAnalysis, lookup_query: QueryAnalysis
    ) -> bool:
        """Heuristically determines if two queries are likely related."""
        base_table, lookup_table = base_query.table.lower(), lookup_query.table.lower()
        related_patterns = [f"{base_table}_", f"_{base_table}"]
        if base_table in ["user", "users"]:
            related_patterns.extend(["profile", "profiles", "userprofile"])
        return any(p and p in lookup_table for p in related_patterns)

    def _create_n_plus_one_detection(
        self, pattern: QueryPattern, analyses: List[QueryAnalysis]
    ) -> NplusOneDetection:
        """Creates an N+1 detection result object from a query pattern."""
        severity = "low"
        if pattern.count >= 50:
            severity = "critical"
        elif pattern.count >= 20:
            severity = "high"
        elif pattern.count >= 10:
            severity = "medium"

        affected_tables = list(set(self._extract_table_name(q) for q in pattern.related_queries))
        main_table = self._extract_table_name(pattern.base_query)

        if affected_tables:
            suggested_fix = f"Use `select_related('{affected_tables[0].lower()}')` or `prefetch_related()` in your `{main_table}` query."
        else:
            suggested_fix = (
                "Consider using `select_related()` or `prefetch_related()` to reduce query count."
            )

        return NplusOneDetection(
            detected=True,
            pattern_type="classic_n_plus_one",
            queries=[a for a in analyses if a.sql in pattern.related_queries],
            suggested_fix=suggested_fix,
            severity=severity,
            affected_tables=affected_tables,
        )

    def _extract_table_name(self, sql: str) -> str:
        """Extracts the primary table name from an SQL query.

        Args:
            sql: The SQL query string to parse.

        Returns:
            str: The primary table name, or 'unknown' if not found.
        """
        if sql is None:
            return "unknown"

        patterns = [
            r'FROM\s+["`]?(\w+)["`]?',
            r'UPDATE\s+["`]?(\w+)["`]?',
            r'INSERT\s+INTO\s+["`]?(\w+)["`]?',
        ]
        for pattern in patterns:
            match = re.search(pattern, sql, re.IGNORECASE)
            if match:
                return match.group(1)
        return "unknown"

    def _extract_operation(self, sql: str) -> str:
        """Extracts the SQL operation type from a query string.

        Args:
            sql: The SQL query string to parse.

        Returns:
            str: The operation type (SELECT, INSERT, UPDATE, DELETE, or OTHER).
        """
        if sql is None:
            return "OTHER"

        sql_upper = sql.strip().upper()
        for op in ["SELECT", "INSERT", "UPDATE", "DELETE"]:
            if sql_upper.startswith(op):
                return op
        return "OTHER"

    def _has_joins(self, sql: str) -> bool:
        """Checks if an SQL query contains any JOIN clauses.

        Args:
            sql: The SQL query string to check.

        Returns:
            bool: True if the query contains JOIN clauses, False otherwise.
        """
        if sql is None:
            return False

        return bool(re.search(r"\b(INNER|LEFT|RIGHT|OUTER)?\s?JOIN\b", sql, re.IGNORECASE))

    def _is_potentially_problematic(self, sql: str, duration: float) -> bool:
        """Identifies if a query is potentially problematic based on common anti-patterns.

        Args:
            sql: The SQL query string to analyze.
            duration: Query execution time in milliseconds.

        Returns:
            bool: True if the query shows signs of being problematic.
        """
        if duration > 100:
            return True

        if sql is None:
            return False

        if (
            sql.strip().upper().startswith("SELECT")
            and "WHERE" not in sql.upper()
            and "LIMIT" not in sql.upper()
        ):
            return True
        return self._is_single_row_lookup(sql)

    def generate_optimization_report(self, detections: List[NplusOneDetection]) -> str:
        """
        Generates a colored, human-readable optimization report.

        Args:
            detections (List[NplusOneDetection]): A list of N+1 detections.

        Returns:
            str: A formatted report string.
        """
        if not detections:
            return colors.colorize(
                "✅ No N+1 query patterns detected!", EduLiteColorScheme.SUCCESS, bold=True
            )

        lines = [
            colors.colorize(
                "🔍 Django Query Analysis Report", EduLiteColorScheme.ACCENT, bold=True
            ),
            "=" * 50,
        ]
        severity_map = {
            "low": (EduLiteColorScheme.WARNING, "⚠️"),
            "medium": (EduLiteColorScheme.SLOW, "🐌"),
            "high": (EduLiteColorScheme.CRITICAL, "🚨"),
            "critical": (EduLiteColorScheme.CRITICAL, "💥"),
        }

        for i, detection in enumerate(detections, 1):
            color, icon = severity_map[detection.severity]
            lines.extend(
                [
                    f"\n{icon} Detection #{i} - {colors.colorize(detection.severity.upper(), color, bold=True)}",
                    f"   - Pattern: {colors.colorize(detection.pattern_type, EduLiteColorScheme.INFO)}",
                    f"   - Queries: {colors.colorize(str(len(detection.queries)), EduLiteColorScheme.TEXT)} repetitive queries",
                    f"   - Tables: {colors.colorize(', '.join(detection.affected_tables), EduLiteColorScheme.SECONDARY)}",
                    f"   - 💡 {colors.colorize('Fix:', EduLiteColorScheme.OPTIMIZATION, bold=True)} {detection.suggested_fix}",
                ]
            )

        lines.append(
            f"\n{colors.colorize('🎯 Summary:', EduLiteColorScheme.ACCENT, bold=True)} {len(detections)} optimization opportunities found."
        )
        return "\n".join(lines)


# --- Context Manager for Performance Monitoring ---


class PerformanceContextManager:
    """
    A context manager that combines performance monitoring with Django query analysis.
    """

    def __init__(self, operation_name: str) -> None:
        """
        Initializes the context manager.

        Args:
            operation_name (str): A name for the operation being monitored.
        """
        self.operation_name = operation_name
        self.analyzer = DjangoAnalysisEngine()
        self.start_queries = 0
        self.n_plus_one_detections: List[NplusOneDetection] = []

    def __enter__(self) -> "PerformanceContextManager":
        """Starts the query logger and records the initial query count."""
        self.analyzer.query_logger.start_logging()
        self.start_queries = len(connection.queries)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """
        Stops the query logger, analyzes the captured queries, and detects N+1 patterns.
        """
        captured_queries = self.analyzer.query_logger.stop_logging()
        if hasattr(connection, "queries"):
            django_queries = connection.queries[self.start_queries :]
            for query in django_queries:
                captured_queries.append(
                    {
                        "sql": query["sql"],
                        "duration": float(query["time"]) * 1000,
                        "params": [],
                        "timestamp": 0,
                    }
                )
        self.n_plus_one_detections = self.analyzer.detect_n_plus_one_queries(captured_queries)

    def get_optimization_report(self) -> str:
        """Generates and returns a colored optimization report."""
        return self.analyzer.generate_optimization_report(self.n_plus_one_detections)

    @property
    def has_n_plus_one_issues(self) -> bool:
        """Returns True if any N+1 issues were detected."""
        return bool(self.n_plus_one_detections)


# --- Test Execution ---

if __name__ == "__main__":
    # Example usage to demonstrate the analysis engine.
    print("🔍 Django Analysis Engine Test")
    print("=" * 40)

    sample_queries = [
        {"sql": "SELECT * FROM auth_user ORDER BY date_joined DESC LIMIT 10", "duration": 15.2},
        {"sql": "SELECT * FROM users_userprofile WHERE user_id = 1", "duration": 2.1},
        {"sql": "SELECT * FROM users_userprofile WHERE user_id = 2", "duration": 2.3},
        {"sql": "SELECT * FROM users_userprofile WHERE user_id = 3", "duration": 2.0},
        {"sql": "SELECT * FROM users_userprofile WHERE user_id = 4", "duration": 2.4},
        {"sql": "SELECT * FROM users_userprofile WHERE user_id = 5", "duration": 1.9},
    ]

    analyzer = DjangoAnalysisEngine()
    detections = analyzer.detect_n_plus_one_queries(sample_queries)
    report = analyzer.generate_optimization_report(detections)
    print(report)
