"""MongoDB query filter visitor for therismos expressions.

This module provides a visitor that converts therismos expressions into MongoDB
query filters compatible with PyMongo and Motor.

The MongoDB visitor does not require PyMongo or Motor to be installed for the
conversion itself - it simply returns dictionaries in MongoDB query format.
However, you'll need one of these libraries to actually execute the queries:

- PyMongo: Synchronous MongoDB driver
- Motor: Asynchronous MongoDB driver for asyncio

Example
-------
>>> from therismos import F, optimize
>>> from therismos.expr.visitors.mongo import MongoVisitor
>>>
>>> age = F("age")
>>> status = F("status")
>>> expr = (age > 18) & (status == "active")
>>>
>>> visitor = MongoVisitor()
>>> mongo_filter = expr.accept(visitor)
>>> # Result: {"$and": [{"age": {"$gt": 18}}, {"status": "active"}]}
>>>
>>> # Use with PyMongo
>>> # collection.find(mongo_filter)
>>>
>>> # Use with Motor
>>> # await collection.find(mongo_filter).to_list(length=100)

Notes
-----
- Regex flags are converted to MongoDB regex option strings
- NULL checks use MongoDB's null type matching
- TRUE expressions convert to empty filter {} (matches all documents)
- FALSE expressions convert to {$expr: false} (matches no documents)
"""

from __future__ import annotations

import re
from typing import Any

from therismos.expr import (
    AllExpr,
    AnyExpr,
    Eq,
    Ge,
    Gt,
    In,
    IsNull,
    Le,
    Lt,
    Ne,
    NotExpr,
    Regex,
)


class MongoVisitor:
    """Visitor that converts therismos expressions to MongoDB query filters.

    This visitor traverses expression trees and generates MongoDB query filter
    dictionaries that can be used directly with PyMongo's find(), update_many(),
    delete_many(), and other methods, or with Motor's async equivalents.

    :ivar bool optimize_simple_and: Whether to optimize simple AND expressions
        by merging field conditions into a single document when possible.
        Defaults to True.
    :vartype optimize_simple_and: bool

    Example
    -------
    >>> from therismos import F
    >>> from therismos.expr.visitors.mongo import MongoVisitor
    >>>
    >>> age = F("age")
    >>> name = F("name")
    >>> expr = (age >= 21) & (name == "Alice")
    >>>
    >>> visitor = MongoVisitor()
    >>> mongo_filter = expr.accept(visitor)
    >>> print(mongo_filter)
    {'age': {'$gte': 21}, 'name': 'Alice'}
    """

    def __init__(self, optimize_simple_and: bool = True) -> None:
        """Initialize the MongoDB visitor.

        :param optimize_simple_and: Whether to optimize simple AND expressions
            by merging field conditions into a single document. When True,
            expressions like (age > 18) & (name == "Alice") become
            {age: {$gt: 18}, name: "Alice"} instead of
            {$and: [{age: {$gt: 18}}, {name: "Alice"}]}.
        :type optimize_simple_and: bool
        """
        self.optimize_simple_and = optimize_simple_and

    def visit_eq(self, expr: Eq) -> dict[str, Any]:
        """Convert equality expression to MongoDB filter.

        :param expr: Equality expression.
        :type expr: Eq
        :returns: MongoDB filter like {field: value}.
        :rtype: dict[str, Any]
        """
        return {expr.field.name: expr.casted_value()}

    def visit_ne(self, expr: Ne) -> dict[str, Any]:
        """Convert inequality expression to MongoDB filter.

        :param expr: Inequality expression.
        :type expr: Ne
        :returns: MongoDB filter like {field: {$ne: value}}.
        :rtype: dict[str, Any]
        """
        return {expr.field.name: {"$ne": expr.casted_value()}}

    def visit_lt(self, expr: Lt) -> dict[str, Any]:
        """Convert less-than expression to MongoDB filter.

        :param expr: Less-than expression.
        :type expr: Lt
        :returns: MongoDB filter like {field: {$lt: value}}.
        :rtype: dict[str, Any]
        """
        return {expr.field.name: {"$lt": expr.casted_value()}}

    def visit_le(self, expr: Le) -> dict[str, Any]:
        """Convert less-than-or-equal expression to MongoDB filter.

        :param expr: Less-than-or-equal expression.
        :type expr: Le
        :returns: MongoDB filter like {field: {$lte: value}}.
        :rtype: dict[str, Any]
        """
        return {expr.field.name: {"$lte": expr.casted_value()}}

    def visit_gt(self, expr: Gt) -> dict[str, Any]:
        """Convert greater-than expression to MongoDB filter.

        :param expr: Greater-than expression.
        :type expr: Gt
        :returns: MongoDB filter like {field: {$gt: value}}.
        :rtype: dict[str, Any]
        """
        return {expr.field.name: {"$gt": expr.casted_value()}}

    def visit_ge(self, expr: Ge) -> dict[str, Any]:
        """Convert greater-than-or-equal expression to MongoDB filter.

        :param expr: Greater-than-or-equal expression.
        :type expr: Ge
        :returns: MongoDB filter like {field: {$gte: value}}.
        :rtype: dict[str, Any]
        """
        return {expr.field.name: {"$gte": expr.casted_value()}}

    def visit_regex(self, expr: Regex) -> dict[str, Any]:
        """Convert regex expression to MongoDB filter.

        :param expr: Regex expression.
        :type expr: Regex
        :returns: MongoDB filter like {field: {$regex: pattern, $options: flags}}.
        :rtype: dict[str, Any]
        """
        filter_dict: dict[str, Any] = {expr.field.name: {"$regex": expr.value}}

        if expr.flags is not None:
            # Convert Python re flags to MongoDB regex options
            options = ""
            if expr.flags & re.IGNORECASE:
                options += "i"
            if expr.flags & re.MULTILINE:
                options += "m"
            if expr.flags & re.DOTALL:
                options += "s"
            # MongoDB also supports 'x' for extended/verbose mode but Python's re.VERBOSE
            # is different from MongoDB's interpretation
            if options:
                filter_dict[expr.field.name]["$options"] = options

        return filter_dict

    def visit_in(self, expr: In) -> dict[str, Any]:
        """Convert IN expression to MongoDB filter.

        :param expr: IN expression.
        :type expr: In
        :returns: MongoDB filter like {field: {$in: [values]}}.
        :rtype: dict[str, Any]
        """
        # Cast all values using the field's cast function
        casted_values = [expr.field.cast(v) for v in expr.values]
        return {expr.field.name: {"$in": casted_values}}

    def visit_is_null(self, expr: IsNull) -> dict[str, Any]:
        """Convert null-check expression to MongoDB filter.

        :param expr: Null-check expression.
        :type expr: IsNull
        :returns: MongoDB filter checking for null or not-null.
        :rtype: dict[str, Any]
        """
        if expr.is_null:
            # Check if field is null
            return {expr.field.name: None}
        else:
            # Check if field is not null
            return {expr.field.name: {"$ne": None}}

    def visit_true(self, expr: Any) -> dict[str, Any]:
        """Convert TRUE constant to MongoDB filter.

        :param expr: TRUE expression.
        :type expr: Any
        :returns: Empty filter {} that matches all documents.
        :rtype: dict[str, Any]
        """
        return {}

    def visit_false(self, expr: Any) -> dict[str, Any]:
        """Convert FALSE constant to MongoDB filter.

        :param expr: FALSE expression.
        :type expr: Any
        :returns: MongoDB filter {$expr: false} that matches no documents.
        :rtype: dict[str, Any]
        """
        # Use $expr with a constant false expression (requires MongoDB 3.6+)
        return {"$expr": False}

    def visit_all(self, expr: AllExpr) -> dict[str, Any]:
        """Convert AND expression to MongoDB filter.

        :param expr: AND expression.
        :type expr: AllExpr
        :returns: MongoDB filter using $and or optimized field merging.
        :rtype: dict[str, Any]
        """
        filters = [child.accept(self) for child in expr.exprs]

        # Remove empty filters (TRUE expressions)
        filters = [f for f in filters if f]

        if not filters:
            # All children were TRUE, return empty filter (matches all)
            return {}

        if len(filters) == 1:
            return filters[0]

        # Try to optimize by merging filters on different fields
        if self.optimize_simple_and:
            merged: dict[str, Any] = {}
            needs_and = False

            for f in filters:
                # If filter has only one key and it's not a MongoDB operator
                if len(f) == 1:
                    field_name = next(iter(f.keys()))
                    if not field_name.startswith("$"):
                        # Check if we already have a condition on this field
                        if field_name in merged:
                            needs_and = True
                            break
                        merged[field_name] = f[field_name]
                    else:
                        # Top-level operator like $expr
                        needs_and = True
                        break
                else:
                    # Multiple fields in one filter
                    for field_name in f:
                        if field_name in merged or field_name.startswith("$"):
                            needs_and = True
                            break
                    if needs_and:
                        break
                    merged.update(f)

            if not needs_and:
                return merged

        # Fall back to explicit $and
        return {"$and": filters}

    def visit_any(self, expr: AnyExpr) -> dict[str, Any]:
        """Convert OR expression to MongoDB filter.

        :param expr: OR expression.
        :type expr: AnyExpr
        :returns: MongoDB filter using $or.
        :rtype: dict[str, Any]
        """
        filters = [child.accept(self) for child in expr.exprs]

        # Check for empty filters (TRUE expressions)
        if any(not f for f in filters):
            # At least one child is TRUE, entire OR is TRUE
            return {}

        if not filters:
            # No children, OR of nothing is FALSE
            return {"$expr": False}

        if len(filters) == 1:
            return filters[0]

        return {"$or": filters}

    def visit_not(self, expr: NotExpr) -> dict[str, Any]:
        """Convert NOT expression to MongoDB filter.

        :param expr: NOT expression.
        :type expr: NotExpr
        :returns: MongoDB filter using $nor.
        :rtype: dict[str, Any]
        """
        child_filter = expr.expr.accept(self)

        # Handle special cases
        if not child_filter:
            # NOT(TRUE) = FALSE
            return {"$expr": False}

        if child_filter == {"$expr": False}:
            # NOT(FALSE) = TRUE
            return {}

        # Use $nor for general negation
        # $nor: [condition] is equivalent to NOT(condition)
        return {"$nor": [child_filter]}


__all__ = [
    "MongoVisitor",
]
