from __future__ import annotations

from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Callable, ClassVar, Literal

from ._decorators import cached_class_property
from .constants import TAG

if TYPE_CHECKING:  # pragma: no cover
    from datetime import tzinfo

    from ._bases import TypeToHook
    from ._bases_meta import PreDecoder
    from ._path_util import EnvFilePaths, SecretsDirs
    from ._type_def import META, FrozenKeys
    from .conditions import Condition
    from .enums import (
        DateTimeTo,
        EnvKeyStrategy,
        EnvPrecedence,
        KeyAction,
        KeyCase,
    )


class ABCOrAndMeta(type):
    """
    Metaclass to add class-level :meth:`__or__` and :meth:`__and__` methods
    to a base class of type :type:`M`.

    Ref:
      - https://stackoverflow.com/q/15008807/10237506
      - https://stackoverflow.com/a/57351066/10237506
    """

    def __or__(cls: META, other: META) -> META:
        """
        Merge two Meta configs. Priority will be given to the source config
        present in `cls`, e.g. the first operand in the '|' expression.

        Use case: Merge the Meta configs for two separate dataclasses into a
        single, unified Meta config.
        """
        src = cls
        src_dict = src.__dict__
        other_dict = other.__dict__

        base_dict = {'__slots__': ()}

        # Set meta attributes here.
        if src is AbstractMeta or src is AbstractEnvMeta:
            # Here we can't use `src` because the `bind_to` method isn't
            # defined on the abstract class. Use `other` instead, which
            # *will* be a concrete subclass of `AbstractMeta`.
            src = other
            # noinspection PyTypeChecker
            for k in src.fields_to_merge:
                if k in other_dict:
                    base_dict[k] = other_dict[k]
        else:
            # noinspection PyTypeChecker
            for k in src.fields_to_merge:
                if k in src_dict:
                    base_dict[k] = src_dict[k]
                elif k in other_dict:
                    base_dict[k] = other_dict[k]

        # This mapping won't be updated. Use the src by default.
        for k in src.__special_attrs__:
            if k in src_dict:
                base_dict[k] = src_dict[k]

        new_cls_name = src.__name__
        # Check if the type of the class we want to create is
        # `JSONWizard.Meta` or a subclass. If so, we want to avoid the
        # mandatory `__init_subclass__` call that gets invoked when creating
        # a new class, so use the superclass type instead.
        if src.__is_inner_meta__:
            # In a reversed MRO, the inheritance tree looks like this:
            #   |___ object -> BaseMeta
            #               -> AbstractMeta
            #               -> BaseJSONWizardMeta -> ...
            # So here, we want to choose the third-to-last class in the list.
            # noinspection PyUnresolvedReferences
            src = src.__mro__[-4]

        # noinspection PyTypeChecker
        return type(new_cls_name, (src, ), base_dict)

    def __and__(cls: META, other: META) -> META:
        """
        Merge the `other` Meta config into the first one, i.e. `cls`. This
        operation does not create a new class, but instead it modifies the
        source config `cls` in-place; the source will be the first operand in
        the '&' expression.

        Use case: Merge a separate Meta config (for a single dataclass) with
        the first config.
        """
        other_dict = other.__dict__

        # Set meta attributes here.
        # noinspection PyTypeChecker
        for k in cls.all_fields:
            if k in other_dict:
                setattr(cls, k, other_dict[k])

        return cls


class BaseMeta(metaclass=ABCOrAndMeta):
    """
    Base (shared) Meta definition.
    """
    __slots__ = ()

    # A list of class attributes that are exclusive to the Meta config.
    # When merging two Meta configs for a class, these are the only
    # attributes which will *not* be merged.
    __special_attrs__ = frozenset({
        'recursive',
        # 'debug',
        'field_to_alias',
        'field_to_alias_dump',
        'field_to_alias_load',
        'tag',
    })

    # Class attribute which enables us to detect a `EnvWizard.Meta` subclass.
    __is_inner_meta__ = False

    # When enabled, a specified Meta config for the main dataclass (i.e. the
    # class on which `from_dict` and `to_dict` is called) will cascade down
    # and be merged with the Meta config for each *nested* dataclass; note
    # that during a merge, priority is given to the Meta config specified on
    # each class.
    #
    # The default behavior is True, so the Meta config (if provided) will
    # apply in a recursive manner.
    recursive: ClassVar[bool] = True

    # The field name that identifies the tag for a class.
    #
    # When set to a value, an :attr:`TAG` field will be populated in the
    # dictionary object in the dump (serialization) process. When loading
    # (or de-serializing) a dictionary object, the :attr:`TAG` field will be
    # used to load the corresponding dataclass, assuming the dataclass field
    # is properly annotated as a Union type, ex.:
    #   my_data: Union[Data1, Data2, Data3]
    tag: ClassVar[str | None] = None

    # The dictionary key that identifies the tag field for a class. This is
    # only set when the `tag` field or the `auto_assign_tags` flag is enabled
    # in the `Meta` config for a dataclass.
    #
    # Defaults to '__tag__' if not specified.
    tag_key: ClassVar[str] = TAG

    # Auto-assign the class name as a dictionary "tag" key, for any dataclass
    # fields which are in a `Union` declaration, ex.:
    #   my_data: Union[Data1, Data2, Data3]
    auto_assign_tags: ClassVar[bool] = False

    # Determines whether we should we skip / omit fields with default values
    # (based on the `default` or `default_factory` argument specified for
    # the :func:`dataclasses.field`) in the serialization process.
    skip_defaults: ClassVar[bool] = False

    # Determines the :class:`Condition` to skip / omit dataclass
    # fields in the serialization process.
    skip_if: ClassVar[Condition | None] = None

    # Determines the condition to skip / omit fields with default values
    # (based on the `default` or `default_factory` argument specified for
    # the :func:`dataclasses.field`) in the serialization process.
    skip_defaults_if: ClassVar[Condition | None] = None

    # Enable Debug mode for more verbose log output.
    #
    # This setting can be a `bool`, `int`, or `str`:
    # - `True` enables debug mode with default verbosity.
    # - A `str` or `int` specifies the minimum log level (e.g., 'DEBUG', 10).
    #
    # Debug mode provides additional helpful log messages, including:
    # - Logging unknown JSON keys encountered during `from_dict` or `from_json`.
    # - Detailed error messages for invalid types during unmarshalling.
    #
    # Note: Enabling Debug mode may have a minor performance impact.
    debug: ClassVar[bool | int | str] = False

    # Custom load hooks for extending type support.
    #
    # Mapping: {Type -> hook}
    #
    # A hook must accept either:
    #   - one positional argument (runtime hook): value -> object
    #   - two positional arguments (codegen
    #     hook): (TypeInfo, Extras) -> str | TypeInfo
    #
    # The hook is invoked when loading a value annotated with the given type.
    type_to_load_hook: ClassVar[TypeToHook | None] = None

    # Custom dump hooks for extending type support.
    #
    # Mapping: {Type -> hook}
    #
    # A hook must accept either:
    #   - one positional argument (runtime hook): object -> JSON-serializable
    #     value
    #   - two positional arguments (codegen
    #     hook): (TypeInfo, Extras) -> str | TypeInfo
    #
    # The hook is invoked when dumping a value whose runtime type matches
    # the given type.
    type_to_dump_hook: ClassVar[TypeToHook | None] = None

    # ``pre_decoder``: Optional hook called before type loading.
    # Receives the container type plus (cls, TypeInfo, Extras) and may
    # return a transformed ``TypeInfo`` (e.g., wrapped in a function
    # which decodes JSON/delimited strings into list/dict for env
    # loading). Returning the input value leaves behavior unchanged.
    #
    #  Pre-decoder signature:
    #   (cls, container_tp, tp, extras) -> new_tp
    pre_decoder: ClassVar[PreDecoder] = None

    # Specifies the letter case used for JSON keys during serialization.
    #
    # This setting determines how dataclass field names are transformed
    # when generating keys in the output JSON object.
    #
    # By default, field names are emitted in `snake_case`.
    #
    # The setting is case-insensitive and supports shorthand assignment,
    # such as using the string 'P' instead of 'PASCAL'.
    #
    # If unset, this value defaults to `case` when provided.
    dump_case: ClassVar[KeyCase | str | None] = None

    # A custom mapping of dataclass fields to their JSON aliases (keys) used
    # during serialization only.
    #
    # Values may be a single alias string or a sequence of alias strings.
    # When a sequence is provided, the first alias is used as the output key.
    #
    # When set, this mapping overrides `field_to_alias` for dump behavior
    # only.
    field_to_alias_dump: ClassVar[
        Mapping[str, str | Sequence[str]] | None
    ] = None

    # Unsafe: Enables parsing of dataclasses in unions without requiring
    # the presence of a `tag_key`, i.e., a dictionary key identifying the
    # tag field in the input. Defaults to False.
    unsafe_parse_dataclass_in_union: ClassVar[bool] = False

    # Specifies how :class:`datetime` (and :class:`time`, where applicable)
    # objects are serialized during output.
    #
    # This setting controls how temporal values are emitted when converting
    # a dataclass to a Python dictionary (`to_dict`) or a JSON string
    # (`to_json`). It applies to serialization only and does not affect
    # deserialization.
    #
    # By default, values are serialized using ISO 8601 string format.
    #
    # Supported values are defined by :class:`DateTimeTo`.
    dump_date_time_as: ClassVar[DateTimeTo | str | None] = None

    # Specifies the timezone to assume for naive :class:`datetime` values
    # during serialization.
    #
    # By default, naive datetimes are rejected to avoid ambiguous or
    # environment-dependent behavior.
    #
    # When set, naive datetimes are interpreted as being in the specified
    # timezone before conversion to a UTC epoch timestamp.
    #
    # Common usage:
    #     assume_naive_datetime_tz = timezone.utc
    #
    # This setting applies to serialization only and does not affect
    # deserialization.
    assume_naive_datetime_tz: ClassVar[tzinfo | None] = None

    # Controls how `typing.NamedTuple` and `collections.namedtuple`
    # fields are loaded and serialized.
    #
    # - False (DEFAULT): load from list/tuple and serialize
    #                     as a positional list.
    # - True: load from mapping and serialize as a dict
    #           keyed by field name.
    #
    # In strict mode, inputs that do not match the selected mode
    # raise TypeError.
    #
    # Note:
    #   This option enforces strict shape matching for performance reasons.
    namedtuple_as_dict: ClassVar[bool | None] = None

    # If True (default: False), ``None`` is coerced to an empty
    # string (``""``)
    # when loading ``str`` fields.
    #
    # When False, ``None`` is coerced using ``str(value)``, so ``None``
    # becomes the literal string ``'None'`` for ``str`` fields.
    #
    # For ``Optional[str]`` fields, ``None`` is preserved by default.
    coerce_none_to_empty_str: ClassVar[bool | None] = None

    # Controls how leaf (non-recursive) types are detected during
    # serialization.
    #
    # - "exact" (DEFAULT): only exact built-in leaf types are treated
    #   as leaf values.
    # - "issubclass": subclasses of leaf types are also treated
    #   as leaf values.
    #
    # Leaf types are returned without recursive traversal. Bytes are still
    # handled separately according to their serialization rules.
    #
    # Note:
    #     The default "exact" mode avoids treating third-party scalar-like
    #     objects (e.g. NumPy scalars) as built-in leaf types.
    leaf_handling: ClassVar[Literal['exact', 'issubclass'] | None] = None

    # noinspection PyMethodParameters
    @cached_class_property
    def all_fields(cls: type) -> FrozenKeys:
        """Return a list of all class attributes"""
        keys = {}
        for base in reversed(cls.__mro__[:-1]):  # drop object, then reverse
            keys.update(base.__annotations__)
        return frozenset(keys)

    # noinspection PyMethodParameters
    @cached_class_property
    def fields_to_merge(cls) -> FrozenKeys:
        """Return a list of class attributes, minus `__special_attrs__`"""
        return cls.all_fields - cls.__special_attrs__


class AbstractMeta(BaseMeta):
    """
    Base class definition for the `JSONWizard.Meta` inner class.
    """
    __slots__ = ()

    # A list of class attributes that are exclusive to the Meta config.
    # When merging two Meta configs for a class, these are the only
    # attributes which will *not* be merged.
    __special_attrs__ = frozenset({
        'recursive',
        # 'debug',
        'field_to_alias',
        'field_to_alias_dump',
        'field_to_alias_load',
        'tag',
    })

    # Class attribute which enables us to detect a `JSONWizard.Meta` subclass.
    __is_inner_meta__ = False

    # Specifies the letter case to use for JSON keys when both loading and
    # dumping.
    #
    # This is a convenience setting that applies the same key casing rule to
    # both deserialization (load) and serialization (dump).
    #
    # If set, it is used as the default for both `load_case` and
    # `dump_case`, unless either is explicitly specified.
    #
    # The setting is case-insensitive and supports shorthand assignment,
    # such as using the string 'C' instead of 'CAMEL'.
    case: ClassVar[KeyCase | str | None] = None

    # Specifies the letter case used to match JSON keys when mapping them
    # to dataclass fields during deserialization.
    #
    # This setting determines how dataclass field names are transformed
    # when looking up corresponding keys in the input JSON object. It does
    # not affect keys in `TypedDict` or `NamedTuple` subclasses.
    #
    # By default, JSON keys are assumed to be in `snake_case`, and fields
    # are matched directly without transformation.
    #
    # The setting is case-insensitive and supports shorthand assignment,
    # such as using the string 'C' instead of 'CAMEL'.
    #
    # If set to `A` or `AUTO`, all supported key casing transforms are
    # attempted at runtime, and the resolved transform is cached for
    # subsequent lookups.
    #
    # If unset, this value defaults to `case` when provided.
    load_case: ClassVar[KeyCase | str | None] = None

    # A custom mapping of dataclass fields to their JSON aliases (keys).
    #
    # Values may be a single alias string or a sequence of alias strings.
    #
    # - During deserialization (load), any listed alias for a field is
    #   accepted.
    # - During serialization (dump), the first alias is used by default.
    #
    # This mapping overrides default key casing and implicit field-to-key
    # transformations (e.g., "my_field" → "myField") for the affected fields.
    #
    # This setting applies to both load and dump unless explicitly overridden
    # by `field_to_alias_load` or `field_to_alias_dump`.
    field_to_alias: ClassVar[Mapping[str, str | Sequence[str]] | None] = None

    # A custom mapping of dataclass fields to their JSON aliases (keys) used
    # during deserialization only.
    #
    # Values may be a single alias string or a sequence of alias strings.
    # Any listed alias is accepted when mapping input JSON keys to
    # dataclass fields.
    #
    # When set, this mapping overrides `field_to_alias` for load behavior
    # only.
    field_to_alias_load: ClassVar[
        Mapping[str, str | Sequence[str]] | None] = None

    # Defines the action to take when an unknown JSON key is encountered
    # during `from_dict` or `from_json` calls. An unknown key is one that
    # does not map to any dataclass field.
    #
    # Valid options are:
    # - `"ignore"` (default): Silently ignore unknown keys.
    # - `"warn"`: Log a warning for each unknown key. Requires `debug`
    #   to be `True` and properly configured logging.
    # - `"raise"`: Raise an `UnknownKeyError` for the first unknown key
    #   encountered.
    on_unknown_key: ClassVar[KeyAction | None] = None

    @classmethod
    def bind_to(cls, dataclass: type, create=True, is_default=True):
        """
        Initialize hook which applies the Meta config to `dataclass`, which is
        typically a subclass of :class:`JSONWizard`.

        :param dataclass: A class which has been decorated by the `@dataclass`
          decorator; typically this is a sub-class of :class:`JSONWizard`.
        :param create: When true, a separate loader/dumper will be created
          for the class. If disabled, this will access the root loader/dumper,
          so modifying this should affect global settings across all
          dataclasses that use the JSON load/dump process.
        :param is_default: When enabled, the Meta will be cached as the
          default Meta config for the dataclass. Defaults to true.

        """
        raise NotImplementedError


class AbstractEnvMeta(BaseMeta):
    """
    Base class definition for the `EnvWizard.Meta` inner class.
    """
    __slots__ = ()

    # A list of class attributes that are exclusive to the Meta config.
    # When merging two Meta configs for a class, these are the only
    # attributes which will *not* be merged.
    __special_attrs__ = frozenset({
        'recursive',
        'debug',
        'field_to_env_load',
        'field_to_alias_dump',
        'tag',
    })

    # Class attribute which enables us to detect a `EnvWizard.Meta` subclass.
    __is_inner_meta__ = False

    # `True` to load environment variables from an `.env` file, or a
    # list/tuple of dotenv files.
    #
    # This can also be set to a path to a custom dotenv file, for example:
    #   `path/to/.env.prod`
    #
    # Simply passing in a filename such as `.env.prod` will search the current
    # directory, as well as any parent folders (working backwards to the root
    # directory), until it locates the given file.
    #
    # If multiple files are passed in, later files in the list/tuple will take
    # priority over earlier files.
    #
    # For example, in below the '.env.last' file takes priority over '.env':
    #   env_file = '.env', '.env.last'
    env_file: ClassVar[EnvFilePaths] = None

    # Prefix for all environment variables. Defaults to `None`.
    env_prefix: ClassVar[str | None] = None

    # secrets_dir: The secret files directory or a sequence of directories.
    # Defaults to `None`.
    secrets_dir: ClassVar[SecretsDirs] = None

    # The key lookup strategy to use for Env Var Names.
    #
    # The default strategy is `SCREAMING_SNAKE_CASE` > `snake_case`.
    load_case: ClassVar[EnvKeyStrategy | str | None] = None

    # Environment Precedence (order) to search for values
    # Defaults to EnvPrecedence.SECRETS_ENV_DOTENV
    env_precedence: ClassVar[EnvPrecedence | None] = None

    # A custom mapping of dataclass fields to their env vars (keys) used
    # during deserialization only.
    #
    # Values may be a single alias string or a sequence of alias strings.
    # Any listed alias is accepted when mapping input env vars to
    # dataclass fields.
    field_to_env_load: ClassVar[
        Mapping[str, str | Sequence[str]] | None] = None

    # Defines the action to take when an unknown JSON key is encountered
    # during `from_dict` or `from_json` calls. An unknown key is one
    # that does not map to any dataclass field.
    #
    # Valid options are:
    # - `"ignore"` (default): Silently ignore unknown keys.
    # - `"warn"`: Log a warning for each unknown key. Requires `debug`
    #   to be `True` and properly configured logging.
    # - `"raise"`: Raise an `UnknownKeyError` for the first unknown key
    #   encountered.
    # on_unknown_key: ClassVar[KeyAction] = None

    @classmethod
    def bind_to(cls, env_class: type, create=True, is_default=True):
        """
        Initialize hook which applies the Meta config to `env_class`,
        which is typically a subclass of :class:`EnvWizard`.

        :param env_class: A sub-class of :class:`EnvWizard`.
        :param create: When true, a separate loader/dumper will be created
          for the class. If disabled, this will access the root loader/dumper,
          so modifying this should affect global settings across all
          dataclasses that use the JSON load/dump process.
        :param is_default: When enabled, the Meta will be cached as the
          default Meta config for the dataclass. Defaults to true.

        """
        raise NotImplementedError


class _BaseHookRegistry:
    __slots__ = ()
    __HOOKS__: ClassVar[dict[type, Callable]]

    @classmethod
    def register_hook(cls, typ: type, func: Callable):
        cls.__HOOKS__[typ] = func

    @classmethod
    def get_hook(cls, typ: type) -> Callable | None:
        return cls.__HOOKS__.get(typ)


class BaseLoadHook(_BaseHookRegistry):
    """
    Container class for load type hooks.
    """


class BaseDumpHook(_BaseHookRegistry):
    """
    Container class for dump type hooks.
    """
