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

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

from ..backend.dialect.exceptions import UnsupportedFeatureError
from ..backend.expression import (
    BaseExpression,
    SQLPredicate,
    Column,
    RawSQLPredicate,
    WhereClause,
    GroupByHavingClause,
    OrderByClause,
    LimitOffsetClause,
    ForUpdateClause,
)
from ..interface import IQueryBuilding
from ..logging.manager import get_logging_manager


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.
    """

    # Instance-level custom logger name
    _logger_name: Optional[str] = None

    def _get_logger_name(self) -> str:
        """Determine the appropriate logger name for this query class.

        Naming rules:
        1. If _logger_name is set, use it directly
        2. For library query classes: Use 'rhosocial.activerecord.query.{ClassName}'
        3. For custom query classes: Use module namespace

        Returns:
            str: The logger name.
        """
        if self._logger_name:
            return self._logger_name

        # Check if this is a library class
        module = self.__class__.__module__
        if module.startswith('rhosocial.activerecord'):
            # Library class: use semantic naming
            base_name = get_logging_manager().LOGGER_QUERY
            return f"{base_name}.{self.__class__.__name__}"
        else:
            # Custom class: use module namespace
            return f"{module}.{self.__class__.__name__}"

    def _log(self, level: int, msg: str, *args, **kwargs) -> None:
        """Log query-related messages using query's own logger."""
        logger_name = self._get_logger_name()
        logger = get_logging_manager().get_logger(logger_name)
        logger.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.

        **IMPORTANT**: This method must be called on a query object returned by Model.query().
        Do NOT call it directly on the Model class like Django's incorrect approach
        (e.g., Model.objects.filter(field=value) or Model.where(field=value)).
        The correct pattern is:

            Model.query().where(Model.c.field == value)

        This design follows the Active Record pattern where Model.query() returns
        a query builder object, and conditions are added via method chaining.

        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

    def for_update(
        self,
        of_columns: Optional[List[Union[str, BaseExpression]]] = None,
        nowait: bool = False,
        skip_locked: bool = False,
    ) -> "BaseQueryMixin":
        """Add FOR UPDATE clause for row-level locking in SELECT statements.

        The FOR UPDATE clause locks selected rows preventing other transactions from
        modifying them until the current transaction is committed or rolled back.
        This is essential for preventing lost updates in concurrent transaction scenarios.

        IMPORTANT: FOR UPDATE only takes effect when called within an active transaction.
        Call backend.begin_transaction() before using this method.

        Args:
            of_columns: Optional list of columns to lock specifically. If None, all selected
                       rows are locked. Can be column names (str) or expression objects.
            nowait: If True, the query will fail immediately if a row is already locked
                   by another transaction, instead of waiting. Default is False.
            skip_locked: If True, locked rows will be skipped instead of waiting for them
                        to be released. Default is False.

        Returns:
            BaseQueryMixin: Query instance for method chaining

        Raises:
            ValueError: If both nowait and skip_locked are True (mutually exclusive)
            UnsupportedFeatureError: If the backend does not support FOR UPDATE

        Examples:
            1. Basic FOR UPDATE (lock all selected rows)
            backend.begin_transaction()
            user = User.query().where(User.c.id == 1).for_update().one()
            user.balance -= 100
            user.save()
            backend.commit_transaction()

            2. FOR UPDATE with NOWAIT (fail immediately if locked)
            backend.begin_transaction()
            try:
                user = User.query().where(User.c.id == 1).for_update(nowait=True).one()
            except DatabaseError:
                # Row is locked by another transaction
                backend.rollback_transaction()
                return
            user.balance -= 100
            user.save()
            backend.commit_transaction()

            3. FOR UPDATE with SKIP LOCKED (skip locked rows)
            backend.begin_transaction()
            users = User.query().where(User.c.status == 'pending').for_update(skip_locked=True).all()
            for user in users:
                user.status = 'processing'
                user.save()
            backend.commit_transaction()

            4. FOR UPDATE with specific columns (PostgreSQL/Oracle specific)
            backend.begin_transaction()
            user = User.query().where(User.c.id == 1).for_update(of_columns=[User.c.id]).one()
            user.balance -= 100
            user.save()
            backend.commit_transaction()

            5. Cross-database compatible usage
            # Check if FOR UPDATE is supported before using it
            if Model.backend().dialect.supports_for_update():
                user = Model.query().where(Model.c.id == 1).for_update().one()
            else:
                # Use alternative locking strategy (e.g., BEGIN IMMEDIATE for SQLite)
                user = Model.find_one({'id': 1})

        Note:
            - FOR UPDATE must be used within a transaction to have any effect
            - Locking behavior varies by database backend:
              - MySQL: Supports FOR UPDATE, FOR UPDATE NOWAIT (8.0+), FOR UPDATE SKIP LOCKED (8.0+)
              - PostgreSQL: Supports FOR UPDATE, FOR UPDATE NOWAIT, FOR UPDATE SKIP LOCKED, FOR UPDATE OF
              - SQLite: Does NOT support FOR UPDATE (uses database-level file locking instead)
            - For SQLite, use backend.begin_transaction() with appropriate isolation level,
              or check supports_for_update() before calling this method
        """
        # Validate mutually exclusive options
        if nowait and skip_locked:
            raise ValueError("nowait and skip_locked are mutually exclusive options")

        # Get dialect from backend
        dialect = self.backend().dialect

        # Check if FOR UPDATE is supported by this backend
        if not dialect.supports_for_update():
            raise UnsupportedFeatureError(
                dialect.name,
                "FOR UPDATE clause",
                "This backend does not support row-level locking with FOR UPDATE. "
                "Use dialect.supports_for_update() to check support before calling this method. "
                "For SQLite, use BEGIN IMMEDIATE transactions for write serialization."
            )

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

        # Create the FOR UPDATE clause
        self._for_update_clause = ForUpdateClause(
            dialect,
            of_columns=converted_columns,
            nowait=nowait,
            skip_locked=skip_locked,
        )

        return self

    # endregion
