# TODO: MODULE DOCS FR FR

# TODO: should we just use cattrs?
# would kinda expose implementation deets...
# TODO: allow defining a fallback?
# TODO: allow access to conversion flag?
# TODO: flag for disallowing all defaults?
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
import functools
from typing import Any, overload
import warnings

from coolqlite._common import (
    CoolqliteWarning,
    InternalCoolqliteError,
    MissingNamedToSqlFnError,
    MissingTypedToSqlFnError,
    SqliteType,
    ToSqlFnSetupError,
)
from coolqlite._util import _identity_fn
from coolqlite.to_sql.datetime_ import (
    any_temporal_as_iso,
    aware_as_zulu_string,
    aware_as_keeptz_string,
    naive_as_string,
)

type ToSqliteConversionFn[Src = Any] = Callable[[Src], SqliteType]

_KEYWORDS = {"DANGER"}


class UuidAs(Enum):
    """How do you want to serialize a UUID?

    Right now only strings are offered, this may be expanded.
    """

    STRING = "string"
    # TODO: idk right now
    # BYTES = "bytes"


@dataclass(kw_only=True, frozen=True)
class ToSqlConfig:
    """Configure the ToSqlFnRegistry."""

    configure_datetime_fns: bool = True
    "Add named functions to convert various datetime types to sql."


def _builtins_from_cfg(config: ToSqlConfig) -> _BuiltinFns:
    builtins = _BuiltinFns()
    if config.configure_datetime_fns:
        builtins["z"] = aware_as_zulu_string
        builtins["keeptz"] = aware_as_keeptz_string
        builtins["naive"] = naive_as_string
        builtins["ANY_temporal"] = any_temporal_as_iso
    return builtins


class ToSqlFnRegistry:
    """
    Dispatch to serializers based on name or type.

    Register type-based dispatch as you would a :func:`functools.singledispatch`:

    .. code-block:: python

       registry = ToSqlFnRegistry()
       @registry.register
       def _handle_int(x: int) -> str:
           return str(x)

    Or register by name:

    .. code-block:: python

       registry = ToSqlFnRegistry()

       @registry.add_named("reversed-int")
       def _reversed_int(x: int) -> str:
           return str(x).reverse()

       registry.add_named("upper", str.upper)

    """

    def __init__(self, *, config: ToSqlConfig = ToSqlConfig()):
        builtins = _builtins_from_cfg(config)
        self._named = _NamedFnRegistry(builtins)

        @functools.singledispatch
        def by_type(x: object) -> Any:
            raise MissingTypedToSqlFnError(
                f"No type-dispatched adapter was registered for {type(x)}"
            )

        self._by_type = by_type
        self.register = self._by_type.register

        # base types
        # WHAT DO YOU MEAN IT DOESN'T MATCH MYPY WHAT ARE YOU SMOKING
        by_type.register(SqliteType, _identity_fn)  # type: ignore[call-overload]

    # TODO: don't quite like having named and typed both get and run,
    # but it's helpful for figuring out where the error is from.
    # Something _feels_ off though.
    # TODO: especially because singledispatch doesn't let us know until it's too late!!!
    # cattrs defaults to identity for unstructure so I guess they don't handle it specially either.
    # structure, they know.
    # but wait, maybe that's configurable?

    def _get_named(self, name: str) -> ToSqliteConversionFn:
        if not name:
            return _identity_fn
        try:
            return self._named[name]
        except KeyError as e:
            raise MissingNamedToSqlFnError(name) from e

    def _run_named(self, name: str, arg: Any) -> SqliteType:
        return self._get_named(name)(arg)

    def _get_typed(self, type_: type) -> ToSqliteConversionFn:
        return self._by_type.dispatch(type_)

    def _run_typed(self, value: Any) -> Any:
        return self._by_type(value)

    # TODO: more specific error type?
    def _set_named[F: ToSqliteConversionFn](
        self, key: str, f: F, warning_stacklevel: int
    ) -> F:
        if not key:
            raise ToSqlFnSetupError("need an adapter name")
        if not key.isidentifier():
            err = ToSqlFnSetupError(f"must be a valid python identifier, {key} is not.")
            err.add_note("this may be relaxed in the future.")
            raise err
        if key in _KEYWORDS:
            raise ToSqlFnSetupError(f"{key} is a built-in keyword")
        self._named.set(key, f, warning_stacklevel)
        return f

    # TODO: test warning stack level is correct
    @overload
    def add_named[F: ToSqliteConversionFn](self, name: str) -> Callable[[F], F]: ...
    @overload
    def add_named(self, name: str, fn: ToSqliteConversionFn) -> None: ...
    def add_named[F: ToSqliteConversionFn](
        self, name: str, fn: F | None = None
    ) -> Callable[[F], F] | None:
        """
        Add a named converter. Usable as a decorator.
        """
        if fn is None:
            return lambda fn: self._set_named(name, fn, 4)
        else:
            self._set_named(name, fn, 4)
            return None


class _BuiltinFns(dict[str, ToSqliteConversionFn]):
    def __setitem__(self, key: str, value: ToSqliteConversionFn):
        if key in self:  # pragma: no cover
            raise InternalCoolqliteError(
                "Attempting to register a builtin with the same name."
            )
        super().__setitem__(key, value)


class _NamedFnRegistry:
    def __init__(self, builtins: _BuiltinFns):
        self.builtins = builtins
        self.user: dict[str, ToSqliteConversionFn] = {}

    # def __contains__(self, key: object) -> bool:
    #     return key in self.user or key in self.builtins

    def __getitem__(
        self, key: str
    ) -> Callable[[Any], int | float | str | bytes | None]:
        if key in self.user:
            return self.user[key]
        if key in self.builtins:
            return self.builtins[key]
        raise KeyError(key)

    def set(self, key: str, value: ToSqliteConversionFn, warning_stacklevel: int):
        if key in self.user:
            warnings.warn(
                f"You are shadowing an already-set user function {key}",
                CoolqliteWarning,
                stacklevel=warning_stacklevel,
            )
        elif key in self.builtins:
            warnings.warn(
                f"You are shadowing a built-in / configured function {key}",
                CoolqliteWarning,
                stacklevel=warning_stacklevel,
            )
        self.user[key] = value


__all__ = ["ToSqlConfig", "ToSqlFnRegistry", "UuidAs"]
