from __future__ import annotations

import warnings
from pathlib import Path

from dynaconf import default_settings
from dynaconf.constants import TOML_EXTENSIONS
from dynaconf.loaders.base import BaseLoader
from dynaconf.utils import object_merge
from dynaconf.vendor import toml  # Backwards compatibility with uiri/toml
from dynaconf.vendor import tomllib  # New tomllib stdlib on py3.11


def load(obj, env=None, silent=True, key=None, filename=None):
    """
    Reads and loads in to "obj" a single key or all keys from source file.

    :param obj: the settings instance
    :param env: settings current env default='development'
    :param silent: if errors should raise
    :param key: if defined load a single key, else load all in env
    :param filename: Optional custom filename to load
    :return: None
    """

    try:
        loader = BaseLoader(
            obj=obj,
            env=env,
            identifier="toml",
            extensions=TOML_EXTENSIONS,
            file_reader=tomllib.load,
            string_reader=tomllib.loads,
            opener_params={"mode": "rb"},
        )
        loader.load(
            filename=filename,
            key=key,
            silent=silent,
        )
    except UnicodeDecodeError:  # pragma: no cover
        """
        NOTE: Compat functions exists to keep backwards compatibility with
        the new tomllib library. The old library was called `toml` and
        the new one is called `tomllib`.

        The old lib uiri/toml allowed unicode characters and re-added files
        as string.

        The new tomllib (stdlib) does not allow unicode characters, only
        utf-8 encoded, and read files as binary.

        NOTE: In dynaconf 4.0.0 we will drop support for the old library
        removing the compat functions and calling directly the new lib.
        """
        loader = BaseLoader(
            obj=obj,
            env=env,
            identifier="toml",
            extensions=TOML_EXTENSIONS,
            file_reader=toml.load,
            string_reader=toml.loads,
        )
        loader.load(
            filename=filename,
            key=key,
            silent=silent,
        )

        warnings.warn(
            "TOML files should have only UTF-8 encoded characters. "
            "starting on 4.0.0 dynaconf will stop allowing invalid chars.",
        )


def write(settings_path, settings_data, merge=True):
    """Write data to a settings file.

    :param settings_path: the filepath
    :param settings_data: a dictionary with data
    :param merge: boolean if existing file should be merged with new data
    """
    settings_path = Path(settings_path)
    if settings_path.exists() and merge:  # pragma: no cover
        try:  # tomllib first
            with open(str(settings_path), "rb") as open_file:
                object_merge(tomllib.load(open_file), settings_data)
        except UnicodeDecodeError:  # pragma: no cover
            # uiri/toml fallback (TBR on 4.0.0)
            with open(
                str(settings_path),
                encoding=default_settings.ENCODING_FOR_DYNACONF,
            ) as open_file:
                object_merge(toml.load(open_file), settings_data)

    try:  # tomllib first
        with open(str(settings_path), "wb") as open_file:
            tomllib.dump(encode_nulls(settings_data), open_file)
    except UnicodeEncodeError:  # pragma: no cover
        # uiri/toml fallback (TBR on 4.0.0)
        with open(
            str(settings_path),
            "w",
            encoding=default_settings.ENCODING_FOR_DYNACONF,
        ) as open_file:
            toml.dump(encode_nulls(settings_data), open_file)

        warnings.warn(
            "TOML files should have only UTF-8 encoded characters. "
            "starting on 4.0.0 dynaconf will stop allowing invalid chars.",
        )


def encode_nulls(data):
    """TOML does not support `None` so this function transforms to '@none '."""
    if data is None:
        return "@none "
    if isinstance(data, dict):
        return {key: encode_nulls(value) for key, value in data.items()}
    elif isinstance(data, (list, tuple)):
        return [encode_nulls(item) for item in data]
    return data
