"""SQL query analysis with Soda-inspired patterns.

Analyzes SQL queries for errors, N+1 patterns,
and data quality issues.
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Dict, List

if TYPE_CHECKING:
    from autodebug.detection.transcript_analyzer import SQLRecord, TranscriptInsight

# Pre-compiled patterns for query normalization
_SQL_STRING_LITERAL_RE = re.compile(r"'(?:''|[^'])*'")
_SQL_NUMBER_RE = re.compile(r"\b\d+\b")
_SQL_UUID_RE = re.compile(
    r"\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b",
    re.IGNORECASE,
)
_SQL_WHITESPACE_RE = re.compile(r"\s+")


class SQLAnalyzer:
    """Analyze SQL queries using Soda-inspired data quality patterns.

    Detects:
    - SQL errors with pgcode analysis
    - Empty query results
    - NULL values in results
    - N+1 query patterns
    - Slow queries
    """

    # PostgreSQL error code categories
    PGCODE_CATEGORIES = {
        # Class 23 — Integrity Constraint Violation
        "23505": ("unique_violation", "Duplicate key violates unique constraint", 4),
        "23503": ("foreign_key_violation", "Foreign key constraint violated", 4),
        "23502": ("not_null_violation", "NULL value in NOT NULL column", 4),
        "23514": ("check_violation", "Check constraint violated", 4),
        # Class 42 — Syntax Error or Access Rule Violation
        "42P01": ("undefined_table", "Table does not exist", 5),
        "42703": ("undefined_column", "Column does not exist", 5),
        "42601": ("syntax_error", "SQL syntax error", 5),
        "42883": ("undefined_function", "Function does not exist", 5),
        # Class 40 — Transaction Rollback
        "40001": ("serialization_failure", "Serialization failure (concurrent update)", 4),
        "40P01": ("deadlock_detected", "Deadlock detected", 5),
        # Class 08 — Connection Exception
        "08000": ("connection_exception", "Database connection error", 5),
        "08003": ("connection_does_not_exist", "Connection does not exist", 5),
        # Class 53 — Insufficient Resources
        "53100": ("disk_full", "Disk full", 5),
        "53200": ("out_of_memory", "Out of memory", 5),
        "53300": ("too_many_connections", "Too many connections", 4),
    }

    def __init__(self, n_plus_one_threshold: int = 10, n_plus_one_window: int = 100):
        """Initialize the SQL analyzer.

        Args:
            n_plus_one_threshold: Number of similar queries to trigger N+1 warning
            n_plus_one_window: Max record span to consider queries part of same burst
        """
        self.n_plus_one_threshold = n_plus_one_threshold
        self.n_plus_one_window = n_plus_one_window

    def analyze(self, records: List[SQLRecord]) -> List[TranscriptInsight]:
        """Analyze SQL records for issues.

        Args:
            records: List of SQL records to analyze

        Returns:
            List of insights found
        """
        insights: List[TranscriptInsight] = []

        for record in records:
            # Check for errors
            if record.exception:
                insights.extend(self._analyze_sql_error(record))

            # Check for empty results (Soda pattern: row_count check)
            insights.extend(self._check_row_count(record))

            # Check for NULL values in results
            insights.extend(self._check_null_values(record))

            # Check for slow queries
            insights.extend(self._check_slow_query(record))

        # Detect N+1 patterns
        insights.extend(self._detect_n_plus_one(records))

        return insights

    def _analyze_sql_error(self, record: SQLRecord) -> List[TranscriptInsight]:
        """Analyze SQL exceptions.

        Args:
            record: SQL record with exception

        Returns:
            List of insights from error analysis
        """
        from autodebug.detection.transcript_analyzer import TranscriptInsight

        exc = record.exception
        if not exc:
            return []

        if not isinstance(exc, dict):
            return [
                TranscriptInsight(
                    category="sql_error",
                    severity=4,
                    description=str(exc)[:200],
                    evidence={
                        "query": self._truncate_query(record.sql_canonical),
                        "error": str(exc)[:500],
                    },
                    related_records=[record.seq],
                    suggested_investigation="Check SQL query and database state",
                )
            ]

        pgcode = exc.get("pgcode", "")

        # Look up category from pgcode
        if pgcode in self.PGCODE_CATEGORIES:
            category, description, severity = self.PGCODE_CATEGORIES[pgcode]
        else:
            # Generic SQL error
            category = "sql_error"
            description = exc.get("message", "SQL error occurred")
            severity = 4

        return [
            TranscriptInsight(
                category=category,
                severity=severity,
                description=description,
                evidence={
                    "query": self._truncate_query(record.sql_canonical),
                    "pgcode": pgcode,
                    "error": exc.get("message"),
                },
                related_records=[record.seq],
                suggested_investigation=self._suggest_for_pgcode(pgcode),
            )
        ]

    def _check_row_count(self, record: SQLRecord) -> List[TranscriptInsight]:
        """Soda-inspired row count check.

        Args:
            record: SQL record to check

        Returns:
            List of insights for empty results
        """
        from autodebug.detection.transcript_analyzer import TranscriptInsight

        # Check if this was a SELECT that returned no rows
        if record.rows is None:
            return []

        if len(record.rows) == 0:
            # Only flag SELECT queries
            sql_upper = record.sql_canonical.strip().upper()
            if sql_upper.startswith("SELECT"):
                return [
                    TranscriptInsight(
                        category="empty_query_result",
                        severity=2,
                        description="SELECT returned no rows",
                        evidence={
                            "query": self._truncate_query(record.sql_canonical),
                        },
                        related_records=[record.seq],
                        suggested_investigation="Check if code handles empty results",
                    )
                ]
        return []

    def _check_null_values(self, record: SQLRecord) -> List[TranscriptInsight]:
        """Soda-inspired NULL value check.

        Args:
            record: SQL record to check

        Returns:
            List of insights for NULL values
        """
        from autodebug.detection.transcript_analyzer import TranscriptInsight

        if not record.rows:
            return []

        # Check first few rows for NULL values
        for row in record.rows[:5]:
            if isinstance(row, dict):
                null_cols = [k for k, v in row.items() if v is None]
                if null_cols:
                    return [
                        TranscriptInsight(
                            category="null_values",
                            severity=1,
                            description=f"NULL values in columns: {', '.join(null_cols[:5])}",
                            evidence={
                                "columns": null_cols[:5],
                                "query": self._truncate_query(record.sql_canonical),
                            },
                            related_records=[record.seq],
                            suggested_investigation="Check if NULL handling is needed",
                        )
                    ]

        return []

    def _check_slow_query(self, record: SQLRecord) -> List[TranscriptInsight]:
        """Check for slow queries.

        Args:
            record: SQL record to check

        Returns:
            List of insights for slow queries
        """
        from autodebug.detection.transcript_analyzer import TranscriptInsight

        # Consider queries over 1 second as slow
        if record.duration_ms > 1000:
            return [
                TranscriptInsight(
                    category="slow_query",
                    severity=2,
                    description=f"Slow query ({record.duration_ms:.0f}ms)",
                    evidence={
                        "query": self._truncate_query(record.sql_canonical),
                        "duration_ms": record.duration_ms,
                    },
                    related_records=[record.seq],
                    suggested_investigation="Consider adding indexes or optimizing query",
                )
            ]
        return []

    def _detect_n_plus_one(self, records: List[SQLRecord]) -> List[TranscriptInsight]:
        """Detect N+1 query patterns.

        Args:
            records: List of SQL records

        Returns:
            List of insights for N+1 patterns
        """
        from autodebug.detection.transcript_analyzer import TranscriptInsight

        # Count normalized query occurrences and span
        query_counts: Dict[str, int] = {}
        query_examples: Dict[str, str] = {}
        first_idx: Dict[str, int] = {}
        last_idx: Dict[str, int] = {}

        for idx, record in enumerate(records):
            normalized = self._normalize_query(record.sql_canonical)
            query_counts[normalized] = query_counts.get(normalized, 0) + 1
            if normalized not in query_examples:
                query_examples[normalized] = record.sql_canonical
                first_idx[normalized] = idx
            last_idx[normalized] = idx

        insights: List[TranscriptInsight] = []

        for query, count in query_counts.items():
            span = (last_idx.get(query, 0) - first_idx.get(query, 0))
            if count >= self.n_plus_one_threshold and span <= self.n_plus_one_window:
                insights.append(
                    TranscriptInsight(
                        category="n_plus_one",
                        severity=3,
                        description=f"N+1 pattern: {count} similar queries",
                        evidence={
                            "query_pattern": self._truncate_query(query),
                            "example": self._truncate_query(query_examples[query]),
                            "count": count,
                        },
                        related_records=[],
                        suggested_investigation="Consider using JOIN or batch loading",
                    )
                )

        return insights

    def _normalize_query(self, sql: str) -> str:
        """Normalize SQL for pattern matching.

        Removes specific values to group similar queries.

        Args:
            sql: SQL query

        Returns:
            Normalized query
        """
        # Remove string literals (handles escaped quotes)
        normalized = _SQL_STRING_LITERAL_RE.sub("'?'", sql)
        # Remove numbers
        normalized = _SQL_NUMBER_RE.sub("?", normalized)
        # Remove UUIDs
        normalized = _SQL_UUID_RE.sub("?", normalized)
        # Normalize whitespace
        normalized = _SQL_WHITESPACE_RE.sub(" ", normalized)
        return normalized.strip()

    def _truncate_query(self, sql: str, length: int = 200) -> str:
        """Truncate query for display.

        Args:
            sql: SQL query
            length: Maximum length

        Returns:
            Truncated query
        """
        sql = sql.strip()
        if len(sql) <= length:
            return sql
        return sql[: length - 3] + "..."

    def _suggest_for_pgcode(self, pgcode: str) -> str:
        """Get investigation suggestion for PostgreSQL error code.

        Args:
            pgcode: PostgreSQL error code

        Returns:
            Suggestion string
        """
        suggestions = {
            "23505": "Handle duplicate key with upsert or check-before-insert",
            "23503": "Ensure referenced records exist before inserting",
            "23502": "Provide value for NOT NULL column or add default",
            "23514": "Ensure data meets check constraint requirements",
            "42P01": "Check table name spelling and schema",
            "42703": "Check column name spelling",
            "42601": "Review SQL syntax",
            "42883": "Check function name and parameter types",
            "40001": "Implement retry logic for serialization failures",
            "40P01": "Review transaction ordering to avoid deadlocks",
            "08000": "Check database connection parameters",
            "08003": "Ensure connection is established before use",
            "53100": "Check disk space",
            "53200": "Check memory usage",
            "53300": "Implement connection pooling",
        }
        return suggestions.get(pgcode, "Check SQL query and database state")
