from typing import Type, Any, TypeVar, TYPE_CHECKING, Iterable, Optional, Union, List
import functools

if TYPE_CHECKING:
    from .models import Model
    from ..core.db import Database
    from .fields import BinaryExpression

T = TypeVar("T", bound="Model")


def aggregate_method(func):
    @functools.wraps(func)
    def wrapper(self: "QuerySet[T]", *args, **kwargs) -> Any:
        agg_expression = func(self, *args, **kwargs)
        sql, params = self._build_sql(select_expression=agg_expression)
        cursor = self.db.backend.execute(sql, params)
        result = cursor.fetchone()
        return result[0] if result else None

    return wrapper


class Q:
    """
    Encapsulates a collection of query conditions that can be combined
    using logical operators (&, |, ~) to build complex query filters.
    """

    AND = "AND"
    OR = "OR"

    def __init__(
        self, *args: Any, _connector: str = AND, _negated: bool = False, **kwargs: Any
    ):
        self.children: List[Any] = list(args) + list(kwargs.items())
        self.connector = _connector
        self.negated = _negated

    def __and__(self, other: "Q") -> "Q":
        return self._combine(other, self.AND)

    def __or__(self, other: "Q") -> "Q":
        return self._combine(other, self.OR)

    def __invert__(self) -> "Q":
        obj = self.copy()
        obj.negated = not self.negated
        return obj

    def _combine(self, other: "Q", connector: str) -> "Q":
        if not isinstance(other, Q):
            raise TypeError(f"Cannot combine Q object with {type(other).__name__}")
        if not self.children:
            return other.copy()
        if not other.children:
            return self.copy()
        new_q = Q(_connector=connector)
        new_q.children = [self, other]
        return new_q

    def copy(self) -> "Q":
        obj = Q(_connector=self.connector, _negated=self.negated)
        obj.children = self.children[:]
        return obj

    def __repr__(self):
        neg = "~" if self.negated else ""
        return f"<Q: {neg}{self.connector}({self.children})>"


class QuerySet(Iterable[T]):
    def __init__(self, model_cls: Type[T], db: "Database"):
        self.model_cls = model_cls
        self.db = db
        self._filters = Q()
        self._expressions = []
        self._order_by: Optional[str] = None
        self._limit: Optional[int] = None
        self._offset: Optional[int] = None
        self._distinct: bool = False
        self._distinct_fields: List[str] = []
        self.related_fields = []

    def _clone(self) -> "QuerySet[T]":
        """
        Create a copy of the current QuerySet state.
        Ensures that operation line first() and last() dont mutate the original instance.
        """
        query = QuerySet(self.model_cls, self.db)
        query._filters = self._filters.copy()
        query._order_by = self._order_by
        query._limit = self._limit
        query._offset = self._offset
        query._distinct = self._distinct
        query._distinct_fields = self._distinct_fields[:]
        query.related_fields = self.related_fields.copy()
        return query

    def distinct(self, *fields: str) -> "QuerySet[T]":
        """
        Return a new QuerySet instance that will use SELCT DISTINCT.

        Args:
        *fields: Optional list of field names to use for DISTINCT ON (PostgreSQL only)
                 If not provided, uses standard SELECT DISTINCT.
        Returns:
            QuerySet[T]: A new QuerySet instance with DISTINCT enabled.
        """
        clone = self._clone()
        clone._distinct = True
        clone._distinct_fields = list(fields)
        return clone

    def first(self) -> Optional[T]:
        """
        Return the first record matching the current filters, or None if no records exist.
        """
        try:
            return self[0]
        except IndexError:
            return None

    def last(self) -> Optional[T]:
        """
        Return the last record matching the current filters, or None if no records exist.
        Reverses the current ordering and executes with LIMIT 1.
        """
        clone = self._clone()
        if clone._order_by:
            if self._order_by.startswith("-"):
                clone._order_by = clone._order_by[1:]
            else:
                clone._order_by = f"-{self._order_by}"
        else:
            if "id" in clone.model_cls._fields:
                clone._order_by = "-id"
            else:
                raise ValueError(
                    f"last() requires an ordered QuerySet or an 'id' field on model '{self.model_cls.__name__}'"
                )
        try:
            return clone[0]
        except IndexError:
            return None

    def filter(
        self, *args: Union["BinaryExpression", Q], **kwargs: Any
    ) -> "QuerySet[T]":
        """
        Support Standard filters: filter(age=18)
        Support Q object filters: filter(Q(age=18) | Q(name="John"))
        """
        if args or kwargs:
            new_filter = Q(*args, **kwargs)
            if not self._filters.children:
                self._filters = new_filter
            else:
                self._filters &= new_filter
        return self

    def exclude(
        self, *args: Union["BinaryExpression", Q], **kwargs: Any
    ) -> "QuerySet[T]":
        """
        The inverse of filter()
        Takes the same arguments as filter(), wraps then in a Q object negates it,
        and appends it to the current query filters.
        """
        if args or kwargs:
            negated_filter = ~Q(*args, **kwargs)
            if not self._filters.children:
                self._filters = negated_filter
            else:
                self._filters &= negated_filter
        return self

    def all(self) -> List[T]:
        """
        Return all records matching the current filters as a list.
        This is equivalent to list(queryset) but more explicit.
        """
        return list(self)

    def exists(self) -> bool:
        """
        Check if any records exist matching the current filters.
        Returns True if at least one record exists, False otherwise.
        """
        from .sql_builder import SQLBuilder

        sql, params = SQLBuilder(self).build_exists()
        result = self.db.execute_scalar(sql, params)
        return bool(result)

    @property
    def related_field(self):
        return self.related_fields

    @related_field.setter
    def related_field(self, value):
        self.related_fields = value

    def select_related(self, field_name: str) -> "QuerySet[T]":
        if field_name not in self.model_cls._fields:
            raise ValueError(
                f"Field '{field_name}' does not exist on model '{self.model_cls.__name__}'"
            )
        self.related_fields.append(field_name)
        return self

    def order_by(self, field_name: str) -> "QuerySet[T]":
        self._order_by = field_name
        return self

    def limit(self, limit: int) -> "QuerySet[T]":
        self._limit = limit
        return self

    def offset(self, offset: int) -> "QuerySet[T]":
        self._offset = offset
        return self

    @aggregate_method
    def count(self) -> str:
        return "COUNT(*)"

    @aggregate_method
    def sum(self, field_name: str) -> str:
        if field_name not in self.model_cls._fields:
            raise ValueError(
                f"Field '{field_name}' does not exist on model '{self.model_cls.__name__}'"
            )
        return f'SUM("{field_name}")'

    @aggregate_method
    def avg(self, field_name: str) -> str:
        if field_name not in self.model_cls._fields:
            raise ValueError(
                f"Field '{field_name}' does not exist on model '{self.model_cls.__name__}'"
            )
        return f'AVG("{field_name}")'

    def delete(self) -> int:
        """
        Delete all records matching the current filters.

        Returns:
            int: Number of deleted records

        Example:
            # Delete all inactive users
            deleted_count = User.filter(active=False).delete()

            # Delete specific user
            User.filter(id=123).delete()
        """
        from .sql_builder import SQLBuilder

        # For cascade handling, we need to fetch the instances first
        # to handle foreign key constraints properly
        instances = list(self.all())  # Get all instances that will be deleted

        if not instances:
            return 0

        # Handle cascade operations for each instance
        for instance in instances:
            instance._handle_cascade_deletion()

        # Now perform the actual deletion
        sql, params = SQLBuilder(self).build_delete()
        cursor = self.db.execute(sql, params)

        # Remove instances from identity map
        for instance in instances:
            if hasattr(instance, "id") and instance.id is not None:
                self.db.remove_from_identity_map(instance)

        # Get number of affected rows
        deleted_count = (
            cursor.rowcount if hasattr(cursor, "rowcount") else len(instances)
        )

        return deleted_count

    def update(self, **values) -> int:
        """
        Update all records matching the current filters with new values.
        Bypasses model validation for efficiency.

        Args:
            **values: Field names and their new values

        Returns:
            int: Number of updated records

        Example:
            # Update all inactive users to active
            updated_count = User.filter(active=False).update(active=True)

            # Update specific user's name and email
            User.filter(id=123).update(name="John Doe", email="john@example.com")
        """
        if not values:
            return 0

        from .sql_builder import SQLBuilder

        sql, params = SQLBuilder(self).build_update(values)
        cursor = self.db.execute(sql, params)

        # Get number of affected rows
        updated_count = cursor.rowcount if hasattr(cursor, "rowcount") else 0

        return updated_count

    def _build_sql(self, select_expression: Optional[str] = None):
        from .sql_builder import SQLBuilder

        return SQLBuilder(self).build(select_expression)

    def _hydrate_rows(self, rows: tuple, description: tuple) -> Any:
        idx = 0

        model_data = {}
        for field_name, field in self.model_cls._fields.items():
            db_value = rows[idx]
            model_data[field_name] = field.from_db_value(db_value)
            idx += 1

        # Check identity map first if we have an ID
        if model_data.get("id") is not None:
            cached_instance = self.db.get_from_identity_map(
                self.model_cls, model_data["id"]
            )
            if cached_instance is not None:
                instance = cached_instance
                # Skip to related fields processing
                for related_field in self.related_field:
                    fk_field = self.model_cls._fields.get(related_field)
                    related_model = fk_field.to
                    # Advance idx past related fields
                    for _ in related_model._fields.items():
                        idx += 1
                return instance

        instance = self.model_cls(**model_data)

        # Add to identity map
        if instance.id is not None:
            self.db.add_to_identity_map(instance)

        for related_field in self.related_field:
            fk_field = self.model_cls._fields.get(related_field)
            related_model = fk_field.to

            related_data = {}
            has_data = False
            for rel_field_name, rel_field in related_model._fields.items():
                value = rows[idx]
                related_data[rel_field_name] = value
                if value is not None:
                    has_data = True
                idx += 1

            if has_data:
                # Check identity map for related instance
                if related_data.get("id") is not None:
                    cached_related = self.db.get_from_identity_map(
                        related_model, related_data["id"]
                    )
                    if cached_related is not None:
                        related_instance = cached_related
                    else:
                        related_instance = related_model(**related_data)
                        self.db.add_to_identity_map(related_instance)
                else:
                    related_instance = related_model(**related_data)
                    if related_instance.id is not None:
                        self.db.add_to_identity_map(related_instance)

                instance.__dict__[f"_{related_field}_cache"] = related_instance
                instance.__dict__[f"{related_field}_id"] = related_instance.id

        return instance

    def __iter__(self):
        sql, params = self._build_sql()
        cursor = self.db.backend.execute(sql, params)
        while True:
            row = cursor.fetchone()
            if not row:
                break
            yield self._hydrate_rows(row, cursor.description)

    def __len__(self):
        """Return the count of results."""
        sql, params = self._build_sql(select_expression="COUNT(*)")
        cursor = self.db.backend.execute(sql, params)
        result = cursor.fetchone()
        return result[0] if result else 0

    def __getitem__(self, index):
        """Support indexing and slicing."""
        if isinstance(index, slice):
            start = index.start or 0
            stop = index.stop
            step = index.step or 1

            if step != 1:
                raise ValueError("QuerySet slicing with step is not supported")

            query = self._clone()
            query._offset = start
            if stop is not None:
                query._limit = stop - start
            return list(query)
        else:
            if index < 0:
                raise IndexError("Negative indexing is not supported")
            query = self._clone()
            query._offset = index
            query._limit = 1
            results = list(query)
            if not results:
                raise IndexError("QuerySet index out of range")
            return results[0]

    def __eq__(self, other):
        """Support equality comparison."""
        if isinstance(other, list):
            return list(self) == other
        return super().__eq__(other)
