import inspect
import json
import logging
from collections.abc import Sized
from pathlib import Path
from typing import List, Type, Tuple

import yaml

from autoconf.directory_config import family

logger = logging.getLogger(__name__)

default_prior = {
    "type": "Uniform",
    "lower_limit": 0.0,
    "upper_limit": 1.0,
    "width_modifier": {"type": "Absolute", "value": 0.2},
    "limits": {"lower": 0.0, "upper": 1.0},
}


def make_config_for_class(cls):
    path = path_for_class(cls)
    arg_spec = inspect.getfullargspec(cls)
    arguments = arg_spec.args[1:]
    defaults = list(reversed(arg_spec.defaults or list()))

    config = dict()
    for i, argument in enumerate(reversed(arguments)):
        if i < len(defaults):
            default = defaults[i]
            if isinstance(default, Sized):
                for j in range(len(default)):
                    config[f"{argument}_{j}"] = default_prior
                continue
        config[argument] = default_prior

    return path, config


def path_for_class(cls) -> List[str]:
    """
    A list describing the import path for a given class.

    Parameters
    ----------
    cls
        A class with some module path

    Returns
    -------
    A list of modules terminating in the name of a class
    """
    return f"{cls.__module__}.{cls.__name__}".split(".")


class JSONPriorConfig:
    def __init__(self, config_dict: dict, directory=None):
        """
        Parses configuration describing priors associated with classes.

        The path pointing to a class is the same as the path to import it.

        Paths can be strings with '.' as a delimiter.
        {"module.class": config}

        Else they can be a series of dictionary keys.
        {"module": {"class": config}}

        Or any combination thereof.

        Parameters
        ----------
        config_dict
            A dictionary describing the prior configuration for constructor arguments
            of different classes.
        """
        self.obj = config_dict
        self.directory = directory
        self._path_value_map = None

    @property
    def paths(self):
        return list(self.path_value_map.keys())

    @property
    def path_value_map(self) -> dict:
        """
        A dictionary matching every possible path to the configuration it points to.
        """
        if self._path_value_map is None:

            def get_path_values(obj):
                path_values = dict()
                if isinstance(obj, dict):
                    for key, value in obj.items():
                        path_values[key] = value
                        for path, path_value in get_path_values(value).items():
                            path_values[f"{key}.{path}"] = path_value

                return path_values

            self._path_value_map = get_path_values(self.obj)
        return self._path_value_map

    @property
    def path_value_tuples(self) -> List[Tuple[str, object]]:
        """
        Tuple pairs matching every possible path to the configuration it points to.
        These are ordered by key length with the longest key first.
        """
        return sorted(
            list(self.path_value_map.items()),
            key=lambda item: len(item[0]),
            reverse=True,
        )

    @classmethod
    def from_directory(cls, directory: str) -> "JSONPriorConfig":
        """
        Load JSONPriorConfiguration from a file.

        Parameters
        ----------
        directory
            The path to a file.

        Returns
        -------
        A configuration instance.
        """
        config_dict = dict()

        config_path = Path(directory)

        for suffix, parser in [
            ("json", json.load),
            ("yaml", yaml.safe_load),
            ("yml", yaml.safe_load),
        ]:
            for file in config_path.rglob(f"*.{suffix}"):
                parts = file.relative_to(config_path).with_suffix("").parts
                with open(file) as f:
                    config_dict[".".join(parts)] = parser(f)

        return JSONPriorConfig(config_dict, directory=directory)

    def __str__(self):
        return json.dumps(self.obj)

    def __getitem__(self, item):
        return JSONPriorConfig(self.obj[".".join(item)], directory=self.directory)

    def __contains__(self, item):
        return ".".join(item) in self.obj

    def for_class_and_suffix_path(self, cls: Type, suffix_path: List[str]):
        """
        Get configuration for a prior.

        If it is just basic configuration then the suffix path is just the
        name of the prior in a list. Width configuration also adds an
        additional "width_modifier" item.

        If configuration is not found for the class then configurations for
        parents of the class are searched.

        Parameters
        ----------
        cls
            The class with which the prior is associated.
        suffix_path
            The path to the prior.

        Returns
        -------
        A configuration dictionary
        """
        for c in family(cls):
            try:
                return self(path_for_class(c) + suffix_path)
            except KeyError:
                pass
        raise KeyError(
            f"No config found for class {cls} and path {suffix_path} in {self.directory}"
        )

    def __call__(self, config_path: List[str]):
        """
        Get the config at the end of the config_path.

        The configuration dictionary is traversed until config is found
        at the end, else an exception is thrown.

        Parameters
        ----------
        config_path
            The import path of a package, module, class or class and constructor
            argument name.

        Returns
        -------
        A configuration dictionary or value

        Raises
        ------
        PriorException
            If no configuration is found.
        """
        key = ".".join(config_path)
        for path, value in self.path_value_tuples:
            if key.endswith(path):
                return value
        raise KeyError(
            f"No configuration was found for the path {config_path}"
            + ("" if self.directory is None else f" ({self.directory})")
        )
