from __future__ import annotations

from dataclasses import dataclass, asdict

import re

import sqlglot
from sqlglot import exp

from auraindex.models import IndexSuggestion, QueryCandidate
from auraindex.utils.text import dedupe_preserve_order, sanitize_identifier


@dataclass
class ExistingIndex:
    name: str
    table: str
    columns: list[str]
    unique: bool
    partial_where: str | None
    source_file: str
    source_line: int
    source_sql: str


_DYNAMIC_PLACEHOLDER_PATTERN = re.compile(r"(\?|\{[^}]+\}|:[a-zA-Z_][a-zA-Z0-9_]*)")


def extract_existing_indexes(
    candidates: list[QueryCandidate],
    *,
    dialect: str,
) -> tuple[list[ExistingIndex], list[str]]:
    indexes: list[ExistingIndex] = []
    warnings: list[str] = []
    seen: set[tuple[str, tuple[str, ...], bool, str | None]] = set()

    for candidate in candidates:
        sql_text = candidate.sql.strip()
        if not sql_text:
            continue
        lowered = sql_text.lower()
        if "create" not in lowered or "index" not in lowered:
            continue

        try:
            tree = sqlglot.parse_one(sql_text, read=dialect)
        except Exception:
            continue
        if not isinstance(tree, exp.Create):
            continue

        kind = str(tree.args.get("kind") or "").lower()
        if "index" not in kind:
            continue

        index_expr = tree.this
        if not isinstance(index_expr, exp.Index):
            continue

        table_name = _extract_index_table(index_expr)
        columns = _extract_index_columns(index_expr)
        if not table_name or not columns:
            if not _is_dynamic_or_unsupported_index_sql(sql_text):
                warnings.append(
                    f"Could not parse index columns/table from {candidate.context.file_path}:{candidate.context.line}"
                )
            continue

        index_name = _extract_index_name(index_expr) or f"unnamed_{table_name}_{'_'.join(columns)}"
        params = index_expr.args.get("params")
        where_expr = params.args.get("where") if isinstance(params, exp.IndexParameters) else None
        partial_where = where_expr.sql(dialect=dialect) if where_expr is not None else None
        unique = bool(tree.args.get("unique") or index_expr.args.get("unique"))

        dedupe_key = (table_name, tuple(columns), unique, partial_where)
        if dedupe_key in seen:
            continue
        seen.add(dedupe_key)

        indexes.append(
            ExistingIndex(
                name=index_name,
                table=table_name,
                columns=columns,
                unique=unique,
                partial_where=partial_where,
                source_file=candidate.context.file_path,
                source_line=candidate.context.line,
                source_sql=sql_text[:500],
            )
        )

    return indexes, warnings


def apply_schema_checks(
    suggestions: list[IndexSuggestion],
    existing_indexes: list[ExistingIndex],
) -> tuple[list[IndexSuggestion], dict[str, object]]:
    by_table: dict[str, list[ExistingIndex]] = {}
    for index in existing_indexes:
        by_table.setdefault(index.table, []).append(index)

    kept: list[IndexSuggestion] = []
    skipped: list[IndexSuggestion] = []
    status_counter: dict[str, int] = {}

    for suggestion in suggestions:
        table_indexes = by_table.get(suggestion.table, [])
        exact_matches = [idx for idx in table_indexes if idx.columns == suggestion.columns]
        covering_prefix = [
            idx
            for idx in table_indexes
            if _is_prefix(suggestion.columns, idx.columns) and idx.columns != suggestion.columns
        ]
        narrower_prefix = [
            idx
            for idx in table_indexes
            if _is_prefix(idx.columns, suggestion.columns) and idx.columns != suggestion.columns
        ]
        overlaps = exact_matches + covering_prefix + narrower_prefix
        unique_overlap = any(index.unique for index in overlaps)
        partial_overlap = any(index.partial_where for index in overlaps)

        if exact_matches:
            status = "skip_exact_existing"
        elif covering_prefix:
            status = "skip_covered_by_existing_prefix"
        elif narrower_prefix:
            status = "review_overlaps_existing_prefix"
        else:
            status = "ok"

        status_counter[status] = status_counter.get(status, 0) + 1
        suggestion.metadata["schema_check"] = {
            "status": status,
            "unique_overlap": unique_overlap,
            "partial_overlap": partial_overlap,
            "exact_matches": [asdict(index) for index in exact_matches[:5]],
            "covering_prefix_matches": [asdict(index) for index in covering_prefix[:5]],
            "narrower_prefix_matches": [asdict(index) for index in narrower_prefix[:5]],
        }

        if status in {"skip_exact_existing", "skip_covered_by_existing_prefix"}:
            skipped.append(suggestion)
            continue

        if status == "review_overlaps_existing_prefix":
            suggestion.needs_manual_review = True
        kept.append(suggestion)

    summary = {
        "discovered_existing_indexes": len(existing_indexes),
        "kept_suggestions": len(kept),
        "skipped_suggestions": len(skipped),
        "status_counts": status_counter,
    }
    return kept, summary


def _extract_index_table(index_expr: exp.Index) -> str:
    table_expr = index_expr.args.get("table")
    if isinstance(table_expr, exp.Table):
        return sanitize_identifier(table_expr.name or "")
    if isinstance(table_expr, exp.Expression):
        return sanitize_identifier(table_expr.sql())
    return ""


def _extract_index_name(index_expr: exp.Index) -> str:
    this_expr = index_expr.args.get("this")
    if isinstance(this_expr, exp.Identifier):
        return sanitize_identifier(this_expr.name or "")
    if isinstance(this_expr, exp.Expression):
        return sanitize_identifier(this_expr.sql())
    return ""


def _extract_index_columns(index_expr: exp.Index) -> list[str]:
    params = index_expr.args.get("params")
    if not isinstance(params, exp.IndexParameters):
        return []
    columns = params.args.get("columns")
    if not isinstance(columns, list):
        return []

    parsed: list[str] = []
    for column_expr in columns:
        if isinstance(column_expr, exp.Ordered):
            inner = column_expr.this
        else:
            inner = column_expr
        if isinstance(inner, exp.Column):
            col_name = sanitize_identifier(inner.name or "")
        elif isinstance(inner, exp.Identifier):
            col_name = sanitize_identifier(inner.name or "")
        else:
            col_name = sanitize_identifier(inner.sql() if isinstance(inner, exp.Expression) else "")
        if col_name:
            parsed.append(col_name)
    return dedupe_preserve_order(parsed)


def _is_prefix(prefix: list[str], full: list[str]) -> bool:
    if len(prefix) > len(full):
        return False
    return prefix == full[: len(prefix)]


def _is_dynamic_or_unsupported_index_sql(sql_text: str) -> bool:
    lowered = sql_text.lower()
    if _DYNAMIC_PLACEHOLDER_PATTERN.search(sql_text):
        return True
    if "using ivfflat" in lowered or "vector_cosine_ops" in lowered:
        return True
    return False
