"""Field accessor and literal delegate."""
from __future__ import annotations

from typing import Callable, NoReturn

import math

from cqla.exprs import Expr, ContextExpr
from cqla.exprs.mixins import AccessorMixin, AggMixin


def _traverse(path: str, record: dict | object) -> object | NoReturn:
    """
    Tries a variety of strategies to walk through nested structures
    and ultimately return a value from the record.
    """

    def switch(key: str, record: object) -> object | None:
        return (
            record.get(key)
            if isinstance(record, dict)
            else getattr(record, key, None)
        )

    stop = switch(path, record)
    if stop is not None:
        # i.e. the given key (with all it's ".") is the name of the field
        return stop

    paths = path.split('.')
    for i, walk in enumerate(paths):
        stop = switch(walk, record if i == 0 else stop)

        if i + 1 != len(paths) and stop is None:
            # i.e. we haven't traversed all the paths but switch returned None
            raise KeyError(
                f'"{walk}" not found at level {i} in {record.__class__}; '
                f'searched using entire "{path}" too'
            )
    return stop


def _wrap(value):
    """Wrap a plain value as LitDelegate if it's not already callable."""
    if callable(value):
        return value
    return LitDelegate(value)


class FieldDelegate(AccessorMixin, AggMixin):
    """Field accessor - extracts a field value from a record."""

    def __init__(self, name: str) -> None:
        self._name = name

    def __repr__(self) -> str:
        return f"field({self._name!r})"

    def __call__(self, record: object) -> object:
        if self._name == '*':
            if isinstance(record, dict):
                return dict(record)
            return vars(record)
        return _traverse(self._name, record)

    def apply(self, fn: Callable) -> Expr:
        """Apply a function to the field value."""
        return Expr(lambda r: fn(self(r)), f"{self!r}.apply(<fn>)")

    # Row-level predicates
    def is_between(self, lower, upper, closed: str = "both") -> Expr:
        """Check if value is between lower and upper bounds.

        Args:
            lower: Lower bound.
            upper: Upper bound.
            closed: Which bounds are inclusive: 'both', 'left', 'right', 'none'.
        """
        if closed == "both":
            return Expr(lambda r: lower <= self(r) <= upper, f"{self!r}.is_between({lower!r}, {upper!r})")
        elif closed == "left":
            return Expr(lambda r: lower <= self(r) < upper, f"{self!r}.is_between({lower!r}, {upper!r}, closed='left')")
        elif closed == "right":
            return Expr(lambda r: lower < self(r) <= upper, f"{self!r}.is_between({lower!r}, {upper!r}, closed='right')")
        elif closed == "none":
            return Expr(lambda r: lower < self(r) < upper, f"{self!r}.is_between({lower!r}, {upper!r}, closed='none')")
        else:
            raise ValueError(f"closed must be 'both', 'left', 'right', or 'none', got {closed!r}")

    def is_in(self, values) -> Expr:
        """Check if value is in the given collection."""
        values_set = set(values) if not isinstance(values, (set, frozenset)) else values
        return Expr(lambda r: self(r) in values_set, f"{self!r}.is_in({values!r})")

    def is_null(self) -> Expr:
        """Check if value is None."""
        return Expr(lambda r: self(r) is None, f"{self!r}.is_null()")

    def is_not_null(self) -> Expr:
        """Check if value is not None."""
        return Expr(lambda r: self(r) is not None, f"{self!r}.is_not_null()")

    def is_nan(self) -> Expr:
        """Check if value is NaN."""
        return Expr(lambda r: isinstance(self(r), float) and math.isnan(self(r)), f"{self!r}.is_nan()")

    def is_not_nan(self) -> Expr:
        """Check if value is not NaN."""
        return Expr(lambda r: not (isinstance(self(r), float) and math.isnan(self(r))), f"{self!r}.is_not_nan()")

    def is_finite(self) -> Expr:
        """Check if numeric value is finite."""
        return Expr(lambda r: isinstance(self(r), (int, float)) and math.isfinite(self(r)), f"{self!r}.is_finite()")

    def is_infinite(self) -> Expr:
        """Check if numeric value is infinite."""
        return Expr(lambda r: isinstance(self(r), float) and math.isinf(self(r)), f"{self!r}.is_infinite()")

    def is_close(self, other, *, rtol: float = 1e-5, atol: float = 1e-8) -> Expr:
        """Check if value is close to another value within tolerance."""
        return Expr(
            lambda r: math.isclose(self(r), other, rel_tol=rtol, abs_tol=atol),
            f"{self!r}.is_close({other!r}, rtol={rtol}, atol={atol})"
        )

    # Comparison operators
    def __eq__(self, other: Callable) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) == other(r), f'({self!r} == {other!r})')

    def __ne__(self, other: Callable) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) != other(r), f'({self!r} != {other!r})')

    def __gt__(self, other: Callable) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) > other(r), f'({self!r} > {other!r})')

    def __ge__(self, other: Callable) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) >= other(r), f'({self!r} >= {other!r})')

    def __lt__(self, other: Callable) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) < other(r), f'({self!r} < {other!r})')

    def __le__(self, other: Callable) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) <= other(r), f'({self!r} <= {other!r})')


class LitDelegate:
    """Literal value wrapper."""

    def __init__(self, value: object) -> None:
        self._value = value

    def __repr__(self) -> str:
        return repr(self._value)

    def __call__(self, record: object) -> object:
        return self._value

    def __eq__(self, other) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) == other(r), f'({self!r} == {other!r})')

    def __ne__(self, other) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) != other(r), f'({self!r} != {other!r})')

    def __gt__(self, other) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) > other(r), f'({self!r} > {other!r})')

    def __ge__(self, other) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) >= other(r), f'({self!r} >= {other!r})')

    def __lt__(self, other) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) < other(r), f'({self!r} < {other!r})')

    def __le__(self, other) -> Expr:
        other = _wrap(other)
        return Expr(lambda r: self(r) <= other(r), f'({self!r} <= {other!r})')


def field(value) -> FieldDelegate:
    """Create a field expression. Passes through if already an expression."""
    if isinstance(value, str):
        return FieldDelegate(value)
    return value


def lit(value: object) -> LitDelegate:
    return LitDelegate(value)
