from dataclasses import dataclass, field
from typing import Any, List, Optional, Callable, Dict, TYPE_CHECKING

from .operators import Eq, Gt, Gte, Lt, Lte, In, Like, ILike, NotEq, IsNull, IsNotNull, RawExpr

if TYPE_CHECKING:
    from ..clients.base import DBClient
    from .queryset import QuerySet


@dataclass
class WhereCondition:
    column: Optional[str] = None
    operator: Any = None
    is_or: bool = False
    group: Optional["WhereGroup"] = None


@dataclass
class WhereGroup:
    conditions: List[WhereCondition] = field(default_factory=list)
    is_or: bool = False
    nested_path: Optional[str] = None


@dataclass
class JoinDef:
    """Definition of a single JOIN clause."""
    table: str
    on_clause: str
    join_type: str = "INNER"  # INNER, LEFT, RIGHT, FULL


@dataclass
class Query:
    table: str
    selects: List[str] = field(default_factory=lambda: ["*"])
    wheres: List[WhereCondition] = field(default_factory=list)
    order_by: List[str] = field(default_factory=list)
    limit: Optional[int] = None
    offset: Optional[int] = None
    group_by: List[str] = field(default_factory=list)
    joins: List[JoinDef] = field(default_factory=list)
    _params: Dict[str, Any] = field(default_factory=dict)


class QueryBuilder:
    def __init__(self, client: "DBClient", table: str):
        self.client = client
        self.query = Query(table=table)
        self._current_group: Optional[WhereGroup] = None

    def where(self, *args) -> "QueryBuilder":
        condition = self._build_condition(*args)
        condition.is_or = False
        if self._current_group is not None:
            self._current_group.conditions.append(condition)
        else:
            self.query.wheres.append(condition)
        return self

    def or_where(self, *args) -> "QueryBuilder":
        condition = self._build_condition(*args)
        condition.is_or = True
        if self._current_group is not None:
            self._current_group.conditions.append(condition)
        else:
            self.query.wheres.append(condition)
        return self

    def _build_condition(self, *args) -> WhereCondition:
        if len(args) == 2:
            column, value = args
            # Detect tuple form: ("IN", [...]), (">=", 5), etc.
            # This lets callers pass conditions as dicts with tuple values.
            if isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], str):
                return self._build_condition(column, value[0], value[1])
            if isinstance(value, (Eq, Gt, Gte, Lt, Lte, In, Like, ILike, NotEq,
                                   IsNull, IsNotNull, RawExpr)):
                return WhereCondition(column, value)
            else:
                return WhereCondition(column, Eq(value))
        elif len(args) == 3:
            column, op_str, value = args
            op_str = op_str.strip().upper()
            # Handle IS NULL / IS NOT NULL specially
            if op_str == "IS":
                return WhereCondition(column, IsNull())
            if op_str == "IS NOT":
                return WhereCondition(column, IsNotNull())
            op_map = {
                "=": Eq, "==": Eq, ">": Gt, ">=": Gte,
                "<": Lt, "<=": Lte, "!=": NotEq,
                "IN": In, "LIKE": Like, "ILIKE": ILike,
            }
            operator = op_map.get(op_str, Eq)(value)
            return WhereCondition(column, operator)
        raise TypeError("where() / or_where() accepts 2 or 3 arguments")

    def where_group(self, callback: Callable[["QueryBuilder"], "QueryBuilder"]) -> "QueryBuilder":
        group = WhereGroup(is_or=False)
        previous = self._current_group
        self._current_group = group
        callback(self)
        self._current_group = previous
        self.query.wheres.append(WhereCondition(group=group))
        return self
    
    def or_where_group(self, callback: Callable[["QueryBuilder"], "QueryBuilder"]) -> "QueryBuilder":
        group = WhereGroup(is_or=True)
        previous = self._current_group
        self._current_group = group
        callback(self)
        self._current_group = previous
        self.query.wheres.append(WhereCondition(group=group, is_or=True))
        return self

    def where_nested(self, path: str, callback: Callable[["QueryBuilder"], "QueryBuilder"]) -> "QueryBuilder":
        group = WhereGroup(is_or=False, nested_path=path)
        previous = self._current_group
        self._current_group = group
        callback(self)
        self._current_group = previous
        self.query.wheres.append(WhereCondition(group=group))
        return self

    def or_where_nested(self, path: str, callback: Callable[["QueryBuilder"], "QueryBuilder"]) -> "QueryBuilder":
        group = WhereGroup(is_or=True, nested_path=path)
        previous = self._current_group
        self._current_group = group
        callback(self)
        self._current_group = previous
        self.query.wheres.append(WhereCondition(group=group, is_or=True))
        return self

    def where_eq(self, column: str, value: Any) -> "QueryBuilder":
        return self.where(column, value)
    
    def where_gte(self, column: str, value: Any) -> "QueryBuilder":
        return self.where(column, Gte(value))
    
    def where_gt(self, column: str, value: Any) -> "QueryBuilder":
        return self.where(column, Gt(value))
    
    def where_lte(self, column: str, value: Any) -> "QueryBuilder":
        return self.where(column, Lte(value))
    
    def where_lt(self, column: str, value: Any) -> "QueryBuilder":
        return self.where(column, Lt(value))
    
    def where_not_eq(self, column: str, value: Any) -> "QueryBuilder":
        return self.where(column, NotEq(value))
    
    def where_in(self, column: str, values: List[Any]) -> "QueryBuilder":
        return self.where(column, In(values))
    
    def where_like(self, column: str, pattern: str) -> "QueryBuilder":
        return self.where(column, Like(pattern))
    
    def where_ilike(self, column: str, pattern: str) -> "QueryBuilder":
        return self.where(column, ILike(pattern))

    # ------------------------------------------------------------------
    # NULL checks
    # ------------------------------------------------------------------

    def where_null(self, column: str) -> "QueryBuilder":
        """WHERE column IS NULL."""
        return self.where(column, IsNull())

    def where_not_null(self, column: str) -> "QueryBuilder":
        """WHERE column IS NOT NULL."""
        return self.where(column, IsNotNull())

    # ------------------------------------------------------------------
    # Raw expression WHERE helpers
    # ------------------------------------------------------------------

    def where_raw(self, column: str, raw_sql: str, params: Dict[str, Any] | None = None) -> "QueryBuilder":
        """WHERE column <raw SQL expression>.

        The *raw_sql* is the right-hand side including the operator
        (e.g. ``">= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL :days DAY))"``).
        Bound parameters are accumulated into the query.
        """
        expr = RawExpr(raw_sql, params or {})
        self.query._params.update(expr.params)
        self.query.wheres.append(WhereCondition(column=column, operator=expr))
        return self

    # ------------------------------------------------------------------
    # Grouping & joining
    # ------------------------------------------------------------------

    def group_by(self, *fields: str) -> "QueryBuilder":
        """Add GROUP BY clause."""
        self.query.group_by = list(fields)
        return self

    def join(self, table: str, on_clause: str, join_type: str = "INNER") -> "QueryBuilder":
        """Add a JOIN clause.

        Example:
            db.table("metrics rt").join(
                "messages m", "m.message_id = rt.message_id AND m.peer_id = rt.peer_id"
            )
        """
        self.query.joins.append(JoinDef(table=table, on_clause=on_clause, join_type=join_type))
        return self

    def select(self, *fields: str) -> "QueryBuilder":
        self.query.selects = list(fields) if fields else ["*"]
        return self

    def order_by(self, *fields: str) -> "QueryBuilder":
        self.query.order_by = list(fields)
        return self

    def limit(self, n: int) -> "QueryBuilder":
        self.query.limit = n
        return self

    def offset(self, n: int) -> "QueryBuilder":
        self.query.offset = n
        return self
    
    def to_sql(self) -> str:
        """
        Returns the raw SQL query string that would be executed.
        Useful for debugging and logging.
        
        Example:
            sql = db.table("users").where("age", ">", 18).to_sql()
            print(sql)  # SELECT * FROM users WHERE age > 18
        """
        if hasattr(self.client, 'raw_query'):
            sql, params = self.client.raw_query(self.query)
            return sql
        raise NotImplementedError("This database client does not support raw_query()")

    async def get(self) -> "QuerySet":
        return await self.client._execute(self.query)

    async def all(self) -> List[Dict]:
        return (await self.get()).all()

    async def first(self) -> Optional[Dict]:
        return (await self.limit(1).get()).first()

    async def one(self) -> Dict:
        return (await self.get()).one()

    async def count(self) -> int:
        return (await self.get()).count()

    async def exists(self) -> bool:
        return (await self.count()) > 0

    # -- Mutation helpers on the builder --

    async def create(
        self,
        data: Dict[str, Any],
        returning: Optional[List[str]] = None,
    ) -> Optional[Dict[str, Any]]:
        """
        Insert a single record into the table.

        Example:
            user = await db.table("users").create({"name": "John", "email": "john@example.com"})
        """
        return await self.client._insert_one(self.query.table, data, returning=returning)

    async def create_many(
        self,
        data: List[Dict[str, Any]],
        returning: Optional[List[str]] = None,
    ) -> List[Dict[str, Any]]:
        """
        Insert multiple records into the table.

        Example:
            users = await db.table("users").create_many([
                {"name": "Alice", "email": "alice@example.com"},
                {"name": "Bob", "email": "bob@example.com"},
            ])
        """
        return await self.client._insert_many(self.query.table, data, returning=returning)

    async def save(
        self,
        data: Dict[str, Any],
        id_field: str = "id",
        returning: Optional[List[str]] = None,
    ) -> Optional[Dict[str, Any]]:
        """
        Insert or update a record (upsert). If the data contains the id_field
        and a matching record exists, it updates; otherwise it inserts.

        Example:
            user = await db.table("users").save({"id": 1, "name": "Updated Name"})
        """
        return await self.client.save(self.query.table, data, id_field=id_field, returning=returning)

    async def update(self, **values: Any) -> int:
        """
        Update records matching the current query conditions.

        Example:
            updated = await db.table("users").where("status", "inactive").update(status="active")
        """
        conditions = self._extract_simple_conditions()
        return await self.client._update(
            self.query.table, conditions, values, limit=self.query.limit
        )

    async def delete(self) -> int:
        """
        Delete records matching the current query conditions.

        Example:
            deleted = await db.table("users").where("status", "inactive").delete()
        """
        conditions = self._extract_simple_conditions()
        return await self.client._delete(
            self.query.table, conditions, limit=self.query.limit
        )

    async def remove(self) -> int:
        """
        Alias for delete(). Removes records matching the current query conditions.

        Example:
            removed = await db.table("users").where("status", "banned").remove()
        """
        return await self.delete()

    def _extract_simple_conditions(self) -> Dict[str, Any]:
        """
        Extract simple equality and IN conditions from the query's where clauses.
        Only flattens top-level, non-OR Eq and In conditions; OR groups, OR'd
        conditions, and other operators are silently skipped (the caller should use
        the client directly for those).
        """
        conditions: Dict[str, Any] = {}
        for wc in self.query.wheres:
            if wc.group is not None:
                # Skip grouped/nested conditions — complex logic
                continue
            if wc.is_or:
                # Skip OR'd conditions — can't represent in a simple AND dict
                continue
            if wc.column is not None and wc.operator is not None:
                from .operators import Eq, In as OpIn
                if isinstance(wc.operator, Eq):
                    conditions[wc.column] = wc.operator.value
                elif isinstance(wc.operator, OpIn):
                    conditions[wc.column] = list(wc.operator.values)
        return conditions
