"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""
from cfnlint.helpers import PSEUDOPARAMS, VALID_PARAMETER_TYPES_LIST
from cfnlint.rules import CloudFormationLintRule, RuleMatch


class Sub(CloudFormationLintRule):
    """Check if Sub values are correct"""

    id = "E1019"
    shortdesc = "Sub validation of parameters"
    description = "Making sure the sub function is properly configured"
    source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html"
    tags = ["functions", "sub"]

    def _test_string(self, cfn, sub_string, parameters, tree):
        """Test if a string has appropriate parameters"""

        matches = []
        string_params = cfn.get_sub_parameters(sub_string)

        for string_param in string_params:
            if isinstance(string_param, (str)):
                matches.extend(
                    self._test_parameter(string_param, cfn, parameters, tree)
                )

        return matches

    def _get_parameters(self, cfn):
        """Get all Parameter Names"""
        results = {}
        parameters = cfn.template.get("Parameters", {})
        if isinstance(parameters, dict):
            for param_name, param_values in parameters.items():
                # This rule isn't here to check the Types but we need
                # something valid if it doesn't exist
                if isinstance(param_values, dict):
                    results[param_name] = param_values.get("Type", "String")

        return results

    def _test_parameters(self, parameters, cfn, tree):
        """Check parameters for appropriate configuration"""

        supported_functions = [
            "Fn::Base64",
            "Fn::FindInMap",
            "Fn::GetAZs",
            "Fn::GetAtt",
            "Fn::If",
            "Fn::ImportValue",
            "Fn::Join",
            "Fn::Select",
            "Fn::Sub",
            "Ref",
            "Fn::ToJsonString",
        ]

        matches = []
        for parameter_name, parameter_value_obj in parameters.items():
            param_tree = tree[:] + [parameter_name]
            if isinstance(parameter_value_obj, dict):
                if len(parameter_value_obj) == 1:
                    for key, value in parameter_value_obj.items():
                        if key not in supported_functions:
                            message = (
                                "Sub parameter should use a valid function for {0}"
                            )
                            matches.append(
                                RuleMatch(
                                    param_tree, message.format("/".join(map(str, tree)))
                                )
                            )
                        elif key in ["Ref"]:
                            matches.extend(self._test_parameter(value, cfn, {}, tree))
                        elif key in ["Fn::GetAtt"]:
                            if isinstance(value, list):
                                # Only test this if all the items are a string
                                if_all_strings = True
                                for v in value:
                                    if not isinstance(v, str):
                                        # skip things got too complex
                                        if_all_strings = False
                                if if_all_strings:
                                    matches.extend(
                                        self._test_parameter(
                                            ".".join(value), cfn, {}, tree
                                        )
                                    )
                            elif isinstance(value, str):
                                matches.extend(
                                    self._test_parameter(value, cfn, {}, tree)
                                )
                else:
                    message = "Sub parameter should be an object of 1 for {0}"
                    matches.append(
                        RuleMatch(param_tree, message.format("/".join(map(str, tree))))
                    )
            elif isinstance(parameter_value_obj, list):
                message = "Sub parameter value should be a string for {0}"
                matches.append(
                    RuleMatch(param_tree, message.format("/".join(map(str, tree))))
                )

        return matches

    def _test_parameter(self, parameter, cfn, parameters, tree):
        """Test a parameter"""

        matches = []
        get_atts = cfn.get_valid_getatts()

        valid_params = list(PSEUDOPARAMS)
        valid_params.extend(cfn.get_resource_names())
        template_parameters = self._get_parameters(cfn)

        for key, _ in parameters.items():
            valid_params.append(key)

        if parameter not in valid_params:
            found = False
            if parameter in template_parameters:
                found = True
                if template_parameters.get(parameter) in VALID_PARAMETER_TYPES_LIST:
                    message = "Fn::Sub cannot use list {0} at {1}"
                    matches.append(
                        RuleMatch(
                            tree, message.format(parameter, "/".join(map(str, tree)))
                        )
                    )
            for resource, attributes in get_atts.items():
                for attribute_name, attribute_values in attributes.items():
                    if resource == parameter.split(".")[0]:
                        if attribute_name == "*":
                            found = True
                        elif attribute_name == ".".join(parameter.split(".")[1:]):
                            if attribute_values.get("Type") == "List":
                                message = "Fn::Sub cannot use list {0} at {1}"
                                matches.append(
                                    RuleMatch(
                                        tree,
                                        message.format(
                                            parameter, "/".join(map(str, tree))
                                        ),
                                    )
                                )
                            found = True
                        else:
                            if (
                                attribute_name == parameter.split(".")[1]
                                and attribute_values.get("Type") == "Map"
                            ):
                                found = True

            if not found:
                message = "Parameter {0} for Fn::Sub not found at {1}"
                matches.append(
                    RuleMatch(tree, message.format(parameter, "/".join(map(str, tree))))
                )

        return matches

    def match(self, cfn):
        matches = []

        sub_objs = cfn.search_deep_keys("Fn::Sub")

        for sub_obj in sub_objs:
            sub_value_obj = sub_obj[-1]
            tree = sub_obj[:-1]
            if isinstance(sub_value_obj, str):
                matches.extend(self._test_string(cfn, sub_value_obj, {}, tree))
            elif isinstance(sub_value_obj, list):
                if len(sub_value_obj) == 2:
                    sub_string = sub_value_obj[0]
                    parameters = sub_value_obj[1]
                    if not isinstance(sub_string, str):
                        message = "Subs first element should be of type string for {0}"
                        matches.append(
                            RuleMatch(
                                tree + [0], message.format("/".join(map(str, tree)))
                            )
                        )
                    if not isinstance(parameters, dict):
                        message = "Subs second element should be an object for {0}"
                        matches.append(
                            RuleMatch(
                                tree + [1], message.format("/".join(map(str, tree)))
                            )
                        )
                    else:
                        matches.extend(
                            self._test_string(cfn, sub_string, parameters, tree + [0])
                        )
                        matches.extend(self._test_parameters(parameters, cfn, tree))
                else:
                    message = "Sub should be an array of 2 for {0}"
                    matches.append(
                        RuleMatch(tree, message.format("/".join(map(str, tree))))
                    )
            elif isinstance(sub_value_obj, dict):
                if len(sub_value_obj) == 1:
                    for key, _ in sub_value_obj.items():
                        if not key == "Fn::Transform":
                            message = (
                                "Sub should be a string or array of 2 items for {0}"
                            )
                            matches.append(
                                RuleMatch(
                                    tree, message.format("/".join(map(str, tree)))
                                )
                            )
                else:
                    message = "Sub should be a string or array of 2 items for {0}"
                    matches.append(
                        RuleMatch(tree, message.format("/".join(map(str, tree))))
                    )
            else:
                message = "Sub should be a string or array of 2 items for {0}"
                matches.append(
                    RuleMatch(tree, message.format("/".join(map(str, tree))))
                )

        return matches
