# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: BSD-2-Clause
"""Reduce a complex Python structure to basic types."""

from __future__ import annotations

import dataclasses
import enum
import ipaddress
import logging
import numbers
import pathlib
import re
import tempfile
import types
import typing
import uuid


if typing.TYPE_CHECKING:
    from collections.abc import Callable, Iterable, Iterator
    from typing import Any, Final


BASIC_TYPES: Final[tuple[type, ...]] = (float, int, str, types.NoneType)
"""The types that can usually be serialized directly."""

LIST_TYPES: Final[tuple[type[Iterable[Any]], ...]] = (list, tuple)
"""The types that we represent as a list."""

SORTED_LIST_TYPES: Final[tuple[type[Iterable[Any]], ...]] = (set,)
"""The types that we represent as a list, but we sort it."""

STR_TYPES: Final[tuple[type, ...]] = (
    enum.Enum,
    ipaddress.IPv4Address,
    ipaddress.IPv6Address,
    numbers.Number,
    pathlib.Path,
    uuid.UUID,
)
"""The types that we stringify."""

AS_DICT_HANDLERS: Final[dict[type[Any], Callable[[Any], Any]]] = {
    logging.Logger: lambda log: f'Logger("{log.name}", {logging.getLevelName(log.level)})',
    re.Pattern: lambda pattern: pattern.pattern,
    tempfile._TemporaryFileWrapper: lambda tempf: tempf.path,  # noqa: SLF001  # well, yeah
}
"""The types that we handle in special ways before anything else."""


@dataclasses.dataclass(frozen=True)
class Overrides:
    """Override some of the default handlers."""

    as_dict_handlers: dict[type[Any], Callable[[Any], Any]] = dataclasses.field(
        default_factory=lambda: AS_DICT_HANDLERS,
    )
    """The types that we handle in special ways before anything else."""

    basic_types: tuple[type, ...] = dataclasses.field(default_factory=lambda: BASIC_TYPES)
    """The types that can usually be serialized directly."""

    list_types: tuple[type[Iterable[Any]], ...] = dataclasses.field(
        default_factory=lambda: LIST_TYPES,
    )
    """The types that we represent as a list."""

    sorted_list_types: tuple[type[Iterable[Any]], ...] = dataclasses.field(
        default_factory=lambda: SORTED_LIST_TYPES,
    )
    """The types that we represent as a list, but we sort it."""

    str_types: tuple[type[Any], ...] = dataclasses.field(default_factory=lambda: STR_TYPES)
    """The types that we stringify."""


def as_dict_rec(  # noqa: C901,PLR0911  # we handle many cases
    obj: Any,  # noqa: ANN401  # it could really be anything
    *,
    skip_null: bool = False,
    overrides: Overrides | None = None,
) -> Any:  # noqa: ANN401  # ...and we can turn it into anything
    """Reduce a complex Python structure, e.g. a dataclass, to basic types.

    Useful for e.g. serializing to JSON, TOML, etc.
    """

    def recurse(value: Any) -> Any:  # noqa: ANN401  # it could still be anything
        """Recurse with the same parameters."""
        return as_dict_rec(value, skip_null=skip_null, overrides=overrides)

    def nonnull(values: Iterable[Any]) -> Iterator[Any]:
        """Only return null values if they are not specifically forbidden."""
        for value in values:
            if value is not None or not skip_null:
                yield value

    if overrides is None:
        overrides = Overrides()

    for base_type, handler in overrides.as_dict_handlers.items():
        if isinstance(obj, base_type):
            return handler(obj)

    if isinstance(obj, overrides.basic_types):
        return obj

    if isinstance(obj, dict):
        return {
            name: recurse(value)
            for name, value in obj.items()
            if value is not None or not skip_null
        }

    if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
        return recurse(dataclasses.asdict(obj))

    if isinstance(obj, overrides.list_types):
        return [recurse(value) for value in nonnull(obj)]

    if isinstance(obj, overrides.sorted_list_types):
        return recurse(sorted(nonnull(obj)))

    if isinstance(obj, overrides.str_types):
        return str(obj)

    # This is a weird fallback, but it works for some cases.
    obj_dict = getattr(obj, "__dict__", None)
    if obj_dict is not None:
        if obj_dict is dict:
            return recurse(obj_dict)

        return recurse(dict(obj_dict))

    raise NotImplementedError(repr(obj))
