from __future__ import annotations

import datetime as dt
import enum
from dataclasses import MISSING, dataclass, field
from operator import attrgetter
from re import search
from typing import TYPE_CHECKING, Any, TypeVar, cast

from typed_settings import default_loaders
from typed_settings import load_settings as _load_settings
from typed_settings.cli_click import ClickHandler
from typed_settings.cli_click import click_options as _click_options
from typed_settings.cli_utils import (
    Default,
    StrDict,
    TypeArgsMaker,
    TypeHandler,
    TypeHandlerFunc,
)
from typed_settings.constants import CLICK_METADATA_KEY, METADATA_KEY
from typed_settings.converters import TSConverter
from typed_settings.types import AUTO, _Auto
from typing_extensions import override

import utilities.click
from utilities.click import Date, LocalDateTime, Time, Timedelta
from utilities.git import get_repo_root_or_cwd_sub_path
from utilities.pathlib import ensure_path
from utilities.types import PathLike, ensure_class

if TYPE_CHECKING:
    from collections.abc import Callable, Iterable, Mapping
    from pathlib import Path

    from click import ParamType
    from typed_settings.loaders import Loader

_T = TypeVar("_T")


def _config_toml(root: Path, /) -> Path | None:
    return path if (path := ensure_path(root, "config.toml")).exists() else None


_CONFIG_FILES = [
    p for p in [get_repo_root_or_cwd_sub_path(_config_toml)] if p is not None
]


class _ExtendedTSConverter(TSConverter):
    """An extension of the TSConverter for custom types."""

    def __init__(
        self,
        *,
        resolve_paths: bool = True,
        strlist_sep: str | Callable[[str], list] | None = ":",
    ) -> None:
        super().__init__(resolve_paths=resolve_paths, strlist_sep=strlist_sep)
        cases: list[tuple[type[Any], Callable[..., Any]]] = []
        try:
            from utilities.whenever import ensure_date, ensure_time, ensure_timedelta
        except ModuleNotFoundError:  # pragma: no cover
            pass
        else:
            cases.extend([
                (dt.date, ensure_date),
                (dt.time, ensure_time),
                (dt.timedelta, ensure_timedelta),
            ])
        try:
            from sqlalchemy import Engine

            from utilities.sqlalchemy import ensure_engine
        except ModuleNotFoundError:  # pragma: no cover
            pass
        else:
            cases.append((Engine, ensure_engine))

        extras = {cls: _pair_type_and_converter(cls, func) for cls, func in cases}
        self.scalar_converters |= extras


def _pair_type_and_converter(
    cls: type[Any], func: Callable[[Any], Any], /
) -> Callable[[Any, type[Any]], Any]:
    def hook(value: Any, _: type[Any] = type, /) -> Any:
        return func(ensure_class(value, (cls, str)))

    return hook


def _get_loaders(
    *,
    appname: str = "appname",
    config_files: Iterable[PathLike] = _CONFIG_FILES,
    config_file_section: str | _Auto = AUTO,
    config_files_var: None | str | _Auto = AUTO,
    env_prefix: None | str | _Auto = AUTO,
) -> list[Loader]:
    if search("_", appname):
        raise _GetLoadersError(appname=appname)
    return default_loaders(
        appname,
        config_files=config_files,
        config_file_section=config_file_section,
        config_files_var=config_files_var,
        env_prefix=env_prefix,
    )


@dataclass(kw_only=True)
class _GetLoadersError(Exception):
    appname: str

    @override
    def __str__(self) -> str:
        return f"App name {self.appname!r} must not contain underscores"


def load_settings(
    cls: type[_T],
    /,
    *,
    appname: str = "appname",
    config_files: Iterable[PathLike] = _CONFIG_FILES,
    config_file_section: str | _Auto = AUTO,
    config_files_var: None | str | _Auto = AUTO,
    env_prefix: None | str | _Auto = AUTO,
) -> _T:
    """Load a settings object with the extended converter."""
    loaders = _get_loaders(
        appname=appname,
        config_files=config_files,
        config_file_section=config_file_section,
        config_files_var=config_files_var,
        env_prefix=env_prefix,
    )
    converter = _ExtendedTSConverter()
    return _load_settings(cast(Any, cls), loaders, converter=converter)


def click_options(
    cls: type[Any],
    /,
    *,
    appname: str = "appname",
    config_files: Iterable[PathLike] = _CONFIG_FILES,
    argname: str | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """Generate click options with the extended converter."""
    loaders = _get_loaders(appname=appname, config_files=config_files)
    converter = _ExtendedTSConverter()
    type_args_maker = TypeArgsMaker(cast(TypeHandler, _make_click_handler()))
    return _click_options(
        cls,
        loaders,
        converter=converter,
        type_args_maker=type_args_maker,
        argname=argname,
    )


def _make_click_handler() -> ClickHandler:
    """Make the click handler."""
    cases: list[tuple[type[Any], type[ParamType], Callable[[Any], str]]] = [
        (enum.Enum, utilities.click.Enum, attrgetter("name"))
    ]
    try:
        from utilities.whenever import (
            serialize_date,
            serialize_local_datetime,
            serialize_time,
            serialize_timedelta,
        )
    except ModuleNotFoundError:  # pragma: no cover
        pass
    else:
        cases.extend([
            (dt.datetime, LocalDateTime, serialize_local_datetime),
            (dt.date, Date, serialize_date),
            (dt.time, Time, serialize_time),
            (dt.timedelta, Timedelta, serialize_timedelta),
        ])
    try:
        import sqlalchemy

        from utilities.sqlalchemy import serialize_engine
    except ModuleNotFoundError:  # pragma: no cover
        pass
    else:
        cases.append((sqlalchemy.Engine, utilities.click.Engine, serialize_engine))
    extra_types = {
        cls: _make_type_handler_func(cls, param, serialize)
        for cls, param, serialize in cases
    }
    return ClickHandler(extra_types=extra_types)


def _make_type_handler_func(
    cls: type[Any], param: type[ParamType], serialize: Callable[[Any], str], /
) -> TypeHandlerFunc:
    """Make the type handler for a given type/parameter."""

    def handler(
        type_: type[Any],
        default: Default,
        is_optional: bool,  # noqa: FBT001
        /,
    ) -> StrDict:
        args = (type_,) if issubclass(type_, enum.Enum) else ()
        mapping: StrDict = {"type": param(*args)}
        if isinstance(default, cls):  # pragma: no cover
            mapping["default"] = serialize(default)
        elif is_optional:  # pragma: no cover
            mapping["default"] = None
        return mapping

    return cast(Any, handler)


def click_field(
    *,
    default: Any = MISSING,
    init: bool = True,
    repr: bool = True,  # noqa: A002
    hash: bool | None = None,  # noqa: A002
    compare: bool = True,
    metadata: Mapping[str, Any] | None = None,
    kw_only: Any = MISSING,
    help: str | None = None,  # noqa: A002
    click: Mapping[str, Any] | None = None,
    param_decls: tuple[str, ...] | None = None,
) -> Any:
    """Create a click field."""
    click_use = ({} if click is None else dict(click)) | (
        {} if param_decls is None else {"param_decls": param_decls}
    )
    metadata_use = _get_metadata(metadata=metadata, help_=help, click=click_use)
    return field(
        default=default,
        init=init,
        repr=repr,
        hash=hash,
        compare=compare,
        metadata=metadata_use,
        kw_only=kw_only,
    )


def _get_metadata(
    *,
    metadata: Mapping[str, Any] | None = None,
    help_: str | None = None,
    click: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
    # copied from typed_settings.cls_attrs, which we cannot import
    metadata_use = {} if metadata is None else dict(metadata)
    ts_meta = metadata_use.setdefault(METADATA_KEY, {})
    ts_meta["help"] = help_
    ts_meta[CLICK_METADATA_KEY] = {"help": help_} | (
        {} if click is None else dict(click)
    )
    return metadata_use


__all__ = ["click_field", "click_options", "load_settings"]
