import os
from dataclasses import MISSING
from pathlib import Path

from ..decorators import cached_class_property
from ..lazy_imports import dotenv
from ..utils.string_conv import to_snake_case


# Type of `os.environ` or `DotEnv` dict
Environ = dict[str, 'str | None']

# noinspection PyTypeChecker
environ = None


# noinspection PyMethodParameters
class Env:

    __slots__ = ()

    _accessed_cleaned_to_env = False

    @classmethod
    def load_environ(cls, force_reload=False):
        """
        Load :attr:`environ` from ``os.environ``.

        If `force_reload` is true, start fresh
        and re-copy `os.environ`.
        """
        global environ

        if (_env_not_setup := environ is None) or force_reload:
            # Copy `os.environ`, so as not to mutate it
            environ = os.environ.copy()

            if not _env_not_setup:
                # Refresh `var_names`, in case env variables
                # were removed (deleted) from `os.environ`
                cls.var_names = set(environ)

                if cls._accessed_cleaned_to_env:
                    cls.cleaned_to_env = {
                        k: v for k, v in cls.cleaned_to_env.items()
                        if v in cls.var_names
                    }

    @cached_class_property
    def var_names(cls):
        """
        Cached mapping of `os.environ` key names. This can be refreshed with
        :meth:`reload` as needed.
        """
        return set(environ) if environ is not None else set()

    @classmethod
    def reload(cls, env=None):
        """Refresh cached environment variable names."""
        env_vars = cls.var_names

        if env is None:
            cls.load_environ(force_reload=True)
            env = environ

        new_vars = set(env) - env_vars

        # update names of environment variables
        env_vars.update(new_vars)

        # update mapping of cleaned environment variables (if needed)
        if cls._accessed_cleaned_to_env:
            cls.cleaned_to_env.update(
                (clean(var), var) for var in new_vars
            )

    @classmethod
    def secret_values(cls, dirs):
        """
        Retrieve the values (environment variables) from secret file(s)
        in a secret directory, or a list/tuple of secret directories.
        """
        if isinstance(dirs, (str, os.PathLike)):
            dirs = [dirs]

        env: Environ = {}

        for d in dirs:
            d: Path = d if isinstance(d, os.PathLike) else Path(d)

            if d.exists():
                if d.is_dir():
                    # Iterate over all files in the directory
                    for f in d.iterdir():
                        if f.is_file():  # Ensure it's a file, not a subdirectory
                            env[f.name] = f.read_text()
                elif d.is_file():
                    raise ValueError(f'Secrets directory `{d!r}` is a file, not a directory.')

        return env

    @classmethod
    def update_with_secret_values(cls, dirs):

        secret_values = cls.secret_values(dirs)

        # reload cached mapping of environment variables
        cls.reload(secret_values)
        # update `environ` with new environment variables
        environ.update(secret_values)

    @classmethod
    def dotenv_values(cls, files):
        """
        Retrieve the values (environment variables) from a dotenv file,
        or a list/tuple of dotenv files.
        """
        if isinstance(files, (str, os.PathLike)):
            files = [files]
        elif files is True:
            files = ['.env']

        env: Environ = {}

        for f in files:
            # iterate backwards (from current directory) to find the
            # dotenv file
            dotenv_path = dotenv.find_dotenv(f)
            # take environment variables from `.env` file
            dotenv_values = dotenv.dotenv_values(dotenv_path)
            env.update(dotenv_values)

        return env

    @classmethod
    def update_with_dotenv(cls, files='.env', dotenv_values=None):

        if dotenv_values is None:
            dotenv_values = cls.dotenv_values(files)

        # reload cached mapping of environment variables
        cls.reload(dotenv_values)
        # update `environ` with new environment variables
        environ.update(dotenv_values)

    # noinspection PyDunderSlots,PyUnresolvedReferences,PyClassVar
    @cached_class_property
    def cleaned_to_env(cls):
        cls._accessed_cleaned_to_env = True
        return {clean(var): var for var in cls.var_names}


def clean(s):
    """
    TODO:
        see https://stackoverflow.com/questions/1276764/stripping-everything-but-alphanumeric-chars-from-a-string-in-python
        also, see if we can refactor to use something like Rust and `pyo3` for a slight performance improvement.
    """
    return s.replace('-', '').replace('_', '').lower()


def try_cleaned(key):
    """
    Return the value of the env variable as a *string* if present in
    the Environment, or `MISSING` otherwise.
    """
    key = Env.cleaned_to_env.get(clean(key))

    if key is not None:
        return environ[key]

    return MISSING


if os.name == 'nt':
    # Where Env Var Names Must Be UPPERCASE
    def lookup_exact(var):
        """
        Lookup by variable name(s) with *exact* letter casing, and return
        `None` if not found in the environment.
        """
        if isinstance(var, str):
            var = var.upper()

            if var in Env.var_names:
                return environ[var]

        else:  # a collection of env variable names.
            for v in var:
                v = v.upper()

                if v in Env.var_names:
                    return environ[v]

        return MISSING

else:
    # Where Env Var Names Can Be Mixed Case
    def lookup_exact(var):
        """
        Lookup by variable name(s) with *exact* letter casing, and return
        `None` if not found in the environment.
        """
        if isinstance(var, str):
            if var in Env.var_names:
                return environ[var]

        else:  # a collection of env variable names.
            for v in var:
                if v in Env.var_names:
                    return environ[v]

        return MISSING


def with_screaming_snake_case(field_name):
    """
    Lookup with `SCREAMING_SNAKE_CASE` letter casing first - this is the
    default lookup.

    This function assumes the dataclass field name is lower-cased.

    For a field named 'my_env_var', this tries the following lookups in order:
        - MY_ENV_VAR (screaming snake-case)
        - my_env_var (snake-case)
        - Any other variations - i.e. MyEnvVar, myEnvVar, myenvvar, my-env-var

    :param field_name: The dataclass field name to lookup in the environment.
    :return: The value of the matched environment variable, if one is found in
      the environment.
    """
    upper_key = field_name.upper()

    if upper_key in Env.var_names:
        return environ[upper_key]

    if field_name in Env.var_names:
        return environ[field_name]

    return try_cleaned(field_name)


def with_snake_case(field_name):
    """Lookup with `snake_case` letter casing first.

    This function assumes the dataclass field name is lower-cased.

    For a field named 'my_env_var', this tries the following lookups in order:
        - my_env_var (snake-case)
        - MY_ENV_VAR (screaming snake-case)
        - Any other variations - i.e. MyEnvVar, myEnvVar, myenvvar, my-env-var

    :param field_name: The dataclass field name to lookup in the environment.
    :return: The value of the matched environment variable, if one is found in
      the environment.
    """
    if field_name in Env.var_names:
        return environ[field_name]

    upper_key = field_name.upper()

    if upper_key in Env.var_names:
        return environ[upper_key]

    return try_cleaned(field_name)


def with_pascal_or_camel_case(field_name):
    """Lookup with `PascalCase` or `camelCase` letter casing first.

    This function assumes the dataclass field name is either pascal- or camel-
    cased.

    For a field named 'myEnvVar', this tries the following lookups in order:
        - myEnvVar, MyEnvVar (camel-case, or pascal-case)
        - MY_ENV_VAR (screaming snake-case)
        - my_env_var (snake-case)
        - Any other variations - i.e. my-env-var, myenvvar

    :param field_name: The dataclass field name to lookup in the environment.
    :return: The value of the matched environment variable, if one is found in
      the environment.
    """
    if field_name in Env.var_names:
        return environ[field_name]

    snake_key = to_snake_case(field_name)
    upper_key = snake_key.upper()

    if upper_key in Env.var_names:
        return environ[upper_key]

    if snake_key in Env.var_names:
        return environ[snake_key]

    return try_cleaned(field_name)
