import operator
from dataclasses import dataclass
from datetime import date
from functools import reduce, wraps
from typing import Any, Callable, Generator, Literal, TypeVar
from urllib.parse import urlencode

from django.db.models import Q
from django.http import HttpRequest
from django.shortcuts import redirect


def safe_division(numerator: int | float, denominator: int | float) -> int | float:
    """
    Divides without triggering division by zero exception.
    Returns 0 if denominator is 0.
    """
    try:
        return numerator / denominator
    except ZeroDivisionError:
        return 0


def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> object:
    """
    Safely get the nested attribute from an object.

    Parameters:
    obj (object): The object from which to retrieve the attribute.
    attr_chain (str): The chain of attributes, separated by dots.
    default: The default value to return if any attribute in the chain does not exist.

    Returns:
    The value of the nested attribute if it exists, otherwise the default value.
    """
    attrs = attr_chain.split(".")
    for attr in attrs:
        try:
            obj = getattr(obj, attr)
        except AttributeError:
            return default
    return obj


def truncate_(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
    return (
        (f"{input_string[: length - len(ellipsis)].rstrip()}{ellipsis}")
        if len(input_string) > length
        else input_string
    )


def truncate(
    input_string: str, length: int = 30, ellipsis: str = "…", endpart: str = ""
) -> str:
    max_content_length = length - len(endpart)
    if max_content_length < 0:
        raise ValueError("Length cannot be shorter than the length of endpart.")

    if len(input_string) > max_content_length:
        return f"{input_string[: max_content_length - len(ellipsis)].rstrip()}{ellipsis}{endpart}"

    return (
        f"{input_string}{endpart}"
        if len(input_string) + len(endpart) <= length
        else f"{input_string[: length - len(ellipsis) - len(endpart)].rstrip()}{ellipsis}{endpart}"
    )


T = TypeVar("T", str, int, date)


def generate_split_ranges(
    value_list: list[T], split_points: list[T]
) -> Generator[tuple[T, T], None, None]:
    for x in range(0, len(split_points) + 1):
        if x == 0:
            start = 0
        elif x >= len(split_points):
            start = value_list.index(split_points[x - 1]) + 1
        else:
            start = value_list.index(split_points[x - 1]) + 1
        try:
            end = value_list.index(split_points[x])
        except IndexError:
            end = len(value_list)
        yield (value_list[start], value_list[end - 1])


def format_float_or_int(number: int | float):
    return int(number) if float(number).is_integer() else f"{number:03.2f}"


OperatorType = Literal["|", "&"]


@dataclass
class FilterEntry:
    condition: Q
    operator: OperatorType = "&"


def build_dynamic_filter(
    filters: list[FilterEntry | Q], default_operator: OperatorType = "&"
):
    """
    Constructs a Django Q filter from a list of filter conditions.

    Args:
        filters (list): A list where each item is either:
            - A Q object (default AND logic applied)
            - A tuple of (Q object, operator) where operator is "|" (OR) or "&" (AND)

    Returns:
        Q: A combined Q object that can be passed to Django's filter().
    """
    op_map: dict[OperatorType, Callable[[Q, Q], Q]] = {
        "|": operator.or_,
        "&": operator.and_,
    }

    # Convert all plain Q objects into (Q, "&") for default AND behavior
    processed_filters = [
        FilterEntry(f, default_operator) if isinstance(f, Q) else f for f in filters
    ]

    # Reduce with dynamic operators
    return reduce(
        lambda combined_filters, filter: op_map[filter.operator](
            combined_filters, filter.condition
        ),
        processed_filters,
        Q(),
    )


def redirect_to(default_view: str, *default_args):
    """
    A decorator that redirects the user back to the referring page or a default view if no 'next' parameter is provided.

    :param default_view: The name of the default view to redirect to if 'next' is missing.
    :param default_args: Any arguments required for the default view.
    """

    def decorator(view_func):
        @wraps(view_func)
        def wrapped_view(request: HttpRequest, *args, **kwargs):
            next_url = request.GET.get("next")
            if not next_url:
                from django.urls import (
                    reverse,  # Import inside function to avoid circular imports
                )

                next_url = reverse(default_view, args=default_args)

            response = view_func(
                request, *args, **kwargs
            )  # Execute the original view logic
            return redirect(next_url)

        return wrapped_view

    return decorator


def add_next_param_to_url(url: str, nexturl: str) -> str:
    return f"{url}?{urlencode({'next': nexturl})}"