from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Union

import yaml
from xlsxwriter.format import Format


def validate_single_format(f: Callable) -> Callable:
    @wraps(f)
    def wrapper(cls, format_dict: Dict[str, Any]) -> Any:
        """
        Decorator to validate that input is a dictionary dictionary.
        """
        if not isinstance(format_dict, dict):
            actual_type = type(format_dict)
            msg = "Formats must be supplied as a dictionary, not" f"{actual_type}"
            raise ValueError(msg)

        for fmt in format_dict.keys():
            cls._validate_format_label(fmt)
        return f(cls, format_dict)

    return wrapper


class Theme:
    """
    Defines format attributes for table elements for use with XlsxWriter.

    For valid values, see XlsxWriter `Format` properties:
    _<https://xlsxwriter.readthedocs.io/format.html#format-methods-and-format-properties>_.

    Attributes
    ----------
    cover_title_format : dict

    cover_subtitle_format : dict

    cover_text_format : dict

    title_format : dict

    subtitle_format : dict

    instructions_format : dict

    scope_format : dict

    column_heading_format : dict

    index_1_format : dict

    index_2_format : dict

    index_3_format : dict

    data_format : dict

    source_format : dict

    legend_format : dict

    description_order : list
    """

    def __init__(self, config: Optional[Union[Dict[str, Any], str]] = None) -> None:
        """
        Initialise theme object.

        Parameters
        ----------
        config : dict or .yaml/.yml file
            theme specification
        """
        # Formats
        self._format_attributes = [
            "cover_title_format",
            "cover_subtitle_format",
            "cover_text_format",
            "title_format",
            "subtitle_format",
            "instructions_format",
            "scope_format",
            "column_heading_format",
            "index_1_format",
            "index_2_format",
            "index_3_format",
            "data_format",
            "source_format",
            "legend_format",
        ]

        for attr in self._format_attributes:
            setattr(self, attr, {})

        # Other attributes
        self.description_order = []

        # Valid Them format attributes
        self._valid_attrs = [
            x.replace("_format", "") for x in self._format_attributes
        ] + ["global"]

        # Valid XlsxWriter Format attributes
        self._valid_format_labels = [
            attr.replace("set_", "")
            for attr in Format().__dir__()
            if attr.startswith("set_") and callable(getattr(Format(), attr))
        ]

        if config:
            self.apply_config(config)

    @staticmethod
    def _parse_config(config: Union[Dict[str, Any], str]) -> Dict[str, Any]:
        """
        Parse yaml configuration to dictionary.
        """
        if isinstance(config, str):
            if not config.endswith((".yml", ".yaml")):
                raise ValueError("Theme configuration files must be YAML")
            with open(config, "r") as file:
                cfg = yaml.safe_load(file)

        elif isinstance(config, dict):
            cfg = config

        else:
            raise ValueError("Theme configuration must be a dict or YAML file")

        return cfg

    def _validate_config(self, config: Dict[str, Any]) -> None:
        """
        Assert that format dictionary lower level keys are valid XlsxWriter
        Format attributes.
        """
        for attr in config.keys():
            if attr in self._valid_attrs:
                attr_config = config[attr] or {}
                for fmt in attr_config.keys():
                    self._validate_format_label(fmt)

    def _validate_format_label(self, format_name: str) -> None:
        """
        Assert that format is a valid XlsxWriter Format attribute.
        """
        if format_name not in self._valid_format_labels:
            raise ValueError(f"`{format_name}` is not a valid format label")

    def apply_config(self, config: Union[Dict[str, Any], str]) -> None:
        """
        Update multiple Theme attributes using a YAML or dictionary config.
        This enables extension of build in Themes.
        """
        cfg = self._parse_config(config)
        self._validate_config(cfg)

        # Update all when global used
        if "global" in cfg.keys():
            default_format = cfg.pop("global")
            self._update_all_formats(default_format)

        # Update with individual methods
        for key, value in cfg.items():
            if key == "description_order":
                getattr(self, "update_" + key)(value)
            elif key in self._valid_attrs:
                if value is not None:
                    getattr(self, "update_" + key + "_format")(value)
            else:
                raise ValueError(f"`{key}` is not a valid Theme attribute")

    def _update_all_formats(self, global_dict: Dict[str, Any]) -> None:
        """
        Updates all theme attributes with a global format dictionary.
        """
        for attr in self._format_attributes:
            if attr.endswith("_format"):
                getattr(self, "update_" + attr)(global_dict)

    @validate_single_format
    def update_column_heading_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `column_heading_format` attribute. Where keys already exist,
        existing items are replaced.
        """
        self.column_heading_format.update(format_dict)

    @validate_single_format
    def update_index_1_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `index_1_format` attribute. Where keys already exist, existing
        items are replaced.
        """
        self.index_1_format.update(format_dict)

    @validate_single_format
    def update_index_2_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `index_2_format` attribute. Where keys already exist, existing
        items are replaced.
        """
        self.index_2_format.update(format_dict)

    @validate_single_format
    def update_index_3_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `index_3_format` attribute. Where keys already exist, existing
        items are replaced.
        """
        self.index_3_format.update(format_dict)

    @validate_single_format
    def update_data_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `data_format` attribute. Where keys already exist,
        existing items are replaced.
        """
        self.data_format.update(format_dict)

    @validate_single_format
    def update_cover_title_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `cover_title_format` attribute. Where keys already exist, existing
        items are replaced.
        """
        self.cover_title_format.update(format_dict)

    @validate_single_format
    def update_cover_subtitle_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `cover_subtitle_format` attribute. Where keys already exist, existing
        items are replaced.
        """
        self.cover_subtitle_format.update(format_dict)

    @validate_single_format
    def update_cover_text_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `cover_text_format` attribute. Where keys already exist, existing
        items are replaced.
        """
        self.cover_text_format.update(format_dict)

    @validate_single_format
    def update_title_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `title_format` attribute. Where keys already exist, existing
        items are replaced.
        """
        self.title_format.update(format_dict)

    @validate_single_format
    def update_subtitle_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `subtitle_format` attribute. Where keys already exist,
        existing items are replaced.
        """
        self.subtitle_format.update(format_dict)

    @validate_single_format
    def update_instructions_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `instructions_format` attribute. Where keys already exist,
        existing items are replaced.
        """
        self.instructions_format.update(format_dict)

    @validate_single_format
    def update_scope_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `scope_format` attribute. Where keys already exist, existing
        items are replaced.
        """
        self.scope_format.update(format_dict)

    @validate_single_format
    def update_location_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `location_format` attribute. Where keys already exist,
        existing items are replaced.
        """
        self.location_format.update(format_dict)

    @validate_single_format
    def update_source_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `source_format` attribute. Where keys already exist,
        existing items are replaced.
        """
        self.source_format.update(format_dict)

    @validate_single_format
    def update_legend_format(self, format_dict: Dict[str, Any]) -> None:
        """
        Update the `legend_format` attribute. Where keys already exist,
        existing items are replaced.
        """
        self.legend_format.update(format_dict)

    def update_description_order(self, order_list: List[str]) -> None:
        """
        Update the `description_order` attribute. Overrides existing order.
        """
        if not isinstance(order_list, list):
            msg = "`description_order` must be a list of description element names"
            raise TypeError(msg)

        valid_elements = ["instructions", "source", "legend", "scope"]
        if not all(element in valid_elements for element in order_list):
            msg = f"`description_order` elements must be in {valid_elements}"
            raise ValueError(msg)
        self.description_order = order_list

    def print_attributes(self) -> None:
        """
        Print all current format attributes and values to the console.
        """
        obj_attr = [
            attr
            for attr in self.__dir__()
            if not attr.startswith("_") and not callable(getattr(self, attr))
        ]
        for attr in obj_attr:
            print(attr, ":", getattr(self, attr))

    def __eq__(self, other: object) -> bool:
        """
        Comparison operator, for testing.
        """
        # don't attempt to compare against unrelated types
        if not isinstance(other, Theme):
            return False

        obj_attr = [
            attr
            for attr in self.__dir__()
            if not attr.startswith("_") and not callable(getattr(self, attr))
        ]
        return all([getattr(self, attr) == getattr(other, attr) for attr in obj_attr])
