"""
Configuration loader module
"""

import os
import shutil
from pathlib import Path
from typing import Optional, Type, TypeVar, TypedDict, cast
import yaml

import zscams
from zscams.agent.src.support.yaml import YamlIndentedListsDumper, resolve_placeholders

ROOT_PATH = Path(zscams.__file__).resolve().parent.joinpath("agent")
CONFIG_PATH = os.path.join(ROOT_PATH.absolute(), "config.yaml")

GetReturnT = TypeVar("GetReturnT")


class MissingConfiguration(BaseException):
    """Error when the requisted key is not found"""


class Configuration:
    """Class to get/set configurations and handle the configurations file."""

    __instance = None
    __config = {}

    def __init__(self):
        self.__load_config()

    def __new__(cls):
        if cls.__instance is None:
            cls.__instance = super().__new__(cls)
        return cls.__instance

    def __load_config(self):
        try:
            with open(CONFIG_PATH, "r", encoding="utf-8") as f:
                self.__config = yaml.safe_load(f)
        except FileNotFoundError:
            shutil.copyfile(ROOT_PATH.joinpath("configuration/config.j2"), CONFIG_PATH)
            self.__load_config()

    def override_config(self, new_config: dict):
        """
        Override the existing configuration with a new one.
        Args:
            new_config (dict): New configuration dictionary to override the existing one.
        """
        self.__config = new_config

        with open(CONFIG_PATH, "w", encoding="utf-8") as file:
            yaml.dump(
                self.__config,
                file,
                Dumper=YamlIndentedListsDumper,
                default_flow_style=False,
                explicit_start=True,
            )

    def to_dict(self):
        return self.__config

    def get(
        self,
        key: str,
        default: Optional[GetReturnT] = None,
        must_resolve=False,
        valtyp: Type[GetReturnT] = str,
    ) -> GetReturnT:
        """
        Returns the configuration value.

        Args:
            key (str): The configurations dotnotation key to get the value of.
            default (Optional[T]): The default value if the configurations wasn't found.
            must_resolve (bool): Wheither to throw an error if the value wasn't found and if there was no default.
            valtyp (Type[T]): The expected type of the return value.
        """

        keys = key.split(".")
        val = self.__config

        for k in keys:
            if isinstance(val, dict) and k in val:
                val = val.get(k, default)
            elif isinstance(val, list) and int(k) < len(val):
                val = val[int(k)]
            elif default is not None:
                val = default
                break
            elif must_resolve:
                raise MissingConfiguration
            else:
                val = None
                break

        return cast(GetReturnT, val)

    def has(self, key: str):
        try:
            self.get(key, must_resolve=True)
            return True
        except MissingConfiguration:
            return False

    def reinitialize(self, **kwargs):
        """Regenerate the config template with new values"""
        template_path = ROOT_PATH.joinpath("configuration/config.j2")
        with open(template_path, encoding="utf-8") as f:
            template = yaml.safe_load(f)
        resolve_placeholders(template, kwargs)
        self.override_config(template)


class RemoteConfig(TypedDict):
    """Type definition for remote configuration."""

    host: str
    port: str
    verify_cert: bool
    client_key: str
    ca_cert: str
    ca_chain: str


config = Configuration()
