"""Filter and sort translation utilities for search backends.

Provides a Django-native dict/list API for filters and sorts that
backends can translate into their engine-specific format.

Django-native filter dict::

    {"city": "Madrid", "is_active": True, "price": 150}
    {"category": ["equipment", "accessories"]}  # IN filter

Django-native sort list::

    ["-created_at", "name"]  # - prefix = descending

Meilisearch filter string::

    "city = 'Madrid' AND is_active = true AND price = 150"
    "category IN ['equipment', 'accessories']"

Meilisearch sort list::

    ["created_at:desc", "name:asc"]
"""

from __future__ import annotations

import math
from typing import Any

# Mean Earth radius in metres (WGS-84 approximation).
_EARTH_RADIUS_M: float = 6_371_000.0

# Operator suffix mapping for Django-style range lookups.
_RANGE_OPERATORS: dict[str, str] = {
    "__gte": ">=",
    "__gt": ">",
    "__lte": "<=",
    "__lt": "<",
}


def translate_filter_to_meilisearch(filters: dict[str, Any] | str) -> str:
    """Convert a Django-native filter dict to a Meilisearch filter string.

    Passes through strings unchanged (for backwards compatibility).

    Args:
        filters: Either a dict of field:value pairs or a raw Meilisearch
            filter string.

    Returns:
        A Meilisearch-compatible filter string.
    """
    if isinstance(filters, str):
        return filters

    if not isinstance(filters, dict) or not filters:
        return ""

    parts = []
    for field, value in filters.items():
        # Check for range lookup suffixes (e.g. price__gte, created_at__lt).
        range_op: str | None = None
        real_field = field
        for suffix, op in _RANGE_OPERATORS.items():
            if field.endswith(suffix):
                real_field = field[: -len(suffix)]
                range_op = op
                break

        if range_op is not None:
            if isinstance(value, (int, float)) and not isinstance(value, bool):
                parts.append(f"{real_field} {range_op} {value}")
            # Non-numeric range values are silently skipped.
        elif isinstance(value, bool):
            parts.append(f"{field} = {str(value).lower()}")
        elif isinstance(value, (int, float)):
            parts.append(f"{field} = {value}")
        elif isinstance(value, list):
            formatted = ", ".join(f"'{v}'" if isinstance(v, str) else str(v) for v in value)
            parts.append(f"{field} IN [{formatted}]")
        elif value is None:
            parts.append(f"{field} IS NULL")
        else:
            parts.append(f"{field} = '{value}'")

    return " AND ".join(parts)


def translate_sort_to_meilisearch(sort: list[str] | str) -> list[str]:
    """Convert a Django-native sort list to Meilisearch sort format.

    Passes through strings and already-formatted lists unchanged.

    Args:
        sort: Either a list of field names (with optional - prefix for
            descending) or a Meilisearch-formatted sort list.

    Returns:
        A list of Meilisearch-compatible sort strings.
    """
    if isinstance(sort, str):
        return [sort] if sort else []

    if not isinstance(sort, list) or not sort:
        return []

    result = []
    for field in sort:
        if ":" in field:
            # Already in Meilisearch format (e.g. "price:desc")
            result.append(field)
        elif field.startswith("-"):
            result.append(f"{field[1:]}:desc")
        else:
            result.append(f"{field}:asc")

    return result


def apply_filters_to_documents(
    documents: list[dict[str, Any]],
    filters: dict[str, Any] | str,
) -> list[dict[str, Any]]:
    """Apply Django-native filters to an in-memory document list.

    Used by DummyBackend and PostgresBackend for filtering without an
    external engine.

    Args:
        documents: List of document dicts.
        filters: Filter dict or string.

    Returns:
        Filtered list of documents.
    """
    if isinstance(filters, str) or not filters:
        return documents

    result = []
    for doc in documents:
        if _matches_filters(doc, filters):
            result.append(doc)
    return result


def _matches_filters(doc: dict[str, Any], filters: dict[str, Any]) -> bool:
    """Check if a document matches all filter conditions."""
    for field, value in filters.items():
        # Check for range lookup suffixes (e.g. price__gte, created_at__lt).
        range_op: str | None = None
        real_field = field
        for suffix, op in _RANGE_OPERATORS.items():
            if field.endswith(suffix):
                real_field = field[: -len(suffix)]
                range_op = op
                break

        if range_op is not None:
            # Range comparisons only apply to numeric values.
            if not isinstance(value, (int, float)) or isinstance(value, bool):
                continue
            doc_value = doc.get(real_field)
            try:
                doc_num = float(doc_value)  # type: ignore[arg-type]
            except (TypeError, ValueError):
                return False
            if range_op == ">=" and not (doc_num >= value):
                return False
            elif range_op == ">" and not (doc_num > value):
                return False
            elif range_op == "<=" and not (doc_num <= value):
                return False
            elif range_op == "<" and not (doc_num < value):
                return False
            continue

        doc_value = doc.get(field)

        if isinstance(value, list):
            # IN filter
            if doc_value not in value and str(doc_value) not in [str(v) for v in value]:
                return False
        elif isinstance(value, bool):
            if bool(doc_value) != value:
                return False
        elif isinstance(value, (int, float)):
            try:
                if float(doc_value) != float(value):
                    return False
            except (TypeError, ValueError):
                return False
        elif value is None:
            if doc_value is not None:
                return False
        else:
            if str(doc_value) != str(value):
                return False

    return True


def apply_sort_to_documents(
    documents: list[dict[str, Any]],
    sort: list[str],
) -> list[dict[str, Any]]:
    """Apply Django-native sort to an in-memory document list.

    Args:
        documents: List of document dicts.
        sort: List of field names with optional - prefix.

    Returns:
        Sorted list of documents.
    """
    if not sort:
        return documents

    # Build sort keys in reverse order (last sort field has lowest priority)
    result = list(documents)
    for field in reversed(sort):
        if field.startswith("-"):
            field_name = field[1:]
            reverse = True
        elif ":" in field:
            # Meilisearch format
            parts = field.split(":")
            field_name = parts[0]
            reverse = parts[1] == "desc"
        else:
            field_name = field
            reverse = False

        result.sort(key=lambda doc, f=field_name: _sort_key(doc.get(f)), reverse=reverse)

    return result


def _sort_key(value: Any) -> tuple:
    """Generate a sort key that handles mixed types and None values."""
    if value is None:
        return (1, "")  # None values sort last
    if isinstance(value, bool):
        return (0, int(value))
    if isinstance(value, (int, float)):
        return (0, value)
    return (0, str(value))


def _haversine_distance(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
    """Calculate the great-circle distance between two points in metres.

    Uses the Haversine formula with a spherical Earth model (mean radius
    6,371 km). Accurate to within ~0.5 % for most practical distances.

    Args:
        lat1: Latitude of the first point in decimal degrees.
        lng1: Longitude of the first point in decimal degrees.
        lat2: Latitude of the second point in decimal degrees.
        lng2: Longitude of the second point in decimal degrees.

    Returns:
        Distance in metres.
    """
    phi1 = math.radians(lat1)
    phi2 = math.radians(lat2)
    d_phi = math.radians(lat2 - lat1)
    d_lambda = math.radians(lng2 - lng1)

    a = math.sin(d_phi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(d_lambda / 2) ** 2
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))

    return _EARTH_RADIUS_M * c
