# src/rhosocial/activerecord/query/base.py
"""BaseQueryMixin implementation."""

from typing import List, Tuple, Optional, Union, Any, overload

from ..backend.expression import (
    BaseExpression,
    SQLPredicate,
    Column,
    RawSQLPredicate,
    WhereClause,
    GroupByHavingClause,
    OrderByClause,
    LimitOffsetClause
)
from ..interface import IQueryBuilding


class BaseQueryMixin(IQueryBuilding):
    """BaseQueryMixin implementation for basic query building operations.

    This class provides foundational query building capabilities for constructing SQL expressions.
    It focuses on query construction methods (WHERE, SELECT, ORDER BY, etc.) without database access.

    For complex logical conditions, use .where() with expression objects that represent
    OR logic. The backend expression system provides better support for complex logical
    predicates.

    The to_sql() method can be used at any time to inspect the constructed SQL query
    and its parameters before execution.

    Note: This class provides query building functionality only. Database access methods
    (all, one, count, exists, etc.) are provided by other mixins like InstanceQueryMixin
    and AggregateQueryMixin.
    """


    def _log(self, level: int, msg: str, *args, **kwargs) -> None:
        """Log query-related messages using backend's logger."""
        # Log using backend's logger if available
        backend = self.backend()
        if hasattr(backend, 'logger'):
            backend.logger.log(level, msg, *args, **kwargs)
        else:
            # Fallback logging
            import logging
            logging.log(level, msg, *args, **kwargs)

    # region Basic Query Methods
    @overload
    def where(self, condition: str, params: Optional[Union[tuple, List[Any]]] = None) -> 'BaseQueryMixin':
        """Add AND condition to the query using a SQL placeholder string.

        This requires you to construct SQL condition fragments with question marks as
        placeholders for parameters, with actual parameters appearing in 'params' in
        the order of placeholders in the SQL.

        Note: User input must absolutely not be directly concatenated here, as it
        would create SQL injection security vulnerabilities.

        Both overloads can be chained together and will be combined into a unified
        WHERE clause with AND logic.

        Args:
            condition: A SQL placeholder string with parameters (e.g., "name = ? AND age > ?")
            params: Query parameters for the placeholders

        Returns:
            Query instance for method chaining
        """
        ...

    @overload
    def where(self, condition: SQLPredicate, params: None = None) -> 'BaseQueryMixin':
        """Add AND condition to the query using a predicate expression.

        This requires you to provide a query predicate. Query predicates can be
        instantiated manually or generated by ActiveRecord field proxy column names,
        such as User.c.age > 25, User.c.name.like('%test%'), or User.c.status.in_([1, 2, 3]).

        Both overloads can be chained together and will be combined into a unified
        WHERE clause with AND logic.

        Args:
            condition: A predicate expression (e.g., User.c.age > 25, which is a SQLPredicate instance)
            params: Should be None when using predicate expressions

        Returns:
            Query instance for method chaining
        """
        ...

    def where(self, condition, params=None):
        """Add AND condition to the query.

        Args:
            condition: Condition expression. Can be:
                      1. A predicate expression (e.g., User.c.age > 25, which is a SQLPredicate instance)
                      2. A SQL placeholder string with parameters (e.g., "name = ? AND age > ?")
            params: Query parameters for placeholder strings (not used with expression objects)

        Returns:
            Query instance for method chaining

        Examples:
            1. Using ActiveRecord field proxy (recommended)
            User.query().where(User.c.status == 'active')
            User.query().where(User.c.age >= 18)
            User.query().where(User.c.name.like('%john%'))

            2. Using complex expressions with ActiveRecord field proxy
            User.query().where((User.c.age >= 18) & (User.c.status == 'active'))
            User.query().where(User.c.created_at >= datetime.now() - timedelta(days=30))

            3. Using raw SQL string with parameters (use with caution)
            # Warning: When using raw SQL strings, you must ensure the query is safe from SQL injection
            User.query().where('status = ?', ('active',))
            User.query().where('age >= ? AND status = ?', (18, 'active'))

        See overloaded method signatures for parameter details.
        """
        # Get dialect from backend
        dialect = self.backend().dialect

        # Convert string condition to SQLPredicate
        if isinstance(condition, str):
            # Use the new RawSQLPredicate class to handle raw SQL string conditions
            predicate = RawSQLPredicate(dialect, condition, tuple(params) if params else ())
        elif isinstance(condition, SQLPredicate):
            # Already a SQLPredicate, use directly
            predicate = condition
        else:
            raise TypeError(f"Condition must be str or SQLPredicate, got {type(condition)}")

        # Use the new and_() method to combine conditions efficiently if where_clause exists, otherwise create new WhereClause
        self.where_clause = self.where_clause.and_(predicate) if self.where_clause else WhereClause(dialect, condition=predicate)

        return self


    def select(self, *columns: Union[str, BaseExpression], append: bool = False) -> 'BaseQueryMixin':
        """Select specific columns or expressions to retrieve from the query.

        This method accepts both column names (strings) and expression objects.

        Args:
            *columns: Variable number of column names (str) or expression objects (BaseExpression) to select
            append: If True, append columns to existing selection.
                   If False (default), replace existing selection.

        Returns:
            IQuery: Query instance for method chaining

        Examples:
            1. Using ActiveRecord field proxy (recommended)
            User.query().select(User.c.id, User.c.name, User.c.email)
            User.query().select(User.c.name).where(User.c.status == 'active')

            2. Using raw column names (use with caution)
            # Warning: When using raw column names as strings, ensure they match your database schema
            User.query().select('id', 'name', 'email')
            User.query().select('name').where('status = ?', ('active',))
        """
        # Get dialect from backend
        dialect = self.backend().dialect

        # Convert string column names to Column objects
        converted_columns = []
        for col in columns:
            if isinstance(col, str):
                # Create a Column object from the string
                converted_columns.append(Column(dialect, col))
            elif isinstance(col, BaseExpression):
                # Already an expression object, use directly
                converted_columns.append(col)
            else:
                raise TypeError(f"Column must be str or BaseExpression, got {type(col)}")

        # If append is True, add to existing selection; otherwise, replace
        if append and self.select_columns is not None:
            self.select_columns.extend(converted_columns)
        else:
            self.select_columns = converted_columns if converted_columns else None

        return self

    def order_by(self, *clauses: Union[str, BaseExpression, Tuple[Union[BaseExpression, str], str]]) -> 'BaseQueryMixin':
        """Add ORDER BY clauses to the query.

        Args:
            *clauses: Variable number of ordering specifications. Each can be:

                     1. A column name as string (e.g., "name")
                     2. An expression object (e.g., User.c.name, which is a BaseExpression instance)
                     3. A tuple of (expression, direction) where direction is "ASC" or "DESC"

        Returns:
            IQuery: Query instance for method chaining

        Note:
            Unlike WHERE or HAVING clauses, ORDER BY clauses typically do not contain
            parameter placeholders. The expressions used in ORDER BY are usually column
            names, functions, or other deterministic expressions that define sort order.
            Any parameters returned by expression.to_sql() are ignored in ORDER BY context.

        Examples:
            1. Using ActiveRecord field proxy (recommended)
            User.query().order_by(User.c.name)
            User.query().order_by(User.c.created_at, User.c.name)

            2. Using expression objects with direction
            User.query().order_by((User.c.name, "ASC"))
            User.query().order_by((User.c.created_at, "DESC"), (User.c.name, "ASC"))

            3. Using string column names (use with caution)
            # Warning: When using raw column names as strings, ensure they match your database schema
            User.query().order_by("name")
            User.query().order_by("created_at", "name")

            4. With direction specification using tuples
            User.query().order_by(("name", "ASC"))
            User.query().order_by(("created_at", "DESC"), ("name", "ASC"))

            5. Complex expressions
            User.query().order_by(functions.upper(User.c.name))
            User.query().order_by((functions.length(User.c.description), "DESC"))
        """
        # Get dialect from backend
        dialect = self.backend().dialect

        # Convert clauses to the format expected by OrderByClause
        order_expressions = []
        for clause in clauses:
            if isinstance(clause, str):
                # Simple column name - default to ASC
                order_expressions.append((Column(dialect, clause), "ASC"))
            elif isinstance(clause, tuple) and len(clause) == 2:
                # (expression, direction) tuple
                expr, direction = clause
                if isinstance(expr, str):
                    expr_obj = Column(dialect, expr)
                elif isinstance(expr, BaseExpression):
                    expr_obj = expr
                else:
                    raise TypeError(f"Expression must be str or BaseExpression, got {type(expr)}")

                # Validate direction
                direction = direction.upper()
                if direction not in ("ASC", "DESC"):
                    raise ValueError(f"Order direction must be 'ASC' or 'DESC', got '{direction}'")

                order_expressions.append((expr_obj, direction))
            elif isinstance(clause, BaseExpression):
                # Expression object with default direction
                order_expressions.append((clause, "ASC"))
            else:
                raise TypeError(f"Order clause must be str, BaseExpression, or (expression, direction) tuple, got {type(clause)}")

        # Create or update the OrderByClause
        if self.order_by_clause:
            # Extend existing expressions with new ones
            self.order_by_clause.expressions.extend(order_expressions)
        else:
            self.order_by_clause = OrderByClause(dialect, order_expressions)

        return self

    def limit(self, count: Union[int, BaseExpression]) -> 'BaseQueryMixin':
        """Add LIMIT clause to restrict the number of rows returned.

        Args:
            count: Maximum number of rows to return, can be an integer or expression

        Returns:
            IQuery: Query instance for method chaining
        """
        # Get dialect from backend
        dialect = self.backend().dialect

        # Create or update the LimitOffsetClause
        if self.limit_offset_clause:
            # Update existing clause with new limit value
            self.limit_offset_clause.limit = count
        else:
            self.limit_offset_clause = LimitOffsetClause(dialect, limit=count)

        return self

    def offset(self, count: Union[int, BaseExpression]) -> 'BaseQueryMixin':
        """Add OFFSET clause to skip a specified number of rows.

        Args:
            count: Number of rows to skip, can be an integer or expression

        Returns:
            IQuery: Query instance for method chaining
        """
        # Get dialect from backend
        dialect = self.backend().dialect

        # Create or update the LimitOffsetClause
        if self.limit_offset_clause:
            # Update existing clause with new offset value
            self.limit_offset_clause.offset = count
        else:
            self.limit_offset_clause = LimitOffsetClause(dialect, offset=count)

        return self
    # endregion


    # region Aggregate Methods
    def group_by(self, *columns: Union[str, BaseExpression]) -> 'BaseQueryMixin':
        """Add GROUP BY columns for complex aggregations.

        Args:
            *columns: Variable of column names (str) or expression objects (BaseExpression) to group by

        Returns:
            IQuery: Query instance for method chaining

        Note:
            Unlike WHERE or HAVING clauses, GROUP BY clauses typically do not contain
            parameter placeholders. The expressions used in GROUP BY are usually column
            names, functions, or other deterministic expressions that define grouping.
            Any parameters returned by expression.to_sql() are ignored in GROUP BY context.

        Examples:
            1. Using ActiveRecord field proxy (recommended)
            User.query().group_by(User.c.department)
            User.query().group_by(User.c.status, User.c.created_at)

            2. Using raw column names (use with caution)
            # Warning: When using raw column names as strings, ensure they match your database schema
            User.query().group_by('department')
            User.query().group_by('status', 'created_at')
        """
        # Get dialect from backend
        dialect = self.backend().dialect

        # Convert string columns to Column objects
        group_expressions = []
        for col in columns:
            if isinstance(col, str):
                group_expressions.append(Column(dialect, col))
            elif isinstance(col, BaseExpression):
                group_expressions.append(col)
            else:
                raise TypeError(f"Column must be str or BaseExpression, got {type(col)}")

        # Create or update the GroupByHavingClause
        if self.group_by_having_clause:
            # Extend existing group by expressions
            self.group_by_having_clause.group_by.extend(group_expressions)
        else:
            self.group_by_having_clause = GroupByHavingClause(dialect, group_by=group_expressions)

        return self

    @overload
    def having(self, condition: str, params: Optional[Union[tuple, List[Any]]] = None) -> 'BaseQueryMixin':
        """Add HAVING condition using a SQL placeholder string for complex aggregations.

        This requires you to construct SQL condition fragments with question marks as
        placeholders for parameters, with actual parameters appearing in 'params' in
        the order of placeholders in the SQL.

        Note: User input must absolutely not be directly concatenated here, as it
        would create SQL injection security vulnerabilities.

        Both overloads can be chained together and will be combined into a unified
        HAVING clause with AND logic.

        Args:
            condition: A SQL placeholder string with parameters (e.g., "COUNT(*) > ?")
            params: Query parameters for the placeholders

        Returns:
            Query instance for method chaining
        """
        ...

    @overload
    def having(self, condition: SQLPredicate, params: None = None) -> 'BaseQueryMixin':
        """Add HAVING condition using a predicate expression for complex aggregations.

        This requires you to provide a query predicate. Query predicates can be
        instantiated manually or generated by ActiveRecord field proxy column names,
        such as User.c.age > 25, User.c.name.like('%test%'), or User.c.status.in_([1, 2, 3]).

        Both overloads can be chained together and will be combined into a unified
        HAVING clause with AND logic.

        Args:
            condition: A predicate expression (e.g., FunctionCall("COUNT", Column("*")) > 5, which is a SQLPredicate instance)
            params: Should be None when using predicate expressions

        Returns:
            Query instance for method chaining
        """
        ...

    def having(self, condition, params=None) -> 'BaseQueryMixin':
        """Add HAVING condition for complex aggregations.

        Args:
            condition: HAVING condition expression. Can be:
                      1. A predicate expression (e.g., User.c.age > 25, which is a SQLPredicate instance)
                      2. A SQL placeholder string with parameters (e.g., "COUNT(*) > ?")
            params: Query parameters for placeholder strings (not used with expression objects)

        Returns:
            BaseQueryMixin: Query instance for method chaining

        Examples:
            1. Using ActiveRecord field proxy (recommended)
            User.query().group_by(User.c.department).having(functions.count(User.c.id) > 5)

            2. Using raw SQL string with parameters (use with caution)
            # Warning: When using raw SQL strings, you must ensure the query is safe from SQL injection
            User.query().group_by('department').having('COUNT(*) > ?', (5,))

        See overloaded method signatures for parameter details.
        """
        # Get dialect from backend
        dialect = self.backend().dialect

        # Convert string condition to SQLPredicate
        if isinstance(condition, str):
            # Use the new RawSQLPredicate class to handle raw SQL string conditions
            predicate = RawSQLPredicate(dialect, condition, tuple(params) if params else ())
        elif isinstance(condition, SQLPredicate):
            # Object that inherits from SQLPredicate, use directly
            predicate = condition
        else:
            raise TypeError(f"Condition must be str or SQLPredicate, got {type(condition)}")

        # Create or update the GroupByHavingClause with the having condition
        if self.group_by_having_clause:
            # Update existing clause with the having condition
            self.group_by_having_clause.having = predicate
        else:
            # Create a new clause with the having condition
            # Since having requires group by, we'll create an empty group by list
            self.group_by_having_clause = GroupByHavingClause(dialect, group_by=[], having=predicate)

        return self

    # endregion

    def explain(self, **kwargs) -> 'BaseQueryMixin':
        """Enable EXPLAIN for the subsequent query execution.

        This method configures the query to generate an execution plan when executed.
        The explain will be performed when calling execution methods like all(), one(),
        count(), etc.

        The explain() method can be called at any point after query() and before
        the final execution method (all/one/exists/count/etc.). It can also be called
        multiple times, with the last call taking effect.

        Args:
            **kwargs: EXPLAIN options. These will be passed to ExplainOptions.

        Returns:
            IQuery: Query instance for method chaining

        Examples:
            1. Basic explain
            User.query().explain().all()

            2. With analysis and JSON output
            User.query()\\
                .explain(analyze=True, format=ExplainFormat.JSON)\\
                .all()

            3. PostgreSQL specific options
            User.query()\\
                .explain(buffers=True, settings=True)\\
                .all()

            4. Configure explain for aggregate query
            plan = User.query()\\
                .group_by(User.c.department)\\
                .explain(format=ExplainFormat.TEXT)\\
                .count(User.c.id)

            5. Explain can be called at any point before execution
            query = User.query().where(User.c.status == 'active')
            query.explain()  # Enable explain
            result = query.all()  # Will show execution plan

            6. Multiple explain calls (last one takes effect)
            User.query()\\
                .where(User.c.status == 'active')\\
                .explain(format=ExplainFormat.TEXT)\\  # First explain call
                .explain(analyze=True)\\               # Second call overrides first
                .all()                                # Will use analyze=True option
        """
        # Enable explain mode
        self._explain_enabled = True
        self._explain_options = kwargs
        return self

    # endregion