import tomllib
import json
import re
from base64 import b64decode, b64encode
from os import environ
from pathlib import Path, PurePath
from threading import Lock
import tomli_w
from typing import Any, TypeVar, TypeAlias

# Method-level generic for config value types.
# Use `Config[T]` in method signatures when you want a specific value type.
T = TypeVar('T')
# modern TypeAlias (Python 3.10+)
Config: TypeAlias = dict[str, T]


class ConfigObjectError(Exception):
    def __init__(self):
        super(ConfigObjectError,self).__init__("Please instantiate this class with the 'get_instance(toml_file:str)' class method")

class Glean_config():

    _config_object: 'Glean_config | None' = None
    _object_lock = Lock()
    _file_lock = Lock()
    # internal storage can hold heterogeneous values loaded from TOML
    config: Config[Any] = {}

    def __init__(self, toml_file: str | None = None, override: bool = False):
        self.modified = True
        self.fileless_mode = False
        
        # Determine the file path
        if toml_file is None:
            # Check environment variable
            toml_file = environ.get('glean_config_file')
            if toml_file is None:
                # Fileless mode
                self.fileless_mode = True
                self.toml_file = None
            else:
                self.toml_file = toml_file
        else:
            self.toml_file = toml_file

        if not override:
            raise ConfigObjectError()
        else:
            # Prepare config text
            if self.fileless_mode:
                config_text = Glean_config.empty_config_toml % 'fileless_config'
            else:
                config_text = Glean_config.empty_config_toml % PurePath(self.toml_file).name
                if Path(self.toml_file).exists():
                    config_text = Path(self.toml_file).read_text(encoding='utf-8')
                    self.modified = False
            
            self.config = tomllib.loads(config_text)
            self._read_env()

    def _read_env(self):
        environment = self.config.get('environment', {})

        env_vars = environment.get('env', [])
        for var in env_vars:
            value = environ.get(var)
            if value is not None:
                self.__setitem__(var, value)

        env_rex = environment.get('env_rex')
        if env_rex:
            config_items = self.config.get('config_items', {})
            for var in environ:
                if var not in config_items and re.match(env_rex, var):
                    # environ is being iterated over its keys, so indexing is safe
                    self.__setitem__(var, environ[var])

    def parameters(self) -> list[str]:
        return list(self.config.keys())

    def save(self, force: bool = False) -> None:
        if self.fileless_mode:
            # In fileless mode, just mark as not modified but don't save
            self.modified = False
            return
        
        if (self.modified or force) and self.config and self.toml_file:
            with Glean_config._file_lock:
                with open(self.toml_file, mode="wb") as fp:
                    tomli_w.dump(self.config, fp)
                self.modified = False

    def get_toml(self)->str:
        return tomli_w.dumps(self.config)

    def get_json(self)->str:
        return json.dumps(self.config)

    @classmethod
    def get_instance(cls, toml_file: str | None = None) -> 'Glean_config':
        with cls._object_lock:
            if not Glean_config._config_object:
                Glean_config._config_object = Glean_config(toml_file=toml_file, override=True)
        return Glean_config._config_object

    def get_config(self) -> Config[Any]:
        if 'config_items' not in self.config:
            raise KeyError("Configuration structure invalid: 'config_items' key not found")
        return self.config['config_items']

    def set_config(self, config: Config[T], merge:bool = False) -> None:
        with Glean_config._object_lock:
            if 'config_items' not in self.config:
                raise KeyError("Configuration structure invalid: 'config_items' key not found")
            if merge:
                self.config['config_items'].update(config)
            else:
                self.config['config_items'] = config
            self.modified = True
        self.save()

    def decode(self,encoded:str)-> str:
        return str(b64decode(encoded),'utf-8')

    def __getitem__(self, key: str):
        if 'config_items' not in self.config:
            raise KeyError("Configuration structure invalid: 'config_items' key not found")
        returnValue = self.config['config_items'].get(key, None)

        if returnValue and key.startswith('encoded_'):
            returnValue = self.decode(returnValue)
        return returnValue

    def __setitem__(self, key: str, value: str) -> None:
        if 'config_items' not in self.config:
            raise KeyError("Configuration structure invalid: 'config_items' key not found")
        if key.startswith('encoded_'):
            value = str(b64encode(bytes(value, 'utf-8')), 'utf-8')
        self.config['config_items'][key] = value
        self.modified = True

    def __enter__(self) -> 'Glean_config':
        return self

    def __exit__(self, exc_type: type | None, exc_value: BaseException | None, traceback: object | None) -> None:
        self.save()

    def close(self) -> None:
        """Explicitly save and close the config. Preferred over relying on __del__."""
        self.save()


    empty_config_toml=\
"""
config_name = '%s'
[environment]
env_rex='^glean_'
env=[]
[config_items]
"""
