from __future__ import annotations

import re
from pathlib import Path

from auraindex.models import QueryCandidate, QueryContext
from auraindex.utils.text import camel_to_snake, looks_like_sql, normalize_whitespace, sanitize_identifier

_STRING_LITERAL_RE = re.compile(
    r"'''[\s\S]*?'''|\"\"\"[\s\S]*?\"\"\"|'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\"|`(?:\\.|[^`\\])*`"
)

_WHERE_METHOD_RE = re.compile(
    r"(?:->|\.|::)(?:where|filter|filter_by|andwhere|orwhere|wherein|wheredate|wherebetween|eq|neq|lt|lte|gt|gte|match)\s*\(\s*['\"`]([A-Za-z_][A-Za-z0-9_]*)['\"`]",
    re.IGNORECASE,
)
_ORDER_METHOD_RE = re.compile(
    r"(?:->|\.|::)(?:order_by|orderby|order|orderBy)\s*\(\s*['\"`]([A-Za-z_][A-Za-z0-9_]*)['\"`]",
    re.IGNORECASE,
)
_GROUP_METHOD_RE = re.compile(
    r"(?:->|\.|::)(?:group_by|groupby|groupBy)\s*\(\s*['\"`]([A-Za-z_][A-Za-z0-9_]*)['\"`]",
    re.IGNORECASE,
)
_JOIN_TABLE_RE = re.compile(
    r"(?:->|\.|::)(?:join|leftjoin|rightjoin|innerjoin|outerjoin|leftJoin|rightJoin|innerJoin|outerJoin)\s*\(\s*['\"`]([A-Za-z_][A-Za-z0-9_.]*)['\"`]",
    re.IGNORECASE,
)

_BASE_TABLE_PATTERNS = [
    re.compile(r"(?:->|\.|::)(?:table|from|select_from|from_table)\s*\(\s*['\"`]([A-Za-z_][A-Za-z0-9_.]*)['\"`]", re.IGNORECASE),
    re.compile(r"\b(?:db|database|knex)\s*\(\s*['\"`]([A-Za-z_][A-Za-z0-9_.]*)['\"`]\s*\)", re.IGNORECASE),
    re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)::where\s*\(", re.IGNORECASE),
]


def extract_text_fallback_file(file_path: Path, *, language: str) -> tuple[list[QueryCandidate], list[str]]:
    source = file_path.read_text(encoding="utf-8", errors="ignore")
    if language in {"javascript", "typescript", "tsx"} and _is_likely_minified(file_path, source):
        return [], []
    candidates: list[QueryCandidate] = []

    candidates.extend(_extract_literal_sql_candidates(source, file_path, language))
    candidates.extend(_extract_chain_sql_candidates(source, file_path, language))
    return candidates, []


def _extract_literal_sql_candidates(source: str, file_path: Path, language: str) -> list[QueryCandidate]:
    candidates: list[QueryCandidate] = []
    for match in _STRING_LITERAL_RE.finditer(source):
        raw_literal = match.group(0)
        unquoted = _unquote_literal(raw_literal)
        normalized = normalize_whitespace(unquoted)
        if not normalized or not _is_confident_sql_literal(normalized):
            continue
        line = source.count("\n", 0, match.start()) + 1
        candidates.append(
            QueryCandidate(
                sql=normalized,
                context=QueryContext(
                    file_path=str(file_path),
                    line=line,
                    function=None,
                    language=language,
                    source_kind="raw_sql",
                ),
                confidence=0.61,
                metadata={"extractor": "text_fallback", "kind": "string_literal"},
            )
        )
    return candidates


def _extract_chain_sql_candidates(source: str, file_path: Path, language: str) -> list[QueryCandidate]:
    candidates: list[QueryCandidate] = []
    for chunk, start_idx in _split_chunks(source):
        lowered = chunk.lower()
        if "where(" not in lowered and "filter(" not in lowered and "join(" not in lowered and "orderby(" not in lowered and "order_by(" not in lowered and "groupby(" not in lowered and "group_by(" not in lowered:
            continue

        pseudo_sql = _build_pseudo_sql(chunk)
        if not pseudo_sql:
            continue
        line = source.count("\n", 0, start_idx) + 1
        candidates.append(
            QueryCandidate(
                sql=pseudo_sql,
                context=QueryContext(
                    file_path=str(file_path),
                    line=line,
                    function=None,
                    language=language,
                    source_kind="orm_pattern",
                ),
                confidence=0.67,
                metadata={"extractor": "text_fallback", "kind": "builder_chain"},
            )
        )
    return candidates


def _split_chunks(source: str) -> list[tuple[str, int]]:
    chunks: list[tuple[str, int]] = []
    start = 0
    in_single = False
    in_double = False
    escaped = False

    for idx, char in enumerate(source):
        if escaped:
            escaped = False
            continue
        if char == "\\":
            escaped = True
            continue
        if char == "'" and not in_double:
            in_single = not in_single
            continue
        if char == '"' and not in_single:
            in_double = not in_double
            continue
        if char == ";" and not in_single and not in_double:
            chunk = source[start:idx]
            if chunk.strip():
                chunks.append((chunk, start))
            start = idx + 1

    tail = source[start:]
    if tail.strip():
        chunks.append((tail, start))

    return chunks


def _build_pseudo_sql(chunk: str) -> str | None:
    base_table = _infer_base_table(chunk)
    if not base_table:
        return None

    where_columns = _extract_columns(chunk, _WHERE_METHOD_RE)
    order_columns = _extract_columns(chunk, _ORDER_METHOD_RE)
    group_columns = _extract_columns(chunk, _GROUP_METHOD_RE)
    join_tables = [sanitize_identifier(item) for item in _JOIN_TABLE_RE.findall(chunk) if sanitize_identifier(item)]

    if not where_columns and not order_columns and not group_columns and not join_tables:
        return None

    parts = [f"SELECT * FROM {base_table}"]
    for join_table in _dedupe(join_tables):
        parts.append(f"JOIN {join_table} ON 1 = 1")
    if where_columns:
        parts.append("WHERE " + " AND ".join(f"{col} = ?" for col in _dedupe(where_columns)))
    if group_columns:
        parts.append("GROUP BY " + ", ".join(_dedupe(group_columns)))
    if order_columns:
        parts.append("ORDER BY " + ", ".join(_dedupe(order_columns)))
    pseudo_sql = normalize_whitespace(" ".join(parts))
    return pseudo_sql if looks_like_sql(pseudo_sql) else None


def _infer_base_table(chunk: str) -> str | None:
    for index, pattern in enumerate(_BASE_TABLE_PATTERNS):
        match = pattern.search(chunk)
        if not match:
            continue
        if index == 2:
            class_name = sanitize_identifier(match.group(1))
            if not class_name:
                return None
            return _pluralize(camel_to_snake(class_name))
        table = sanitize_identifier(match.group(1))
        if table:
            return table
    return None


def _extract_columns(chunk: str, pattern: re.Pattern[str]) -> list[str]:
    return [col for col in (sanitize_identifier(item) for item in pattern.findall(chunk)) if col]


def _unquote_literal(value: str) -> str:
    if value.startswith("'''") and value.endswith("'''"):
        body = value[3:-3]
    elif value.startswith('"""') and value.endswith('"""'):
        body = value[3:-3]
    else:
        body = value[1:-1]
    body = body.replace("\\n", " ").replace("\\t", " ").replace("\\r", " ")
    body = body.replace('\\"', '"').replace("\\'", "'").replace("\\`", "`")
    body = re.sub(r"\$\{[^}]+\}", "?", body)
    body = re.sub(r"\{[^{}]+\}", "?", body)
    return body


def _pluralize(word: str) -> str:
    if not word:
        return word
    if word.endswith("y") and len(word) > 1 and word[-2] not in "aeiou":
        return f"{word[:-1]}ies"
    if word.endswith("s"):
        return word
    return f"{word}s"


def _dedupe(items: list[str]) -> list[str]:
    seen: set[str] = set()
    ordered: list[str] = []
    for item in items:
        if item in seen:
            continue
        seen.add(item)
        ordered.append(item)
    return ordered


def _is_confident_sql_literal(text: str) -> bool:
    normalized = normalize_whitespace(text)
    lower = normalized.lower()
    if len(lower) < 16:
        return False
    if "<" in lower or "</" in lower:
        return False
    if " function " in f" {lower} ":
        return False

    starts = (
        "select ",
        "insert ",
        "update ",
        "delete ",
        "with ",
        "create ",
        "alter ",
        "drop ",
        "replace ",
        "merge ",
    )
    if not lower.startswith(starts):
        return False

    if lower.startswith("select "):
        return " from " in lower
    if lower.startswith("insert "):
        return " into " in lower
    if lower.startswith("update "):
        return " set " in lower
    if lower.startswith("delete "):
        return " from " in lower
    if lower.startswith("with "):
        return " select " in lower and " from " in lower
    if lower.startswith("create "):
        return any(keyword in lower for keyword in (" table ", " index ", " view ", " schema ", " database ", " extension "))
    if lower.startswith("alter "):
        return any(keyword in lower for keyword in (" table ", " index ", " view "))
    if lower.startswith("drop "):
        return any(keyword in lower for keyword in (" table ", " index ", " view ", " schema ", " database "))
    if lower.startswith("replace "):
        return " into " in lower
    if lower.startswith("merge "):
        return " into " in lower
    return looks_like_sql(normalized)


def _is_likely_minified(file_path: Path, source: str) -> bool:
    name = file_path.name.lower()
    if ".min." in name:
        return True
    lines = source.splitlines()
    if not lines:
        return False
    max_line_length = max(len(line) for line in lines)
    average_line_length = sum(len(line) for line in lines) / max(1, len(lines))
    return max_line_length >= 2000 and average_line_length >= 300
