"""File-backed config store
"""
import copy
import os
from logging import getLogger

from ruamel.yaml import YAML  # type: ignore
from smif.data_layer.abstract_config_store import ConfigStore
from smif.data_layer.validate import (
    validate_sos_model_config,
    validate_sos_model_format,
)
from smif.exception import (
    SmifDataExistsError,
    SmifDataMismatchError,
    SmifDataNotFoundError,
)


class YamlConfigStore(ConfigStore):
    """Config backend saving to YAML configuration files.

    Arguments
    ---------
    base_folder: str
        The path to the configuration and data files
    """

    def __init__(self, base_folder, validation=False):
        super().__init__()
        self.logger = getLogger(__name__)
        self.validation = validation

        self.base_folder = str(base_folder)
        self.config_folder = str(os.path.join(self.base_folder, "config"))
        self.config_folders = {}
        config_folders = [
            "dimensions",
            "model_runs",
            "scenarios",
            "sector_models",
            "sos_models",
        ]
        for folder in config_folders:
            dirname = os.path.join(self.config_folder, folder)
            # ensure each directory exists
            if not os.path.exists(dirname):
                msg = "Expected configuration folder at '{}' but it does not exist"
                abs_path = os.path.abspath(dirname)
                raise SmifDataNotFoundError(msg.format(abs_path))

            self.config_folders[folder] = dirname

        # cache results of reading project_config (invalidate on write)
        self._project_config_cache_invalid = True
        # MUST ONLY access through self.read_project_config()
        self._project_config_cache = None

        # ensure project config file exists
        try:
            self.read_project_config()
        except FileNotFoundError:
            # write empty config if none found
            self._write_project_config({})

    def read_project_config(self):
        """Read the project configuration

        Returns
        -------
        dict
            The project configuration
        """
        if self._project_config_cache_invalid:

            self._project_config_cache = _read_yaml_file(self.base_folder, "project")
            self._project_config_cache_invalid = False
        return copy.deepcopy(self._project_config_cache)

    def _write_project_config(self, data):
        """Write the project configuration

        Argument
        --------
        data: dict
            The project configuration
        """
        self._project_config_cache_invalid = True
        self._project_config_cache = None
        _write_yaml_file(self.base_folder, "project", data)

    def _read_config(self, config_type, config_name):
        """Read config item - used by decorators for existence/consistency checks"""
        if config_type == "scenario":
            return self.read_scenario(config_name)
        else:
            raise NotImplementedError(
                "Cannot read %s:%s through generic method." % (config_type, config_name)
            )

    # region Model runs
    def read_model_runs(self):
        names = _read_filenames_in_dir(self.config_folders["model_runs"], ".yml")
        model_runs = [self.read_model_run(name) for name in names]
        return model_runs

    def read_model_run(self, model_run_name):
        _assert_file_exists(self.config_folders, "model_run", model_run_name)
        modelrun_config = self._read_model_run(model_run_name)
        del modelrun_config["strategies"]
        return modelrun_config

    def _read_model_run(self, model_run_name):
        return _read_yaml_file(self.config_folders["model_runs"], model_run_name)

    def _overwrite_model_run(self, model_run_name, model_run):
        _write_yaml_file(self.config_folders["model_runs"], model_run_name, model_run)

    def write_model_run(self, model_run):
        _assert_file_not_exists(self.config_folders, "model_run", model_run["name"])
        config = copy.copy(model_run)
        config["strategies"] = []
        _write_yaml_file(self.config_folders["model_runs"], config["name"], config)

    def update_model_run(self, model_run_name, model_run):
        if model_run["name"] != model_run_name:
            raise SmifDataMismatchError(
                "Model run name '%s' must match '%s'"
                % (model_run_name, model_run["name"])
            )
        _assert_file_exists(self.config_folders, "model_run", model_run_name)
        prev = self._read_model_run(model_run_name)
        config = copy.copy(model_run)
        config["strategies"] = prev["strategies"]
        self._overwrite_model_run(model_run_name, config)

    def delete_model_run(self, model_run_name):
        _assert_file_exists(self.config_folders, "model_run", model_run_name)
        os.remove(
            os.path.join(self.config_folders["model_runs"], model_run_name + ".yml")
        )

    # endregion

    # region System-of-system models
    def read_sos_models(self):
        names = _read_filenames_in_dir(self.config_folders["sos_models"], ".yml")
        sos_models = [self.read_sos_model(name) for name in names]
        return sos_models

    def read_sos_model(self, sos_model_name):
        _assert_file_exists(self.config_folders, "sos_model", sos_model_name)

        data = _read_yaml_file(self.config_folders["sos_models"], sos_model_name)
        if self.validation:
            validate_sos_model_format(data)
        return data

    def write_sos_model(self, sos_model):
        _assert_file_not_exists(self.config_folders, "sos_model", sos_model["name"])
        _write_yaml_file(
            self.config_folders["sos_models"], sos_model["name"], sos_model
        )

    def update_sos_model(self, sos_model_name, sos_model):
        if sos_model["name"] != sos_model_name:
            raise SmifDataMismatchError(
                "SoSModel name '%s' must match '%s'"
                % (sos_model_name, sos_model["name"])
            )
        _assert_file_exists(self.config_folders, "sos_model", sos_model_name)
        if self.validation:
            validate_sos_model_config(
                sos_model,
                self.read_models(),
                self.read_scenarios(),
            )
        _write_yaml_file(
            self.config_folders["sos_models"], sos_model["name"], sos_model
        )

    def delete_sos_model(self, sos_model_name):
        _assert_file_exists(self.config_folders, "sos_model", sos_model_name)
        os.remove(
            os.path.join(self.config_folders["sos_models"], sos_model_name + ".yml")
        )

    # endregion

    # region Models
    def read_models(self):
        names = _read_filenames_in_dir(self.config_folders["sector_models"], ".yml")
        models = [self.read_model(name) for name in names]
        return models

    def read_model(self, model_name):
        _assert_file_exists(self.config_folders, "sector_model", model_name)

        model = _read_yaml_file(self.config_folders["sector_models"], model_name)
        return model

    def write_model(self, model):
        _assert_file_not_exists(self.config_folders, "sector_model", model["name"])
        model = copy.deepcopy(model)
        if model["interventions"]:
            self.logger.warning("Ignoring interventions")
            model["interventions"] = []

        model = _skip_coords(model, ("inputs", "outputs", "parameters"))
        _write_yaml_file(self.config_folders["sector_models"], model["name"], model)

    def update_model(self, model_name, model):
        if model["name"] != model_name:
            raise SmifDataMismatchError(
                "Model name '%s' must match '%s'" % (model_name, model["name"])
            )
        _assert_file_exists(self.config_folders, "sector_model", model_name)
        model = copy.deepcopy(model)

        # ignore interventions and initial conditions which the app doesn't handle
        if model["interventions"] or model["initial_conditions"]:

            old_model = _read_yaml_file(
                self.config_folders["sector_models"], model["name"]
            )

        if model["interventions"]:
            self.logger.warning("Ignoring interventions write")
            model["interventions"] = old_model["interventions"]

        if model["initial_conditions"]:
            self.logger.warning("Ignoring initial conditions write")
            model["initial_conditions"] = old_model["initial_conditions"]

        model = _skip_coords(model, ("inputs", "outputs", "parameters"))

        _write_yaml_file(self.config_folders["sector_models"], model["name"], model)

    def delete_model(self, model_name):
        _assert_file_exists(self.config_folders, "sector_model", model_name)
        os.remove(
            os.path.join(self.config_folders["sector_models"], model_name + ".yml")
        )

    # endregion

    # region Scenarios
    def read_scenarios(self):
        scenario_names = _read_filenames_in_dir(
            self.config_folders["scenarios"], ".yml"
        )
        return [self.read_scenario(name) for name in scenario_names]

    def read_scenario(self, scenario_name):
        _assert_file_exists(self.config_folders, "scenario", scenario_name)

        scenario = _read_yaml_file(self.config_folders["scenarios"], scenario_name)
        return scenario

    def write_scenario(self, scenario):
        _assert_file_not_exists(self.config_folders, "scenario", scenario["name"])
        scenario = _skip_coords(scenario, ["provides"])
        _write_yaml_file(self.config_folders["scenarios"], scenario["name"], scenario)

    def update_scenario(self, scenario_name, scenario):
        _assert_file_exists(self.config_folders, "scenario", scenario_name)
        scenario = _skip_coords(scenario, ["provides"])
        _write_yaml_file(self.config_folders["scenarios"], scenario["name"], scenario)

    def delete_scenario(self, scenario_name):
        _assert_file_exists(self.config_folders, "scenario", scenario_name)
        os.remove(
            os.path.join(
                self.config_folders["scenarios"], "{}.yml".format(scenario_name)
            )
        )

    # endregion

    # region Scenario Variants
    def read_scenario_variants(self, scenario_name):
        scenario = self.read_scenario(scenario_name)
        return scenario["variants"]

    def read_scenario_variant(self, scenario_name, variant_name):
        variants = self.read_scenario_variants(scenario_name)
        return _pick_from_list(variants, variant_name)

    def write_scenario_variant(self, scenario_name, variant):
        scenario = self.read_scenario(scenario_name)
        scenario["variants"].append(variant)
        self.update_scenario(scenario_name, scenario)

    def update_scenario_variant(self, scenario_name, variant_name, variant):
        scenario = self.read_scenario(scenario_name)
        v_idx = _idx_in_list(scenario["variants"], variant_name)
        scenario["variants"][v_idx] = variant
        self.update_scenario(scenario_name, scenario)

    def delete_scenario_variant(self, scenario_name, variant_name):
        scenario = self.read_scenario(scenario_name)
        v_idx = _idx_in_list(scenario["variants"], variant_name)
        del scenario["variants"][v_idx]
        self.update_scenario(scenario_name, scenario)

    # endregion

    # region Narratives
    def read_narrative(self, sos_model_name, narrative_name):
        sos_model = self.read_sos_model(sos_model_name)
        narrative = _pick_from_list(sos_model["narratives"], narrative_name)
        if not narrative:
            msg = "Narrative '{}' not found in '{}'"
            raise SmifDataNotFoundError(msg.format(narrative_name, sos_model_name))
        return narrative

    # endregion

    # region Strategies
    def read_strategies(self, modelrun_name):
        model_run_config = self._read_model_run(modelrun_name)
        return model_run_config["strategies"]

    def write_strategies(self, modelrun_name, strategies):
        model_run = self._read_model_run(modelrun_name)
        model_run["strategies"] = strategies
        self._overwrite_model_run(modelrun_name, model_run)

    # endregion


def _read_yaml_file(directory, name):
    """Read yaml config file into plain data (lists, dicts and simple values)

    Parameters
    ----------
    directory : str
    name : str
    """
    path = os.path.join(directory, "{}.yml".format(name))
    with open(path, "r") as file_handle:
        return YAML().load(file_handle)


def _write_yaml_file(directory, name, data):
    """Write plain data to a file as yaml

    Arguments
    ---------
    directory: str
        Path to directory
    name: str
        Name of config item (filename without .yml extension)
    data
        Data to be written to the file
    """
    path = os.path.join(directory, "{}.yml".format(name))
    with open(path, "w") as file_handle:
        yaml = YAML()
        yaml.default_flow_style = False
        yaml.allow_unicode = True
        return yaml.dump(data, file_handle)


def _assert_file_exists(file_dir, dtype, name):
    if not _file_exists(file_dir, dtype, name):
        raise SmifDataNotFoundError("%s '%s' not found" % (dtype.capitalize(), name))


def _assert_file_not_exists(file_dir, dtype, name):
    if _file_exists(file_dir, dtype, name):
        raise SmifDataExistsError("%s '%s' already exists" % (dtype.capitalize(), name))


def _file_exists(file_dir, dtype, name):
    dir_key = "%ss" % dtype
    try:
        return os.path.exists(os.path.join(file_dir[dir_key], name + ".yml"))
    except TypeError:
        msg = "Could not parse file name {} and dtype {}"
        raise SmifDataNotFoundError(msg.format(name, dtype))


def _read_filenames_in_dir(path, extension):
    """Returns the name of the Yaml files in a certain directory

    Arguments
    ---------
    path: str
        Path to directory
    extension: str
        Extension of files (such as: '.yml' or '.csv')

    Returns
    -------
    list
        The list of files in `path` with extension
    """
    files = []
    for filename in os.listdir(path):
        if filename.endswith(extension):
            files.append(os.path.splitext(filename)[0])
    return files


def _skip_coords(config, keys):
    """Given a config dict and list of top-level keys for lists of specs,
    delete coords from each spec in each list.
    """
    config = copy.deepcopy(config)
    for key in keys:
        for spec in config[key]:
            try:
                del spec["coords"]
            except KeyError:
                pass
    return config


def _pick_from_list(list_of_dicts, name):
    for item in list_of_dicts:
        if "name" in item and item["name"] == name:
            return item
    return None


def _idx_in_list(list_of_dicts, name):
    for i, item in enumerate(list_of_dicts):
        if "name" in item and item["name"] == name:
            return i
    return None
