from copy import copy
import json
import os
import time

from . import read_types


class CachedConfigValue:
    def __init__(self, key, value, cache_ttl):
        self.key = key
        self.value = value
        self.cache_ttl = cache_ttl
        if self.cache_ttl > 0:
            self.cache_end = time.time() + cache_ttl
        else:
            self.cache_end = None

    @property
    def expired(self) -> bool:
        if self.cache_ttl == -1:
            return False
        if self.cache_ttl == 0:
            return True
        return time.time() >= self.cache_end


class Config:
    def __init__(self, base_dir: str, config_file_path: str = 'config.json', gcp_project_id: str = None):
        self._BASE_DIR = base_dir
        self._CONFIG_FILE_PATH = config_file_path
        self._GCP_PROJECT_ID = gcp_project_id

        self._config_file = None
        self._config_file_last_fetch = None
        self._env_vars = None
        self._env_vars_last_fetch = None
        self._secret_manager_secrets = {}

        self._cache = {}

    def _load_config_file(self):
        try:
            with open(os.path.join(self._BASE_DIR, self._CONFIG_FILE_PATH)) as fp:
                self._config_file = json.load(fp)
        except FileNotFoundError:
            self._config_file = {}
        self._config_file_last_fetch = time.time()

    def _load_env_vars(self):
        self._env_vars = copy(os.environ)
        self._env_vars_last_fetch = time.time()

    def _needs_reloading(self, cache_ttl: int, last_fetch: float):
        if cache_ttl == -1:
            return False
        if cache_ttl + time.time() >= last_fetch:
            return True

    def _fetch_config_file_value(self, key: str, cache_ttl: float):
        """Returns a list, dict or str, or None if key not found."""
        if self._config_file is None or self._needs_reloading(cache_ttl, self._config_file_last_fetch) is True:
            self._load_config_file()
        return self._config_file.get(key, None)

    def _fetch_env_vars_value(self, key: str, cache_ttl: float):
        """Returns a str or None if key not found."""
        if self._env_vars is None or self._needs_reloading(cache_ttl, self._env_vars_last_fetch) is True:
            self._load_env_vars()
        return self._env_vars.get(key, None)

    def _cache_value(self, key: str, value, cache_ttl: int):
        if cache_ttl == 0:
            # Do not cache
            return
        self._cache[key] = CachedConfigValue(key, value, cache_ttl)
        return value

    def _check_cache(self, key: str):
        if (cached_value := self._cache.get(key, None)) is not None:
            if cached_value.expired:
                del self._cache[key]
                return
            return cached_value.value

    def get(self, read_function, read_args: dict, *, key: str, default, required: bool, cache_ttl: int):
        if cache_ttl < -1:
            raise ValueError('Cache TTL must be >= -1')
        if value := self._check_cache(key):
            return value
        if value := self._fetch_env_vars_value(key, cache_ttl):
            pass
        elif value := self._fetch_config_file_value(key, cache_ttl):
            pass
        if value is not None:
            value = read_function(value, **read_args)
            self._cache_value(key, value, cache_ttl)
            return value
        is_required = required if callable(required) is False else required()
        if is_required is True:
            raise ValueError(f'Value not provided for required setting {key}')
        return default if callable(default) is False else default()


class Settings:
    def __init__(self, base_dir: str, config_file_path: str = 'config.json', suffix_underscore=False,
                 gcp_project_id: str = None):
        self._config = Config(base_dir, config_file_path, gcp_project_id)
        self._suffix_underscore = suffix_underscore

    def set_gcp_project_id(self, gcp_project_id: str):
        self._config._GCP_PROJECT_ID = gcp_project_id

    def dir(self, global_vars: dict):
        out = []
        for global_variable_name in global_vars.keys():
            if global_variable_name.startswith('__'):
                continue
            if global_variable_name.endswith('__'):
                continue
            if global_variable_name.isupper() is False:
                continue
            if self._suffix_underscore is True:
                if global_variable_name.endswith('_'):
                    if isinstance(global_vars[global_variable_name], ConfigValue) is False:
                        raise ValueError(f'{global_variable_name} follows naming pattern but is not a ConfigValue')
                    global_variable_name = global_variable_name[:-1]
                else:
                    if isinstance(global_vars[global_variable_name], ConfigValue) is True:
                        raise ValueError(f'{global_variable_name} does not follow naming pattern but is a ConfigValue')
            else:
                if global_variable_name.startswith('_'):
                    if isinstance(global_vars[global_variable_name], ConfigValue) is False:
                        raise ValueError(f'{global_variable_name} follows naming pattern but is not a ConfigValue')
                    global_variable_name = global_variable_name[1:]
                else:
                    if isinstance(global_vars[global_variable_name], ConfigValue) is True:
                        raise ValueError(f'{global_variable_name} does not follow naming pattern but is a ConfigValue')
            if global_variable_name in out:
                raise ValueError(f'Variable collision, {global_variable_name} defined twice')
            out.append(global_variable_name)
        return out

    def _generate_original_variable_name(self, name: str) -> str:
        return f'{name}_' if self._suffix_underscore else f'_{name}'

    def getattr(self, name: str, global_vars: dict):
        if name in self.dir(global_vars):
            name = self._generate_original_variable_name(name)
            if name in global_vars:
                if isinstance(var := global_vars[name], ConfigValue):
                    return var.value
        raise AttributeError()

    def cache_static_values(self, global_vars: dict, minimum_cache_ttl: int = -1):
        for name in self.dir(global_vars):
            if name not in global_vars:
                config_value = global_vars[self._generate_original_variable_name(name)]
                cache_ttl = config_value._get_kwargs['cache_ttl']
                if cache_ttl == -1:
                    config_value.value
                    continue
                if minimum_cache_ttl == -1:
                    continue
                if minimum_cache_ttl < 0:
                    raise ValueError('Minimum cache ttl must be at least 0')
                if cache_ttl >= minimum_cache_ttl:
                    config_value.value

    def custom_value(self, get_kwargs: dict, read_function, read_args: dict, value_type):
        return ConfigValue(self._config, read_function, value_type, read_args, get_kwargs)

    def string_value(self, key: str, default=None, *, required=False, cache_ttl: int = -1) -> 'ConfigValue':
        read_args = dict()
        get_kwargs = dict(
            key=key,
            default=default,
            required=required,
            cache_ttl=cache_ttl,
        )
        return self.custom_value(get_kwargs, read_types.read_str, read_args, str)

    def boolean_value(self, key: str, default=None, *, required=False, cache_ttl: int = -1) -> 'ConfigValue':
        read_args = dict()
        get_kwargs = dict(
            key=key,
            default=default,
            required=required,
            cache_ttl=cache_ttl,
        )
        return self.custom_value(get_kwargs, read_types.read_bool, read_args, bool)

    def list_value(self, key: str, default=None, *, required=False, cache_ttl: int = -1,
                   split_char=';') -> 'ConfigValue':
        read_args = dict(
            split_char=split_char,
        )
        get_kwargs = dict(
            key=key,
            default=default,
            required=required,
            cache_ttl=cache_ttl,
        )
        return self.custom_value(get_kwargs, read_types.read_list, read_args, list)


class ConfigValue:
    def __init__(self, config: Config, read_function, value_type, read_args: dict, get_kwargs: dict):
        self._config = config
        self._read_function = read_function
        self.value_type = value_type
        self._read_args = read_args
        self._get_kwargs = get_kwargs

    @property
    def value(self):
        return self._config.get(self._read_function, self._read_args, **self._get_kwargs)

    def __repr__(self):
        return repr(self.value)

    def __str__(self):
        return str(self.value)
