from typing import Any, List, Optional, Tuple, TYPE_CHECKING

from .. import JSONField

if TYPE_CHECKING:
    from .query import QuerySet

from .fields import BinaryExpression
from .query import Q


class SQLBuilder:
    def __init__(self, queryset: "QuerySet"):
        self.queryset = queryset
        self.model_cls = queryset.model_cls
        self.db = queryset.db
        self.table_name = f'"{self.model_cls._table_name}"'
        self.params: List[Any] = []

    def build(self, select_expression: Optional[Any] = None) -> Tuple[str, List[Any]]:
        self.params = []

        columns, joins = self._build_columns_and_joins()

        if select_expression is None:
            select_expression = ", ".join(columns)

        # Handle DISTINCT
        distinct_clause = ""
        if self.queryset._distinct:
            if self.queryset._distinct_fields:
                # Resolve field names to column names for DISTINCT ON
                distinct_cols = []
                for field_name in self.queryset._distinct_fields:
                    if field_name not in self.model_cls._fields:
                        raise ValueError(
                            f"Field '{field_name}' in distinct() does not exist on model '{self.model_cls.__name__}'"
                        )
                    field = self.model_cls._fields[field_name]
                    distinct_cols.append(f'"{field.get_column_name()}"')

                distinct_clause = f"DISTINCT ON ({', '.join(distinct_cols)})"
            else:
                distinct_clause = "DISTINCT"

        sql_parts = ["SELECT"]
        if distinct_clause:
            sql_parts.append(distinct_clause)

        sql_parts.append(select_expression)
        sql_parts.append(f"FROM {self.table_name}")

        sql = " ".join(sql_parts)

        if joins:
            sql += " " + " ".join(joins)

        where_clause = self._build_where_clause()
        if where_clause:
            sql += f" WHERE {where_clause}"

        sql = self._apply_pagination_and_ordering(sql)

        return sql, self.params

    def build_exists(self) -> Tuple[str, List[Any]]:
        """
        Public method to build an optimized EXISTS SQL query.
        Returns (sql_string, params)
        """
        self.params = []

        sql = f"SELECT EXISTS(SELECT 1 FROM {self.table_name}"

        where_clause = self._build_where_clause()
        if where_clause:
            sql += f" WHERE {where_clause}"

        sql += ")"

        return sql, self.params

    def build_delete(self) -> Tuple[str, List[Any]]:
        """
        Build a DELETE SQL query with WHERE clause from current filters.
        Returns (sql_string, params)
        """
        self.params = []

        sql = f"DELETE FROM {self.table_name}"

        where_clause = self._build_where_clause()
        if where_clause:
            sql += f" WHERE {where_clause}"

        return sql, self.params

    def build_update(self, values: dict) -> Tuple[str, List[Any]]:
        """
        Build an UPDATE SQL query with SET and WHERE clauses from current filters.

        Args:
            values: Dictionary of field names and their new values

        Returns:
            Tuple[str, List[Any]]: (sql_string, params)
        """
        self.params = []

        # Build SET clause
        set_clauses = []
        for field_name, value in values.items():
            # Validate field exists on model
            if field_name not in self.model_cls._fields:
                raise ValueError(
                    f"Field '{field_name}' does not exist on model '{self.model_cls.__name__}'"
                )

            field = self.model_cls._fields[field_name]
            column_name = field.get_column_name()

            # Convert value through backend preparation
            db_value = self.db.backend.prepare_value(value)

            set_clauses.append(f'"{column_name}" = ?')
            self.params.append(db_value)

        set_clause = ", ".join(set_clauses)
        sql = f"UPDATE {self.table_name} SET {set_clause}"

        # Add WHERE clause if filters exist
        where_clause = self._build_where_clause()
        if where_clause:
            sql += f" WHERE {where_clause}"

        return sql, self.params

    def _build_columns_and_joins(self) -> Tuple[List[str], List[str]]:
        columns = [
            f'{self.table_name}."{field.get_column_name()}"'
            for field in self.model_cls._fields.values()
        ]
        joins = []
        related_columns = {}

        for field_name in self.queryset.related_field:
            fk_field = self.model_cls._fields.get(field_name)
            if not fk_field or not hasattr(fk_field, "to"):
                raise ValueError(
                    f"Field '{field_name}' is not a valid ForeignKey field on model '{self.model_cls.__name__}'"
                )
            related_model = fk_field.to
            related_table_name = f'"{related_model._table_name}"'

            related_columns[field_name] = []
            for related_field in related_model._fields.values():
                col_name = related_field.get_column_name()
                alias = f'{related_table_name}."{col_name}"'
                columns.append(alias)
                related_columns[field_name].append(col_name)

            joins.append(
                f'LEFT JOIN {related_table_name} ON {self.table_name}."{fk_field.get_column_name()}" = {related_table_name}."id"'
            )

        return columns, joins

    def _build_where_clause(self) -> str:
        sql, params = self._process_q(self.queryset._filters)
        if sql:
            self.params.extend(params)
            return sql
        return ""

    def _process_q(self, q: "Q") -> Tuple[Optional[str], List[Any]]:
        """
        Recursively converts a Q object into SQL WHERE clause and parameters list.
        """
        if not q.children:
            return None, []

        parts = []
        params = []
        placeholder = self.db.backend.placeholder_char
        for child in q.children:
            # Recursive call for nested Q objects
            if isinstance(child, Q):
                sql, child_params = self._process_q(child)
                if sql:
                    parts.append(f"({sql})")
                    params.extend(child_params)
            elif isinstance(child, BinaryExpression):
                sql, child_params = child.to_sql(placeholder)
                parts.append(sql)
                params.extend(child_params)
            elif isinstance(child, tuple):
                key, value = child
                sql, p = self._parse_lookup(key, value)
                parts.append(sql)
                params.extend(p)
        if not parts:
            return None, []

        combined_sql = f" {q.connector} ".join(parts)
        if q.negated:
            return f"NOT ({combined_sql})", params
        return combined_sql, params

    def _parse_lookup(self, key: str, value: Any) -> Tuple[str, List[Any]]:
        """
        Parses Django-style lookups (field__lookup) into SQL.
        """
        parts = key.split("__")
        field_name = parts[0]
        lookup_map = {
            "exact": "=",
            "gt": ">",
            "gte": ">=",
            "lt": "<",
            "lte": "<=",
            "ne": "!=",
            "in": "IN",
            "contains": "LIKE",
            "startswith": "LIKE",
            "endswith": "LIKE",
        }

        # Check if field exists directly
        if field_name not in self.model_cls._fields:
            # Try removing _id suffix for ForeignKey fields
            if field_name.endswith("_id"):
                base_field_name = field_name[:-3]
                if base_field_name in self.model_cls._fields:
                    field = self.model_cls._fields[base_field_name]
                    # Use the column name directly since it already has _id
                    column_name = f'{self.table_name}."{field_name}"'
                else:
                    raise ValueError(
                        f"Field '{field_name}' does not exist on model '{self.model_cls.__name__}'"
                    )
            else:
                raise ValueError(
                    f"Field '{field_name}' does not exist on model '{self.model_cls.__name__}'"
                )
        else:
            field = self.model_cls._fields[field_name]
            column_name = f'{self.table_name}."{field.get_column_name()}"'

        placeholder = self.db.backend.placeholder_char

        lookup = "exact"
        if isinstance(field, JSONField) and len(parts) > 1:
            path_parts = parts[1:]

            if parts[-1] in lookup_map:
                lookup = parts[-1]
                path_parts = parts[1:-1]

            if path_parts:
                cast_type = None
                if isinstance(value, int):
                    cast_type = "INTEGER"
                elif isinstance(value, float):
                    cast_type = "NUMERIC"
                elif isinstance(value, bool):
                    cast_type = "BOOLEAN"

                column_name = self.db.backend.get_json_extract_sql(
                    column_name, path_parts, cast_type
                )

        elif len(parts) > 1:
            lookup = parts[1]
        operator = lookup_map.get(lookup)
        if not operator:
            raise ValueError(f"Unsupported lookup '{lookup}' for field '{field_name}'")

        if lookup == "in":
            placeholders = ", ".join([placeholder] * len(value))
            return (
                f'{self.table_name}."{field.get_column_name()}" IN ({placeholders})',
                list(value),
            )

        if lookup in ("contains", "startswith", "endswith"):
            if lookup == "contains":
                value = f"%{value}%"
            elif lookup == "startswith":
                value = f"{value}%"
            elif lookup == "endswith":
                value = f"%{value}"
            return (
                f'{self.table_name}."{field.get_column_name()}" {operator} {placeholder}',
                [value],
            )
        prepared_value = self.db.backend.prepare_value(value)
        return f"{column_name} {operator} {placeholder}", [prepared_value]

    def _apply_pagination_and_ordering(self, sql: str) -> str:
        if self.queryset._order_by:
            direction = "DESC" if self.queryset._order_by.startswith("-") else "ASC"
            field_name = self.queryset._order_by.lstrip("-")
            sql += f' ORDER BY {self.table_name}."{field_name}" {direction}'

        if self.queryset._limit:
            sql += f" LIMIT {self.queryset._limit}"

        if self.queryset._offset:
            sql += f" OFFSET {self.queryset._offset}"

        return sql
