from __future__ import annotations

import ast
from pathlib import Path

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

_SQL_CALL_NAMES = {
    "execute",
    "executemany",
    "query",
    "raw",
    "text",
}

_SQL_KEYWORDS = {
    "sql",
    "query",
    "statement",
    "text",
}


def extract_python_file(file_path: Path) -> tuple[list[QueryCandidate], list[str]]:
    source = file_path.read_text(encoding="utf-8", errors="ignore")
    try:
        tree = ast.parse(source, filename=str(file_path))
    except SyntaxError as exc:
        return [], [f"Python AST fallback could not parse {file_path}: {exc.msg} (line {exc.lineno})."]

    visitor = _PythonSqlVisitor(file_path=file_path)
    visitor.visit(tree)
    return visitor.candidates, []


class _PythonSqlVisitor(ast.NodeVisitor):
    def __init__(self, file_path: Path):
        self.file_path = file_path
        self.function_stack: list[str] = []
        self.scope_stack: list[dict[str, str]] = [{}]
        self.candidates: list[QueryCandidate] = []

    def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
        self.function_stack.append(node.name)
        self.scope_stack.append(dict(self.scope_stack[-1]))
        self.generic_visit(node)
        self.scope_stack.pop()
        self.function_stack.pop()

    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
        self.function_stack.append(node.name)
        self.scope_stack.append(dict(self.scope_stack[-1]))
        self.generic_visit(node)
        self.scope_stack.pop()
        self.function_stack.pop()

    def visit_ClassDef(self, node: ast.ClassDef) -> None:
        self.scope_stack.append(dict(self.scope_stack[-1]))
        self.generic_visit(node)
        self.scope_stack.pop()

    def visit_Assign(self, node: ast.Assign) -> None:
        sql_text = self._resolve_sql_expr(node.value)
        if sql_text:
            for target in node.targets:
                if isinstance(target, ast.Name):
                    self.scope_stack[-1][target.id] = sql_text
        self.generic_visit(node)

    def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
        if isinstance(node.target, ast.Name) and node.value is not None:
            sql_text = self._resolve_sql_expr(node.value)
            if sql_text:
                self.scope_stack[-1][node.target.id] = sql_text
        self.generic_visit(node)

    def visit_Call(self, node: ast.Call) -> None:
        call_name = self._call_name(node.func)
        if call_name in _SQL_CALL_NAMES:
            sql_text = self._resolve_sql_from_call(node)
            if sql_text and looks_like_sql(sql_text):
                self.candidates.append(
                    QueryCandidate(
                        sql=normalize_whitespace(sql_text),
                        context=QueryContext(
                            file_path=str(self.file_path),
                            line=max(1, int(getattr(node, "lineno", 1))),
                            function=self.function_stack[-1] if self.function_stack else None,
                            language="python",
                            source_kind="raw_sql",
                        ),
                        confidence=0.83,
                        metadata={
                            "extractor": "python_ast_fallback",
                            "call": call_name,
                        },
                    )
                )
        self.generic_visit(node)

    def _resolve_sql_from_call(self, node: ast.Call) -> str | None:
        if node.args:
            sql_text = self._resolve_sql_expr(node.args[0])
            if sql_text:
                return sql_text
        for keyword in node.keywords:
            if keyword.arg and keyword.arg.lower() in _SQL_KEYWORDS:
                sql_text = self._resolve_sql_expr(keyword.value)
                if sql_text:
                    return sql_text
        return None

    def _resolve_sql_expr(self, node: ast.AST) -> str | None:
        if isinstance(node, ast.Constant) and isinstance(node.value, str):
            text = node.value.strip()
            return text if text else None

        if isinstance(node, ast.JoinedStr):
            parts: list[str] = []
            for value in node.values:
                if isinstance(value, ast.Constant) and isinstance(value.value, str):
                    parts.append(value.value)
                elif isinstance(value, ast.FormattedValue):
                    parts.append("{}")
            text = "".join(parts).strip()
            return text if text else None

        if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
            left = self._resolve_sql_expr(node.left)
            right = self._resolve_sql_expr(node.right)
            if left and right:
                return f"{left}{right}"
            return left or right

        if isinstance(node, ast.Call):
            call_name = self._call_name(node.func)
            if call_name == "text" and node.args:
                return self._resolve_sql_expr(node.args[0])
            return None

        if isinstance(node, ast.Name):
            for scope in reversed(self.scope_stack):
                if node.id in scope:
                    return scope[node.id]

        return None

    @staticmethod
    def _call_name(node: ast.AST) -> str:
        if isinstance(node, ast.Name):
            return node.id.lower()
        if isinstance(node, ast.Attribute):
            return node.attr.lower()
        return ""
