"""
Generic query builder for fluent, chainable access to tarot data.

Provides Query and QueryResult classes for building filters and accessing data
in a dynamic, expressive way.

Usage:
    # By name
    result = letter.iching().name('peace')
    result = letter.alphabet().name('english')

    # By filter expressions
    result = letter.iching().filter('number:1')
    result = letter.alphabet().filter('name:hebrew')
    result = number.number().filter('value:5')

    # Get all results
    results = letter.iching().all()  # Dict[int, Hexagram]
    results = letter.iching().list()  # List[Hexagram]
"""

from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union

from utils.object_formatting import format_value, get_object_attributes, is_nested_object

T = TypeVar("T")


class QueryResult:
    """Single result from a query."""

    def __init__(self, data: Any) -> None:
        self.data = data

    def __repr__(self) -> str:
        if hasattr(self.data, "__repr__"):
            return repr(self.data)
        return f"{self.__class__.__name__}({self.data})"

    def __str__(self) -> str:
        if hasattr(self.data, "__str__"):
            return str(self.data)
        return repr(self)

    def __getattr__(self, name: str) -> Any:
        """Pass through attribute access to the wrapped data."""
        if name.startswith("_"):
            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
        return getattr(self.data, name)


class Query:
    """
    Fluent query builder for accessing and filtering tarot data.

    Supports chaining: .filter() → .name() → .get()
    """

    def __init__(self, data: Union[Dict[Any, T], List[T]]) -> None:
        """Initialize with data source (dict or list)."""
        self._original_data = data
        self._data = data if isinstance(data, list) else list(data.values())
        self._filters: List[Callable[[T], bool]] = []

    def filter(self, expression: str) -> "Query":
        """
        Filter by key:value expression.

        Examples:
            .filter('name:peace')
            .filter('number:1')
            .filter('sephera:gevurah')
            .filter('value:5')

        Supports multiple filters by chaining:
            .filter('number:1').filter('name:creative')
        """
        key, value = expression.split(":", 1) if ":" in expression else (expression, "")

        def filter_func(item: T) -> bool:
            # Special handling for 'name' key
            if key == "name":
                if hasattr(item, "name"):
                    value_lower = value.lower()
                    item_name = str(item.name).lower()
                    return value_lower == item_name or value_lower in item_name
                return False

            if not hasattr(item, key):
                return False
            item_value = getattr(item, key)
            # Flexible comparison
            if isinstance(item_value, str):
                return value.lower() in str(item_value).lower()
            else:
                return str(value) in str(item_value)

        self._filters.append(filter_func)
        return self

    def name(self, value: str) -> Optional["QueryResult"]:
        """
        Deprecated: Use .filter('name:value') instead.

        Find item by name (exact or partial match, case-insensitive).
        Returns QueryResult wrapping the found item, or None if not found.
        """
        return self.filter(f"name:{value}").first()

    def get(self) -> Optional["QueryResult"]:
        """
        Get first result matching all applied filters.

        Returns QueryResult or None if no match.
        """
        for item in self._data:
            if all(f(item) for f in self._filters):
                return QueryResult(item)
        return None

    def all(self) -> Dict[Any, T]:
        """
        Get all results matching filters as dict.

        Returns original dict structure (if input was dict) with filtered values.
        """
        filtered = {}
        if isinstance(self._original_data, dict):
            for key, item in self._original_data.items():
                if all(f(item) for f in self._filters):
                    filtered[key] = item
        else:
            for i, item in enumerate(self._data):
                if all(f(item) for f in self._filters):
                    filtered[i] = item
        return filtered

    def list(self) -> List[T]:
        """
        Get all results matching filters as list.

        Returns list of filtered items.
        """
        return [item for item in self._data if all(f(item) for f in self._filters)]

    def first(self) -> Optional["QueryResult"]:
        """Alias for get() - returns first matching item."""
        return self.get()

    def count(self) -> int:
        """Count items matching all filters."""
        return len(self.list())

    def __repr__(self) -> str:
        return f"Query({self.count()} items)"

    def __str__(self) -> str:
        items = self.list()
        if not items:
            return "Query(0 items)"
        return f"Query({self.count()} items): {items[0]}"


class CollectionAccessor(Generic[T]):
    """Context-aware accessor that provides a scoped .query() interface."""

    def __init__(self, data_provider: Callable[[], Union[Dict[Any, T], List[T]]]) -> None:
        self._data_provider = data_provider

    def _new_query(self) -> Query:
        data = self._data_provider()
        return Query(data)

    def query(self) -> Query:
        """Return a new Query scoped to this collection."""
        return self._new_query()

    def filter(self, expression: str) -> Query:
        """Shortcut to run a filtered query."""
        return self.query().filter(expression)

    def name(self, value: str) -> Optional[QueryResult]:
        """Shortcut to look up by name."""
        return self.query().name(value)

    def all(self) -> Dict[Any, T]:
        """Return all entries (optionally filtered)."""
        return self.query().all()

    def list(self) -> List[T]:
        """Return all entries as a list."""
        return self.query().list()

    def first(self) -> Optional[QueryResult]:
        """Return first matching entry."""
        return self.query().first()

    def count(self) -> int:
        """Count entries for this collection."""
        return self.query().count()

    def display(self) -> str:
        """
        Format all entries for user-friendly display with proper indentation.

        Returns a formatted string with each item separated by blank lines.
        Nested objects are indented and separated with their own sections.
        """
        data = self.all()
        if not data:
            return "(empty collection)"

        lines = []
        for key, item in data.items():
            lines.append(f"--- {key} ---")

            # Format all attributes with proper nesting
            for attr_name, attr_value in get_object_attributes(item):
                if is_nested_object(attr_value):
                    lines.append(f"  {attr_name}:")
                    lines.append(f"    --- {attr_name.replace('_', ' ').title()} ---")
                    nested = format_value(attr_value, indent=4)
                    lines.append(nested)
                else:
                    lines.append(f"  {attr_name}: {attr_value}")

            lines.append("")  # Blank line between items

        return "\n".join(lines)

    def __repr__(self) -> str:
        """Full detailed representation."""
        return self.display()

    def __str__(self) -> str:
        """Full detailed string representation."""
        return self.display()


class FilterableDict(dict):
    """Dict subclass that provides .filter() method for dynamic querying."""

    def filter(self, expression: str = "") -> Query:
        """
        Filter dict values by attribute:value expression.

        Examples:
            data.filter('name:peace')
            data.filter('number:1')
            data.filter('')  # Returns query of all items
        """
        return Query(self).filter(expression) if expression else Query(self)

    def display(self) -> str:
        """
        Format all items in the dict for user-friendly display.

        Returns a formatted string with each item separated by blank lines.
        Nested objects are indented and separated with their own sections.
        """
        if not self:
            return "(empty collection)"

        lines = []
        for key, item in self.items():
            lines.append(f"--- {key} ---")

            # Format all attributes with proper nesting
            for attr_name, attr_value in get_object_attributes(item):
                if is_nested_object(attr_value):
                    lines.append(f"  {attr_name}:")
                    lines.append(f"    --- {attr_name.replace('_', ' ').title()} ---")
                    nested = format_value(attr_value, indent=4)
                    lines.append(nested)
                else:
                    lines.append(f"  {attr_name}: {attr_value}")

            lines.append("")  # Blank line between items

        return "\n".join(lines)


def make_filterable(data: Union[Dict[Any, T], List[T]]) -> Union["FilterableDict", Query]:
    """
    Convert dict or list to a filterable object with .filter() support.

    Examples:
        walls = make_filterable(Cube.wall())
        peace = walls.filter('name:North').first()
    """
    if isinstance(data, dict):
        # Create a FilterableDict from the regular dict
        filterable = FilterableDict(data)
        return filterable
    else:
        # For lists, wrap in a Query
        return Query(data)
