from copy import deepcopy
from typing import Union, Dict, Any, List, Optional

from checkov.common.graph.graph_builder.graph_components.attribute_names import CustomAttributes
from checkov.common.graph.graph_builder.utils import calculate_hash, join_trimmed_strings
from checkov.common.graph.graph_builder.variable_rendering.breadcrumb_metadata import BreadcrumbMetadata


class Block:
    def __init__(
            self,
            name: str,
            config: Dict[str, Any],
            path: str,
            block_type: str,
            attributes: Dict[str, Any],
            id: str = "",
            source: str = "",
    ) -> None:
        """
            :param name: unique name given to the block, for example
            :param config: the section in tf_definitions that belong to this block
            :param path: the file location of the block
            :param block_type: str
            :param attributes: dictionary of the block's original attributes in the origin file
        """
        self.name = name
        self.config = deepcopy(config)
        self.path = path
        self.block_type = block_type
        self.attributes = attributes
        self.id = id
        self.source = source
        self.changed_attributes: Dict[str, List[Any]] = {}
        self.breadcrumbs: Dict[str, List[Dict[str, Any]]] = {}

        attributes_to_add = self._extract_inner_attributes()
        self.attributes.update(attributes_to_add)

    def _extract_inner_attributes(self) -> Dict[str, Any]:
        attributes_to_add = {}
        for attribute_key in self.attributes:
            attribute_value = self.attributes[attribute_key]
            if isinstance(attribute_value, dict) or (
                    isinstance(attribute_value, list) and len(attribute_value) > 0 and isinstance(attribute_value[0],
                                                                                                  dict)):
                inner_attributes = get_inner_attributes(attribute_key, attribute_value)
                attributes_to_add.update(inner_attributes)
        return attributes_to_add

    def __str__(self) -> str:
        return f"{self.block_type}: {self.name}"

    def get_attribute_dict(self, add_hash=True) -> Dict[str, Any]:
        """
           :return: map of all the block's native attributes (from the source file),
           combined with the attributes generated by the module builder.
           If the attributes are not a primitive type, they are converted to strings.
           """
        base_attributes = self.get_base_attributes()
        self.get_origin_attributes(base_attributes)

        if self.changed_attributes:
            # add changed attributes only for calculating the hash
            base_attributes["changed_attributes"] = sorted(self.changed_attributes.keys())

        if self.breadcrumbs:
            sorted_breadcrumbs = dict(sorted(self.breadcrumbs.items()))
            base_attributes[CustomAttributes.RENDERING_BREADCRUMBS] = sorted_breadcrumbs

        if add_hash:
            base_attributes[CustomAttributes.HASH] = calculate_hash(base_attributes)

        if "changed_attributes" in base_attributes:
            # removed changed attributes if it was added previously for calculating hash.
            del base_attributes["changed_attributes"]

        return base_attributes

    def get_origin_attributes(self, base_attributes: Dict[str, Any]) -> None:
        for attribute_key in list(self.attributes.keys()):
            attribute_value = self.attributes[attribute_key]
            if isinstance(attribute_value, list) and len(attribute_value) == 1:
                attribute_value = attribute_value[0]
            if isinstance(attribute_value, (list, dict)):
                inner_attributes = get_inner_attributes(attribute_key, attribute_value)
                base_attributes.update(inner_attributes)
            if attribute_key == "self":
                base_attributes["self_"] = attribute_value
                continue
            else:
                base_attributes[attribute_key] = attribute_value

    def get_hash(self) -> str:
        attributes_dict = self.get_attribute_dict()
        return attributes_dict.get(CustomAttributes.HASH, "")

    def update_attribute(
            self, attribute_key: str, attribute_value: Any, change_origin_id: Optional[int],
            previous_breadcrumbs: List[BreadcrumbMetadata], attribute_at_dest: Optional[str]
    ) -> None:
        if self._should_add_previous_breadcrumbs(change_origin_id, previous_breadcrumbs, attribute_at_dest):
            previous_breadcrumbs.append(BreadcrumbMetadata(change_origin_id, attribute_at_dest))

        attribute_key_parts = attribute_key.split(".")
        if len(attribute_key_parts) == 1:
            self.attributes[attribute_key] = attribute_value
            if self._should_set_changed_attributes(change_origin_id, attribute_at_dest):
                self.changed_attributes[attribute_key] = previous_breadcrumbs
            return
        for i in range(len(attribute_key_parts)):
            key = join_trimmed_strings(char_to_join=".", str_lst=attribute_key_parts, num_to_trim=i)
            if key.find(".") > -1:
                self.attributes[key] = attribute_value
                attribute_value = {attribute_key_parts[len(attribute_key_parts) - 1 - i]: attribute_value}
                if self._should_set_changed_attributes(change_origin_id, attribute_at_dest):
                    self.changed_attributes[key] = previous_breadcrumbs

    @staticmethod
    def _should_add_previous_breadcrumbs(change_origin_id: Optional[int],
            previous_breadcrumbs: List[BreadcrumbMetadata], attribute_at_dest: Optional[str]):
        return not previous_breadcrumbs or previous_breadcrumbs[-1].vertex_id != change_origin_id

    @staticmethod
    def _should_set_changed_attributes(change_origin_id: Optional[int], attribute_at_dest: Optional[str]):
        return True

    def get_export_data(self) -> Dict[str, Union[bool, str]]:
        return {"type": self.block_type, "name": self.name, "path": self.path}

    def get_base_attributes(self) -> Dict[str, Union[str, List[str], Dict[str, Any]]]:
        return {
            CustomAttributes.BLOCK_NAME: self.name,
            CustomAttributes.BLOCK_TYPE: self.block_type,
            CustomAttributes.FILE_PATH: self.path,
            CustomAttributes.CONFIG: self.config,
            CustomAttributes.LABEL: str(self),
            CustomAttributes.ID: self.id,
            CustomAttributes.SOURCE: self.source,
        }


def get_inner_attributes(attribute_key: str, attribute_value: Union[str, List[str], Dict[str, Any]]) -> Dict[str, Any]:
    inner_attributes: Dict[str, Any] = {}
    if isinstance(attribute_value, list) and len(attribute_value) == 1:
        attribute_value = attribute_value[0]

    if isinstance(attribute_value, (dict, list)):
        inner_attributes[attribute_key] = [None] * len(attribute_value) if isinstance(attribute_value, list) else {}
        iterator: Union[range, List[str]] = range(len(attribute_value)) if isinstance(attribute_value, list) else list(
            attribute_value.keys())
        for key in iterator:
            if key != "":
                inner_key = f"{attribute_key}.{key}"
                inner_value = attribute_value[key]
                inner_attributes.update(get_inner_attributes(inner_key, inner_value))
                inner_attributes[attribute_key][key] = inner_attributes[inner_key]
            else:
                del attribute_value[key]
    else:
        inner_attributes[attribute_key] = attribute_value
    return inner_attributes
