from __future__ import annotations

import re
import sqlite3
import subprocess

from auraindex.models import IndexSuggestion, ParsedQueryFeatures

_NAMED_PARAM_PATTERN = re.compile(r"(?<!:)(:[a-zA-Z_][a-zA-Z0-9_]*)|@[a-zA-Z_][a-zA-Z0-9_]*|\$[a-zA-Z_][a-zA-Z0-9_]*")
_PERCENT_S_PATTERN = re.compile(r"%s")


def validate_suggestions_with_explain(
    suggestions: list[IndexSuggestion],
    parsed_queries: list[ParsedQueryFeatures],
    *,
    dialect: str,
    explain_validate: bool,
    explain_db_path: str | None,
    explain_command: str | None,
    explain_timeout_seconds: int,
    explain_max_queries: int,
) -> tuple[dict[str, object], list[str]]:
    if not explain_validate:
        return {}, []

    warnings: list[str] = []
    summary: dict[str, object] = {
        "enabled": True,
        "engine": None,
        "validated": 0,
        "errors": 0,
        "skipped": 0,
    }

    if dialect == "sqlite" and explain_db_path:
        summary["engine"] = "sqlite"
        return _run_sqlite_explain(
            suggestions,
            parsed_queries,
            explain_db_path=explain_db_path,
            explain_max_queries=explain_max_queries,
            timeout_seconds=explain_timeout_seconds,
            summary=summary,
            warnings=warnings,
        )

    if explain_command:
        summary["engine"] = "command"
        return _run_command_explain(
            suggestions,
            parsed_queries,
            explain_command=explain_command,
            explain_max_queries=explain_max_queries,
            timeout_seconds=explain_timeout_seconds,
            summary=summary,
            warnings=warnings,
        )

    warnings.append(
        "EXPLAIN validation enabled but no runner configured. Provide --explain-db-path for sqlite "
        "or --explain-command for external engines."
    )
    for suggestion in suggestions:
        suggestion.metadata["explain_validation"] = {"status": "not_configured"}
    summary["skipped"] = len(suggestions)
    return summary, warnings


def _run_sqlite_explain(
    suggestions: list[IndexSuggestion],
    parsed_queries: list[ParsedQueryFeatures],
    *,
    explain_db_path: str,
    explain_max_queries: int,
    timeout_seconds: int,
    summary: dict[str, object],
    warnings: list[str],
) -> tuple[dict[str, object], list[str]]:
    try:
        connection = sqlite3.connect(explain_db_path, timeout=max(1, timeout_seconds))
    except Exception as exc:
        warnings.append(f"Could not connect to sqlite database for EXPLAIN: {exc}")
        for suggestion in suggestions:
            suggestion.metadata["explain_validation"] = {"status": "connection_error", "error": str(exc)}
        summary["errors"] = len(suggestions)
        return summary, warnings

    try:
        for suggestion in suggestions:
            candidates = _match_queries_for_suggestion(suggestion, parsed_queries, limit=explain_max_queries)
            if not candidates:
                suggestion.metadata["explain_validation"] = {"status": "no_matching_query"}
                summary["skipped"] = int(summary.get("skipped", 0)) + 1
                continue

            query = candidates[0]
            explain_sql = _normalize_sql_for_explain(query.query.sql)
            statement = f"EXPLAIN QUERY PLAN {explain_sql}"

            try:
                rows = connection.execute(statement).fetchall()
            except Exception as exc:
                suggestion.metadata["explain_validation"] = {
                    "status": "error",
                    "engine": "sqlite",
                    "error": str(exc),
                    "query_excerpt": query.query.sql[:320],
                    "source_file": query.query.context.file_path,
                    "source_line": query.query.context.line,
                }
                summary["errors"] = int(summary.get("errors", 0)) + 1
                continue

            suggestion.metadata["explain_validation"] = {
                "status": "validated",
                "engine": "sqlite",
                "query_excerpt": query.query.sql[:320],
                "source_file": query.query.context.file_path,
                "source_line": query.query.context.line,
                "plan_rows": [str(row) for row in rows[:10]],
            }
            summary["validated"] = int(summary.get("validated", 0)) + 1
    finally:
        connection.close()

    return summary, warnings


def _run_command_explain(
    suggestions: list[IndexSuggestion],
    parsed_queries: list[ParsedQueryFeatures],
    *,
    explain_command: str,
    explain_max_queries: int,
    timeout_seconds: int,
    summary: dict[str, object],
    warnings: list[str],
) -> tuple[dict[str, object], list[str]]:
    if "{sql}" not in explain_command:
        warnings.append("Explain command does not include '{sql}' placeholder; SQL will be appended at the end.")

    for suggestion in suggestions:
        candidates = _match_queries_for_suggestion(suggestion, parsed_queries, limit=explain_max_queries)
        if not candidates:
            suggestion.metadata["explain_validation"] = {"status": "no_matching_query"}
            summary["skipped"] = int(summary.get("skipped", 0)) + 1
            continue

        query = candidates[0]
        sql_text = _normalize_sql_for_explain(query.query.sql)
        command = explain_command.replace("{sql}", sql_text) if "{sql}" in explain_command else f"{explain_command} {sql_text}"

        try:
            completed = subprocess.run(
                command,
                shell=True,
                capture_output=True,
                text=True,
                timeout=max(1, timeout_seconds),
                check=False,
            )
        except Exception as exc:
            suggestion.metadata["explain_validation"] = {
                "status": "error",
                "engine": "command",
                "error": str(exc),
                "query_excerpt": query.query.sql[:320],
            }
            summary["errors"] = int(summary.get("errors", 0)) + 1
            continue

        if completed.returncode != 0:
            suggestion.metadata["explain_validation"] = {
                "status": "error",
                "engine": "command",
                "exit_code": completed.returncode,
                "stderr": (completed.stderr or "")[:600],
                "stdout": (completed.stdout or "")[:600],
                "query_excerpt": query.query.sql[:320],
            }
            summary["errors"] = int(summary.get("errors", 0)) + 1
            continue

        suggestion.metadata["explain_validation"] = {
            "status": "validated",
            "engine": "command",
            "query_excerpt": query.query.sql[:320],
            "stdout": (completed.stdout or "")[:800],
        }
        summary["validated"] = int(summary.get("validated", 0)) + 1

    return summary, warnings


def _match_queries_for_suggestion(
    suggestion: IndexSuggestion,
    parsed_queries: list[ParsedQueryFeatures],
    *,
    limit: int,
) -> list[ParsedQueryFeatures]:
    matched: list[ParsedQueryFeatures] = []
    target_cols = set(suggestion.columns)
    for parsed in parsed_queries:
        if suggestion.table not in parsed.tables:
            continue
        referenced_cols = {
            col
            for table, col in (parsed.where_columns + parsed.join_columns + parsed.order_by_columns + parsed.group_by_columns)
            if table == suggestion.table
        }
        if not (target_cols & referenced_cols):
            continue
        matched.append(parsed)
        if len(matched) >= max(1, limit):
            break
    return matched


def _normalize_sql_for_explain(sql_text: str) -> str:
    normalized = sql_text
    normalized = _NAMED_PARAM_PATTERN.sub("NULL", normalized)
    normalized = normalized.replace("?", "NULL")
    normalized = _PERCENT_S_PATTERN.sub("NULL", normalized)
    return normalized
