from collections.abc import Mapping
from dataclasses import MISSING
from dataclasses import Field as _Field
from typing import NewType

from ._type_def import ExplicitNull
from .constants import PY310_OR_ABOVE, PY314_OR_ABOVE
from .utils._object_path import split_object_path

# Define a simple type (alias) for the `CatchAll` field
#
# The `type` statement is introduced in Python 3.12
# Ref: https://docs.python.org/3.12/reference/simple_stmts.html#type
#
# TODO: uncomment following usage of `type` statement
#   once we drop support for Python 3.9 - 3.11
# if PY312_OR_ABOVE:
#     type CatchAll = Mapping
CatchAll = NewType('CatchAll', Mapping)


def _normalize_alias_path_args(all_paths, load, dump):
    """Normalize `AliasPath` arguments and canonicalize path values."""
    if load is not None:
        all_paths = load
        load = None
        dump = ExplicitNull

    elif dump is not None:
        all_paths = dump
        dump = None
        load = ExplicitNull

    if isinstance(all_paths, str):
        all_paths = (split_object_path(all_paths),)
    else:
        all_paths = tuple([
            split_object_path(a) if isinstance(a, str) else a
            for a in all_paths
        ])

    return all_paths, load, dump


def _normalize_alias_args(default, default_factory, all_aliases, load, dump, env):
    """Normalize `Alias` arguments and canonicalize alias values."""

    if default is not MISSING and default_factory is not MISSING:
        raise ValueError('cannot specify both default and default_factory')

    if all_aliases:
        load = dump = all_aliases

    elif load is not None and isinstance(load, str):
        load = (load,)

    elif env is not None:
        if isinstance(env, str):
            env = (env,)
        elif env is True:
            env = load

    return all_aliases, load, dump, env


# Instances of Field are only ever created from within this module,
# and only from the field() function, although Field instances are
# exposed externally as (conceptually) read-only objects.
#
# name and type are filled in after the fact, not in __init__.
# They're not known at the time this class is instantiated, but it's
# convenient if they're available later.

# noinspection PyPep8Naming,PyShadowingBuiltins
def Env(*load,
        default=MISSING,
        default_factory=MISSING,
        init=True, repr=True,
        hash=None, compare=True, metadata=None,
        **field_kwargs):

    # noinspection PyTypeChecker
    return Alias(
        env=load,
        default=default,
        default_factory=default_factory,
        init=init,
        repr=repr,
        hash=hash,
        compare=compare,
        metadata=metadata,
        **field_kwargs,
    )

# In Python 3.14, dataclasses adds a new parameter to the :class:`Field`
# constructor: `doc`
#
# Ref: https://docs.python.org/3.14/library/dataclasses.html#dataclasses.field
if PY314_OR_ABOVE:
    # noinspection PyPep8Naming,PyShadowingBuiltins
    def Alias(
        *all,
        load=None,
        dump=None,
        env=None,
        skip=False,
        default=MISSING,
        default_factory=MISSING,
        init=True,
        repr=True,
        hash=None,
        compare=True,
        metadata=None,
        kw_only=MISSING,
        doc=None,
    ):

        all, load, dump, env = _normalize_alias_args(default, default_factory, all, load, dump, env)

        return Field(
            load,
            dump,
            env,
            skip,
            None,
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
            kw_only,
            doc,
        )

    # noinspection PyPep8Naming,PyShadowingBuiltins
    def AliasPath(
        *all,
        load=None,
        dump=None,
        skip=False,
        default=MISSING,
        default_factory=MISSING,
        init=True,
        repr=True,
        hash=None,
        compare=True,
        metadata=None,
        kw_only=MISSING,
        doc=None,
    ):
        all, load, dump = _normalize_alias_path_args(all, load, dump)

        return Field(
            load,
            dump,
            load,
            skip,
            all,
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
            kw_only,
            doc,
        )

    # noinspection PyShadowingBuiltins
    def skip_if_field(
        condition,
        *,
        default=MISSING,
        default_factory=MISSING,
        init=True,
        repr=True,
        hash=None,
        compare=True,
        metadata=None,
        kw_only=MISSING,
        doc=None,
    ):

        if default is not MISSING and default_factory is not MISSING:
            raise ValueError("cannot specify both default and default_factory")

        if metadata is None:
            metadata = {}

        metadata["__skip_if__"] = condition

        return _Field(
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
            kw_only,
            doc,
        )

    class Field(_Field):

        __slots__ = ("load_alias", "dump_alias", "env_vars", "skip", "path")

        # noinspection PyShadowingBuiltins
        def __init__(
            self,
            load_alias,
            dump_alias,
            env_vars,
            skip,
            path,
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
            kw_only,
            doc=None,
        ):

            # noinspection PyArgumentList
            super().__init__(
                default,
                default_factory,
                init,
                repr,
                hash,
                compare,
                metadata,
                kw_only,
                doc,
            )

            self.load_alias = load_alias
            self.dump_alias = dump_alias
            self.env_vars = env_vars
            self.skip = skip
            self.path = path


# In Python 3.10, dataclasses adds a new parameter to the :class:`Field`
# constructor: `kw_only`
#
# Ref: https://docs.python.org/3.10/library/dataclasses.html#dataclasses.dataclass
elif PY310_OR_ABOVE:  # pragma: no cover

    # noinspection PyPep8Naming,PyShadowingBuiltins
    def Alias(*all,
              load=None,
              dump=None,
              env=None,
              skip=False,
              default=MISSING,
              default_factory=MISSING,
              init=True, repr=True,
              hash=None, compare=True,
              metadata=None, kw_only=MISSING):

        all, load, dump, env = _normalize_alias_args(default, default_factory, all, load, dump, env)

        return Field(
            load,
            dump,
            env,
            skip,
            None,
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
            kw_only,
        )

    # noinspection PyPep8Naming,PyShadowingBuiltins
    def AliasPath(*all,
                  load=None,
                  dump=None,
                  skip=False,
                  default=MISSING,
                  default_factory=MISSING,
                  init=True, repr=True,
                  hash=None, compare=True,
                  metadata=None, kw_only=MISSING):
        all, load, dump = _normalize_alias_path_args(all, load, dump)

        return Field(
            load,
            dump,
            load,
            skip,
            all,
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
            kw_only,
        )

    # noinspection PyShadowingBuiltins
    def skip_if_field(
        condition,
        *,
        default=MISSING,
        default_factory=MISSING,
        init=True,
        repr=True,
        hash=None,
        compare=True,
        metadata=None,
        kw_only=MISSING
    ):

        if default is not MISSING and default_factory is not MISSING:
            raise ValueError("cannot specify both default and default_factory")

        if metadata is None:
            metadata = {}

        metadata["__skip_if__"] = condition

        return _Field(
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
            kw_only,
        )

    class Field(_Field):

        __slots__ = ('load_alias',
                     'dump_alias',
                     'env_vars',
                     'skip',
                     'path')

        # noinspection PyShadowingBuiltins
        def __init__(self,
                     load_alias, dump_alias, env_vars, skip, path,
                     default, default_factory, init, repr, hash, compare,
                     metadata, kw_only):

            super().__init__(default, default_factory, init, repr, hash,
                             compare, metadata, kw_only)

            if path is not None:
                if isinstance(path, str):
                    path = split_object_path(path) if path else (path, )

            self.load_alias = load_alias
            self.dump_alias = dump_alias
            self.env_vars = env_vars
            self.skip = skip
            self.path = path

else:  # pragma: no cover
    # noinspection PyPep8Naming,PyShadowingBuiltins
    def Alias(*all,
              load=None,
              dump=None,
              env=None,
              skip=False,
              default=MISSING,
              default_factory=MISSING,
              init=True, repr=True,
              hash=None, compare=True, metadata=None):

        all, load, dump, env = _normalize_alias_args(default, default_factory, all, load, dump, env)

        return Field(
            load,
            dump,
            env,
            skip,
            None,
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
        )

    # noinspection PyPep8Naming,PyShadowingBuiltins
    def AliasPath(*all,
                  load=None,
                  dump=None,
                  skip=False,
                  default=MISSING,
                  default_factory=MISSING,
                  init=True, repr=True,
                  hash=None, compare=True,
                  metadata=None):
        all, load, dump = _normalize_alias_path_args(all, load, dump)

        return Field(
            load,
            dump,
            load,
            skip,
            all,
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
        )

    # noinspection PyShadowingBuiltins
    def skip_if_field(
        condition,
        *,
        default=MISSING,
        default_factory=MISSING,
        init=True,
        repr=True,
        hash=None,
        compare=True,
        metadata=None
    ):

        if default is not MISSING and default_factory is not MISSING:
            raise ValueError("cannot specify both default and default_factory")

        if metadata is None:
            metadata = {}

        metadata["__skip_if__"] = condition

        # noinspection PyArgumentList
        return _Field(
            default,
            default_factory,
            init,
            repr,
            hash,
            compare,
            metadata,
        )

    class Field(_Field):

        __slots__ = ('load_alias',
                     'dump_alias',
                     'env_vars',
                     'skip',
                     'path')

        # noinspection PyArgumentList,PyShadowingBuiltins
        def __init__(self,
                     load_alias, dump_alias, env_vars, skip, path,
                     default, default_factory, init, repr, hash, compare,
                     metadata):

            super().__init__(default, default_factory, init, repr, hash,
                             compare, metadata)

            if path is not None:
                if isinstance(path, str):
                    path = split_object_path(path) if path else (path,)

            self.load_alias = load_alias
            self.dump_alias = dump_alias
            self.env_vars = env_vars
            self.skip = skip
            self.path = path


Alias.__doc__ = """
    Maps one or more JSON key names to a dataclass field.

    This function acts as an alias for ``dataclasses.field(...)``, with additional
    support for associating a field with one or more JSON keys. It customizes
    serialization and deserialization behavior, including handling keys with
    varying cases or alternative names.

    The mapping is case-sensitive; JSON keys must match exactly (e.g., ``myField``
    will not match ``myfield``). If multiple keys are provided, the first one
    is used as the default for serialization.

    :param all: One or more JSON key names to associate with the dataclass field.
    :type all: str
    :param load: Key(s) to use for deserialization. Defaults to ``all`` if not specified.
    :type load: str | Sequence[str] | None
    :param dump: Key to use for serialization. Defaults to the first key in ``all``.
    :type dump: str | None
    :param skip: If ``True``, the field is excluded during serialization. Defaults to ``False``.
    :type skip: bool
    :param default: Default value for the field. Cannot be used with ``default_factory``.
    :type default: Any
    :param default_factory: Callable to generate the default value. Cannot be used with ``default``.
    :type default_factory: Callable[[], Any]
    :param init: Whether the field is included in the generated ``__init__`` method. Defaults to ``True``.
    :type init: bool
    :param repr: Whether the field appears in the ``__repr__`` output. Defaults to ``True``.
    :type repr: bool
    :param hash: Whether the field is included in the ``__hash__`` method. Defaults to ``None``.
    :type hash: bool
    :param compare: Whether the field is included in comparison methods. Defaults to ``True``.
    :type compare: bool
    :param metadata: Additional metadata for the field. Defaults to ``None``.
    :type metadata: dict
    :param kw_only: If ``True``, the field is keyword-only. Defaults to ``False``.
    :type kw_only: bool
    :return: A dataclass field with additional mappings to one or more JSON keys.
    :rtype: Field

    **Examples**

    **Example 1** -- Mapping multiple key names to a field::

        from dataclasses import dataclass

        from dataclass_wizard import Alias, LoadMeta, fromdict

        @dataclass
        class Example:
            my_field: str = Alias('key1', 'key2', default="default_value")

        print(fromdict(Example, {'key2': 'a value!'}))
        #> Example(my_field='a value!')

    **Example 2** -- Skipping a field during serialization::

        from dataclasses import dataclass

        from dataclass_wizard import Alias, JSONWizard

        @dataclass
        class Example(JSONWizard):
            my_field: str = Alias('key', skip=True)

        ex = Example.from_dict({'key': 'some value'})
        print(ex)                  #> Example(my_field='a value!')
        assert ex.to_dict() == {}  #> True
"""

AliasPath.__doc__ = """
    Creates a dataclass field mapped to one or more nested JSON paths.

    This function acts as an alias for ``dataclasses.field(...)``, with additional
    functionality to associate a field with one or more nested JSON paths,
    including complex or deeply nested structures.

    The mapping is case-sensitive, meaning that JSON keys must match exactly
    (e.g., "myField" will not match "myfield"). Nested paths can include dot
    notations or bracketed syntax for accessing specific indices or keys.

    :param all: One or more nested JSON paths to associate with
        the dataclass field (e.g., ``a.b.c`` or ``a["nested"]["key"]``).
    :type all: PathType | str
    :param load: Path(s) to use for deserialization. Defaults to ``all`` if not specified.
    :type load: PathType | str | None
    :param dump: Path(s) to use for serialization. Defaults to ``all`` if not specified.
    :type dump: PathType | str | None
    :param skip: If True, the field is excluded during serialization. Defaults to False.
    :type skip: bool
    :param default: Default value for the field. Cannot be used with ``default_factory``.
    :type default: Any
    :param default_factory: A callable to generate the default value. Cannot be used with ``default``.
    :type default_factory: Callable[[], Any]
    :param init: Whether the field is included in the generated ``__init__`` method. Defaults to True.
    :type init: bool
    :param repr: Whether the field appears in the ``__repr__`` output. Defaults to True.
    :type repr: bool
    :param hash: Whether the field is included in the ``__hash__`` method. Defaults to None.
    :type hash: bool
    :param compare: Whether the field is included in comparison methods. Defaults to True.
    :type compare: bool
    :param metadata: Additional metadata for the field. Defaults to None.
    :type metadata: dict
    :param kw_only: If True, the field is keyword-only. Defaults to False.
    :type kw_only: bool
    :return: A dataclass field with additional mapping to one or more nested JSON paths.
    :rtype: Field

    **Examples**

    **Example 1** -- Mapping multiple nested paths to a field::

        from dataclasses import dataclass

        from dataclass_wizard import AliasPath, fromdict

        @dataclass
        class Example:
            my_str: str = AliasPath('a.b.c.1', 'x.y["-1"].z', default="default_value")

        # Maps nested paths ('a', 'b', 'c', 1) and ('x', 'y', '-1', 'z')
        # to the `my_str` attribute. '-1' is treated as a literal string key,
        # not an index, for the second path.

        print(fromdict(Example, {'x': {'y': {'-1': {'z': 'some_value'}}}}))
        #> Example(my_str='some_value')

    **Example 2** -- Using Annotated::

        from dataclasses import dataclass
        from typing import Annotated

        from dataclass_wizard import AliasPath, JSONWizard

        @dataclass
        class Example(JSONWizard):
            my_str: Annotated[str, AliasPath('my."7".nested.path.-321')]


        ex = Example.from_dict({'my': {'7': {'nested': {'path': {-321: 'Test'}}}}})
        print(ex)  #> Example(my_str='Test')
"""

Field.__doc__ = """
    Alias to a :class:`dataclasses.Field`, but one which also represents a
    mapping of one or more JSON key names to a dataclass field.

    See the docs on the :func:`Alias` and :func:`AliasPath` for more info.
"""
