import json
from os import PathLike
from pathlib import Path
from typing import Any, Union, Optional, Literal
import yaml

class Config(object):
    def __init__(self, data: dict, base_path: str):
        self._write_mode = True
        self._base_path = base_path

        for key, val in data.items():
            if isinstance(val, (list, tuple)):
                generator = (self.__parse_value(item) for item in val)
                setattr(self, key, tuple(generator))
            else:
                setattr(self, key, self.__parse_value(val))

        delattr(self, '_base_path')
        delattr(self, '_write_mode')

    def __parse_value(self, value: Any):
        if isinstance(value, dict):
            return self.__class__(value, self._base_path)

        if isinstance(value, str):
            if value.startswith('path:'):
                value = value[len('path:'):]
                value = str((Path(self._base_path) / value).absolute())

        return value

    def __setattr__(self, key, value):
        if key == '_write_mode' or hasattr(self, '_write_mode'):
            super().__setattr__(key, value)
        else:
            raise Exception('Set config')

    def __delattr__(self, item):
        if item == '_write_mode' or hasattr(self, '_write_mode'):
            super().__delattr__(item)
        else:
            raise Exception('Del config')

    def __contains__(self, name):
        return name in self.__dict__

    def __getitem__(self, name):
        return self.__dict__[name]

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

    @staticmethod
    def __item_to_dict(val):
        if isinstance(val, Config):
            return val.to_dict()
        if isinstance(val, (list, tuple)):
            generator = (Config.__item_to_dict(item) for item in val)
            return list(generator)
        return val
    
    def merge(self, other_conf):
        return Config(
            data={**self.to_dict(), **other_conf.to_dict()},
            base_path=''
        )
    
    def get(self, key, default=None):
        return self.__dict__.get(key, default)

    def to_dict(self) -> dict:
        """
        Convert object to dict recursively!
        :return: Dictionary output
        """
        return {
            key: Config.__item_to_dict(val) for key, val in self.__dict__.items()
        }


def load_config(config_file_path: Union[str, PathLike], base_path: Optional[Union[str, PathLike]] = None,
                file_type: Literal['json', 'JSON', 'yml', 'YML', 'yaml', 'YAML', None] = None) -> Config:
    """
    Load configs from a YAML or JSON file.
    :param config_file_path: File path as a string or pathlike object
    :param base_path: Base path for `path:` strings, default value is parent of `config_file_path`
    :param file_type: What is the format of the file. If none it will look at the file extension
    :return: A config object
    """
    if base_path is None:
        base_path = str(Path(config_file_path).resolve().parent)
    if file_type is None:
        file_type = Path(config_file_path).suffix
        file_type = file_type[1:]  # remove extra first dot!

    content = Path(config_file_path).read_text(encoding='utf-8')
    load_content = {
        'json': json.loads,
        'yaml': yaml.safe_load,
        'yml': yaml.safe_load
    }[file_type.lower()]
    return Config(load_content(content), base_path)