# `envenom` - an elegant application configurator for the more civilized age
# Copyright (C) 2024-  Artur Ciesielski <artur.ciesielski@gmail.com>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

from collections.abc import Callable, Iterable
from dataclasses import Field, dataclass, field
from typing import Any, Type, TypeAlias, TypeVar

from envenom.entries import Entry, EntryWithDefault, OptionalEntry, RequiredEntry
from envenom.vars import DefaultType, ParserType, Var

T = TypeVar("T")

FieldDefinition: TypeAlias = tuple[str, Type[T], Field[T]]

# Why so much <# type: ignore> in this file?
#
# So dataclasses are lying to us.
#
# When the `field` function is called it creates a Field[T] object, but it tells
# us that it really returns T, I swear, pretty please. Because we're exposing
# a similar interface and hooking into this exact layer, we need to:
#   - ignore the return value from `field`; it's a lie anyway
#   - we need to lie to our consumers on the API side just like `field` would
# Except of course we supply three versions of the `field` function, so we need
# to lie on all three fronts.


def __default_parser(v: str) -> str:
    """
    This is technically not strictly needed and could be inlined in Var code.
    But the nice bonus is that it coerces the default return type to `str`
    if the user does not specify any parser type.
    """
    return v


def config(
    namespace: Iterable[str] | str | None = None,
) -> Callable[[Type[T]], Type[T]]:
    def wrapper(cls: Type[T]) -> Type[T]:
        field_var: Var[Any]
        new_fields = {
            field_name: field(
                init=False,
                repr=True,
                hash=True,
                metadata={
                    "type": cls.__annotations__[field_name],
                    "var": (field_var := field_entry.get_var(field_name, namespace)),
                },
                default_factory=field_var.get,
            )
            for field_name, field_entry in cls.__dict__.items()
            if isinstance(field_entry, Entry) and field_name in cls.__annotations__
        }

        for field_name, field_entry in new_fields.items():
            setattr(cls, field_name, field_entry)

        return dataclass(frozen=True, eq=True)(cls)

    return wrapper


def subconfig(factory: Callable[[], T]) -> T:
    return field(default_factory=factory)


def optional(
    parser: ParserType[T] = __default_parser, *, file: bool = True
) -> T | None:
    return OptionalEntry(parser=parser, file=file)  # type: ignore


def with_default(
    parser: ParserType[T] = __default_parser,
    *,
    default: DefaultType[T],
    file: bool = True,
) -> T:
    return EntryWithDefault(
        parser=parser,
        file=file,
        default=default,  # type: ignore
    )


def required(parser: ParserType[T] = __default_parser, *, file: bool = True) -> T:
    return RequiredEntry(parser=parser, file=file)  # type: ignore
