"""
Caprail ORM Query Builder.

Fluent, chainable async query builder inspired by Laravel's Eloquent
query builder. Wraps SQLAlchemy 2.x Core.
"""
from __future__ import annotations

from typing import Any, TYPE_CHECKING

if TYPE_CHECKING:
    from caprail.orm.model import Model


class QueryBuilder:
    """
    Fluent query builder for Caprail models.

    Every method returns ``self`` so calls can be chained::

        users = await User.query().where("active", True).order_by("name").get()
        page  = await User.query().where("role", "admin").paginate(page=1, per_page=15)
    """

    def __init__(self, model_class: type["Model"]) -> None:
        self._model = model_class
        self._wheres: list[dict] = []
        self._order_bys: list[tuple[str, str]] = []
        self._limit_val: int | None = None
        self._offset_val: int | None = None
        self._with_relations: list[str] = []
        self._selects: list[str] = ["*"]

    # ------------------------------------------------------------------
    # Selection
    # ------------------------------------------------------------------

    def select(self, *columns: str) -> "QueryBuilder":
        self._selects = list(columns)
        return self

    # ------------------------------------------------------------------
    # Where clauses
    # ------------------------------------------------------------------

    def where(self, column: str, operator_or_value: Any, value: Any = None) -> "QueryBuilder":
        if value is None:
            operator, val = "=", operator_or_value
        else:
            operator, val = operator_or_value, value
        self._wheres.append({"type": "basic", "column": column, "operator": operator, "value": val, "boolean": "and"})
        return self

    def or_where(self, column: str, operator_or_value: Any, value: Any = None) -> "QueryBuilder":
        if value is None:
            operator, val = "=", operator_or_value
        else:
            operator, val = operator_or_value, value
        self._wheres.append({"type": "basic", "column": column, "operator": operator, "value": val, "boolean": "or"})
        return self

    def where_in(self, column: str, values: list) -> "QueryBuilder":
        self._wheres.append({"type": "in", "column": column, "values": values, "boolean": "and"})
        return self

    def where_not_in(self, column: str, values: list) -> "QueryBuilder":
        self._wheres.append({"type": "not_in", "column": column, "values": values, "boolean": "and"})
        return self

    def where_null(self, column: str) -> "QueryBuilder":
        self._wheres.append({"type": "null", "column": column, "boolean": "and"})
        return self

    def where_not_null(self, column: str) -> "QueryBuilder":
        self._wheres.append({"type": "not_null", "column": column, "boolean": "and"})
        return self

    def where_between(self, column: str, low: Any, high: Any) -> "QueryBuilder":
        self._wheres.append({"type": "between", "column": column, "low": low, "high": high, "boolean": "and"})
        return self

    # ------------------------------------------------------------------
    # Ordering / limiting
    # ------------------------------------------------------------------

    def order_by(self, column: str, direction: str = "asc") -> "QueryBuilder":
        self._order_bys.append((column, direction.lower()))
        return self

    def latest(self, column: str = "created_at") -> "QueryBuilder":
        return self.order_by(column, "desc")

    def oldest(self, column: str = "created_at") -> "QueryBuilder":
        return self.order_by(column, "asc")

    def limit(self, value: int) -> "QueryBuilder":
        self._limit_val = value
        return self

    def take(self, value: int) -> "QueryBuilder":
        return self.limit(value)

    def offset(self, value: int) -> "QueryBuilder":
        self._offset_val = value
        return self

    def skip(self, value: int) -> "QueryBuilder":
        return self.offset(value)

    # ------------------------------------------------------------------
    # Eager loading
    # ------------------------------------------------------------------

    def with_relations(self, *relations: str) -> "QueryBuilder":
        self._with_relations.extend(relations)
        return self

    # ------------------------------------------------------------------
    # Terminal methods
    # ------------------------------------------------------------------

    async def get(self) -> list["Model"]:
        from caprail.orm.model import _get_session
        async with _get_session() as session:
            rows = await self._execute_select(session)
            instances = [self._model._from_row(row) for row in rows]
        if self._with_relations:
            await self._load_relations(instances)
        return instances

    async def first(self) -> "Model | None":
        self.limit(1)
        results = await self.get()
        return results[0] if results else None

    async def first_or_fail(self) -> "Model":
        result = await self.first()
        if result is None:
            raise self._model.ModelNotFoundError(
                f"{self._model.__name__} not found."
            )
        return result

    async def count(self) -> int:
        from caprail.orm.model import _get_session
        import sqlalchemy as sa
        table = self._model._get_table()
        stmt = sa.select(sa.func.count()).select_from(table)
        stmt = self._apply_wheres(stmt, table)
        async with _get_session() as session:
            result = await session.execute(stmt)
            return result.scalar() or 0

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

    async def paginate(self, page: int = 1, per_page: int = 15) -> dict:
        total = await self.count()
        self.offset((page - 1) * per_page).limit(per_page)
        items = await self.get()
        last_page = max(1, (total + per_page - 1) // per_page)
        return {
            "data": items,
            "total": total,
            "per_page": per_page,
            "current_page": page,
            "last_page": last_page,
            "from": (page - 1) * per_page + 1 if items else None,
            "to": (page - 1) * per_page + len(items) if items else None,
        }

    async def delete(self) -> int:
        from caprail.orm.model import _get_session
        import sqlalchemy as sa
        table = self._model._get_table()
        stmt = sa.delete(table)
        stmt = self._apply_wheres_delete(stmt, table)
        async with _get_session() as session:
            result = await session.execute(stmt)
            await session.commit()
            return result.rowcount

    async def update(self, values: dict) -> int:
        from caprail.orm.model import _get_session
        import sqlalchemy as sa
        table = self._model._get_table()
        stmt = sa.update(table).values(**values)
        stmt = self._apply_wheres_update(stmt, table)
        async with _get_session() as session:
            result = await session.execute(stmt)
            await session.commit()
            return result.rowcount

    async def pluck(self, column: str) -> list:
        self.select(column)
        rows = await self.get()
        return [getattr(r, column, None) for r in rows]

    # ------------------------------------------------------------------
    # Internal SQL building
    # ------------------------------------------------------------------

    async def _execute_select(self, session) -> list:
        import sqlalchemy as sa
        table = self._model._get_table()
        if self._selects == ["*"]:
            stmt = sa.select(table)
        else:
            cols = [table.c[c] for c in self._selects]
            stmt = sa.select(*cols)
        stmt = self._apply_wheres(stmt, table)
        for col, direction in self._order_bys:
            c = table.c[col]
            stmt = stmt.order_by(c.asc() if direction == "asc" else c.desc())
        if self._limit_val is not None:
            stmt = stmt.limit(self._limit_val)
        if self._offset_val is not None:
            stmt = stmt.offset(self._offset_val)
        result = await session.execute(stmt)
        return result.mappings().all()

    def _apply_wheres(self, stmt, table):
        import sqlalchemy as sa
        conditions = self._build_conditions(table)
        if conditions:
            stmt = stmt.where(sa.and_(*conditions))
        return stmt

    def _apply_wheres_delete(self, stmt, table):
        import sqlalchemy as sa
        conditions = self._build_conditions(table)
        if conditions:
            stmt = stmt.where(sa.and_(*conditions))
        return stmt

    def _apply_wheres_update(self, stmt, table):
        import sqlalchemy as sa
        conditions = self._build_conditions(table)
        if conditions:
            stmt = stmt.where(sa.and_(*conditions))
        return stmt

    def _build_conditions(self, table) -> list:
        import sqlalchemy as sa
        conditions = []
        for w in self._wheres:
            col = table.c[w["column"]]
            wtype = w["type"]
            if wtype == "basic":
                op = w["operator"]
                val = w["value"]
                if op == "=":
                    cond = col == val
                elif op in ("!=", "<>"):
                    cond = col != val
                elif op == ">":
                    cond = col > val
                elif op == ">=":
                    cond = col >= val
                elif op == "<":
                    cond = col < val
                elif op == "<=":
                    cond = col <= val
                elif op.lower() == "like":
                    cond = col.like(val)
                elif op.lower() == "ilike":
                    cond = col.ilike(val)
                else:
                    cond = col == val
            elif wtype == "in":
                cond = col.in_(w["values"])
            elif wtype == "not_in":
                cond = col.not_in(w["values"])
            elif wtype == "null":
                cond = col.is_(None)
            elif wtype == "not_null":
                cond = col.is_not(None)
            elif wtype == "between":
                cond = col.between(w["low"], w["high"])
            else:
                continue
            conditions.append(cond)
        return conditions

    async def _load_relations(self, instances: list["Model"]) -> None:
        for relation_name in self._with_relations:
            for instance in instances:
                if hasattr(instance, relation_name):
                    rel = getattr(instance, relation_name)
                    if callable(rel):
                        result = await rel()
                        object.__setattr__(instance, f"_loaded_{relation_name}", result)
