import copy
import os

from utils.vars import REQUIRED_DB_SCHEMA_VERSION, MAP_FILE_BASENAME
from utils.exceptions import NotAValidDatabaseConfigError, DatabaseError
from utils.helper_funcs import recursively_iter_dict
from utils.input_output import load_yaml, write_yaml


class DbYamlInteractor:
    def __init__(self, yaml, db_yaml_dict=None):
        self.yaml = yaml
        if db_yaml_dict:
            self._config = db_yaml_dict
        else:
            self._config = self.get_db_config_dict_from_yaml(self.yaml)

    def __str__(self):
        return f"db type: {self._config['type']}\n" \
               + f"   version: {self._config['version']}\n" \
               + f"   file: {self.yaml}"

    @staticmethod
    def get_db_config_dict_from_yaml(yaml):
        dct = load_yaml(yaml)
        if dct is None:
            dct = {}
        return dct

    def get_db_schema_version(self) -> int:
        schema_version_key = "db_schema_version"
        if schema_version_key not in self._config:
            schema_version = 0
        else:
            schema_version = int(self._config[schema_version_key])
        return schema_version

    def is_db_schema_version_outdated(self) -> bool:
        db_yaml_version = self.get_db_schema_version()
        if db_yaml_version < REQUIRED_DB_SCHEMA_VERSION:
            is_outdated = True
        elif db_yaml_version == REQUIRED_DB_SCHEMA_VERSION:
            is_outdated = False
        else:
            raise ValueError(
                "Database yaml schema version higher than supported by this Prophane version.\n" +
                f"Max. supported version: {REQUIRED_DB_SCHEMA_VERSION}\n" +
                f"Found version: {db_yaml_version}\n" +
                f"in: {self.yaml}"
            )
        return is_outdated

    def write_yaml(self, force=False):
        if ((not os.path.exists(self.yaml))
                or force):
            write_yaml(dictionary=self._config, filename=self.yaml, sort_keys=False)
        else:
            raise FileExistsError(f"yaml-file already exists (force with 'force=True'): {self.yaml}")

    def get_yaml(self, relative=False):
        if relative:
            return os.path.relpath(self.yaml, os.getcwd())
        else:
            return self.yaml

    def get_config_deepcopy(self):
        return copy.deepcopy(self._config)

    def is_empty(self):
        return self._config == {}


class Database(DbYamlInteractor):
    valid_types = []
    mandatory_keys = ['name', 'type', 'version', 'comment', 'scope', 'db_schema_version']

    def __init__(self, yaml, db_yaml_dict=None, validate=True):
        super().__init__(yaml, db_yaml_dict)
        if validate:
            self.validate_config()

    def __str__(self):
        msg = f"Database described by yaml file: '{self.get_yaml()}'\n"
        msg += f"version: {self.get_version()}\n"
        msg += f"type: {self.get_type()}"
        return msg

    def validate_config(self):
        if not self.is_database_dict(self._config):
            raise NotAValidDatabaseConfigError(
                f"Invalid database yaml: {self.yaml}\n"
                + f"required keys in yaml are:\n"
                + "\t" + "\n\t".join(self.mandatory_keys)
            )
        if self.is_requiring_helper_dbs():
            if not self.is_yaml_containing_required_dbs(self._config):
                raise NotAValidDatabaseConfigError(
                    f"Invalid database yaml: {self.yaml}\n"
                    + f"Missing 'required_dbs':\n"
                    + "\t" + "\n\t".join(self.required_db_types)
                )

    @property
    def required_db_types(self) -> list:
        raise NotImplementedError("This parameter must be implemented by the Child class.")

    @classmethod
    def is_database_dict(cls, dct):
        if type(dct) is not dict:
            return False
        elif all(key in dct for key in cls.mandatory_keys):
            return True
        else:
            return False

    @classmethod
    def is_yaml_containing_required_dbs(cls, dct):
        if 'required_dbs' in dct:
            for req_db in cls.required_db_types:
                if req_db not in dct['required_dbs']:
                    return False
                elif 'version' not in dct['required_dbs'][req_db]:
                    return False
            return True
        else:
            return False

    @classmethod
    def is_database_yaml(cls, yaml):
        yaml_dict = load_yaml(yaml)
        return cls.is_database_dict(yaml_dict)

    @classmethod
    def is_valid_type(cls, db_type):
        return db_type in cls.valid_types

    def download_db(self):
        raise NotImplementedError
        pass

    def get_name(self):
        return self._config['name']

    @staticmethod
    def get_version_from_db_config_dict(db_cfg_dct):
        return str(db_cfg_dct['version'])

    def get_version(self):
        return self.get_version_from_db_config_dict(self._config)

    def get_scope(self):
        return self._config['scope']

    def get_comment(self):
        return self._config['comment']

    def get_acc_hit_regex_pattern(self):
        return self._config['acc_hit_regexp']

    def get_no_of_lca_levels(self):
        return self._config['lca_level']

    def get_db_map_file(self, *args, **kwargs):
        path_wo_ext = self.join_with_db_path(os.path.join("processed", MAP_FILE_BASENAME), *args, **kwargs)
        return path_wo_ext + ".map.gz"

    def get_all_db_associated_map_files(self):
        map_files = []
        for k, v in recursively_iter_dict(self._config):
            if self._is_map_entry(k):
                map_files.append(v)
        return map_files

    def _is_map_entry(self, config_key):
        if config_key.endswith('map'):
            return True
        else:
            return False

    def get_all_mandatory_files(self, *args, **kwargs):
        if 'files' in self._config:
            return [self.join_with_db_path(f, *args, **kwargs) for f in self._config['files'].values()]
        else:
            return []

    def join_with_db_path(self, f, relative=False):
        db_dir = os.path.dirname(self.get_yaml())
        if relative:
            db_dir = os.path.relpath(db_dir, os.getcwd())
        full_path = os.path.join(db_dir, f)
        return full_path

    @staticmethod
    def get_type_from_db_config_dict(db_cfg_dct):
        return db_cfg_dct['type']

    def get_type(self):
        return self.get_type_from_db_config_dict(self._config)

    @classmethod
    def get_type_of_db_yaml(cls, db_yaml):
        db_cfg_dct = cls.get_db_config_dict_from_yaml(db_yaml)
        return cls.get_type_from_db_config_dict(db_cfg_dct)

    @classmethod
    def is_requiring_helper_dbs(cls):
        return False

    @classmethod
    def get_valid_types(cls):
        return cls.valid_types[:]


class DatabaseRequiringHelperDbs(Database):
    mandatory_keys = Database.mandatory_keys + ['required_dbs']

    def __init__(self, *args, required_dbs, **kwargs):
        super().__init__(*args, **kwargs)
        self.__required_dbs_dict = self._build_dict_required_dbs(required_dbs)

    def get_helper_db_by_type(self, db_type):
        return self.__required_dbs_dict[db_type]

    @classmethod
    def is_requiring_helper_dbs(cls):
        return True

    @classmethod
    def get_lst_of_tuples_of_required_dbs(cls, yaml_path_or_cfg_dct):
        if type(yaml_path_or_cfg_dct) is str:
            cfg_dct = cls.get_db_config_dict_from_yaml(yaml_path_or_cfg_dct)
        elif type(yaml_path_or_cfg_dct) is dict:
            cfg_dct = yaml_path_or_cfg_dct
        else:
            raise TypeError(
                f"parameter 'yaml_path_or_cfg_dct' must be of type dict or str but is: {type(yaml_path_or_cfg_dct)}")
        lst_of_tuples_of_required_dbs = []
        for db_type in cls.required_db_types:
            try:
                db_version = str(cfg_dct['required_dbs'][db_type]['version'])
            except KeyError:
                raise DatabaseError(
                    f"Provided dict does not contain required keys ['required_dbs']['{db_type}']['version']\n"
                    + "Provided dict:\n"
                    + str(cfg_dct) + "\n"
                )
            lst_of_tuples_of_required_dbs.append((db_type, db_version))
        return lst_of_tuples_of_required_dbs

    def _build_dict_required_dbs(self, lst_required_dbs):
        assert type(lst_required_dbs) is list
        lst_remaining_required_dbs = self.required_db_types[:]
        dct_required_dbs = {}
        for db_o in lst_required_dbs:
            db_type = db_o.get_type()
            required_version = str(self._config['required_dbs'][db_type]['version'])
            provided_version = db_o.get_version()
            try:
                lst_remaining_required_dbs.remove(db_type)
            except ValueError as e:
                raise DatabaseError(f"Provided Database of type '{db_type}'\n"
                                    + "is not required by this Database class\n"
                                    + f"Required types are: {', '.join(self.required_db_types)}")
            if provided_version != required_version:
                raise DatabaseError(f"Provided Database of type '{db_type}'\n"
                                    + "does not have required version.\n"
                                    + f"Required version: {required_version}\n"
                                    + f"Provided version: {provided_version}")
            dct_required_dbs[db_type] = db_o
        if len(lst_remaining_required_dbs) > 0:
            raise DatabaseError(f"Missing required Databases of type(s): {', '.join(lst_remaining_required_dbs)}")
        return dct_required_dbs
