"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""
import regex as re

from cfnlint.helpers import REGEX_CIDR
from cfnlint.rules import CloudFormationLintRule, RuleMatch


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

    id = "E1024"
    shortdesc = "Cidr validation of parameters"
    description = "Making sure the function CIDR is a list with valid values"
    source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-cidr.html"
    tags = ["functions", "cidr"]

    supported_functions = [
        "Fn::FindInMap",
        "Fn::Select",
        "Ref",
        "Fn::GetAtt",
        "Fn::Sub",
        "Fn::ImportValue",
    ]

    def check_ip_block(self, value, path):
        matches = []
        if isinstance(value, dict):
            if len(value) == 1:
                for index_key, _ in value.items():
                    if index_key not in self.supported_functions:
                        if index_key == "Fn::If":
                            if len(value.get("Fn::If")) == 3 and isinstance(
                                value.get("Fn::If"), list
                            ):
                                matches.extend(
                                    self.check_ip_block(
                                        value.get("Fn::If")[1],
                                        path=path[:] + [index_key, 1],
                                    )
                                )
                                matches.extend(
                                    self.check_ip_block(
                                        value.get("Fn::If")[2],
                                        path=path[:] + [index_key, 2],
                                    )
                                )
                        else:
                            message = (
                                "Cidr ipBlock should be Cidr Range, Ref, GetAtt, Sub or"
                                " Select for {0}"
                            )
                            matches.append(
                                RuleMatch(
                                    path, message.format("/".join(map(str, value)))
                                )
                            )
        elif isinstance(value, (str)):
            if not re.match(REGEX_CIDR, value):
                message = "Cidr ipBlock should be a Cidr Range based string for {0}"
                matches.append(
                    RuleMatch(path, message.format("/".join(map(str, path))))
                )
        else:
            message = "Cidr ipBlock should be a string for {0}"
            matches.append(RuleMatch(path, message.format("/".join(map(str, path)))))

        return matches

    def check_count(self, value, path):
        matches = []
        count_parameters = []
        if isinstance(value, dict):
            if len(value) == 1:
                for index_key, index_value in value.items():
                    if index_key not in self.supported_functions:
                        if index_key == "Fn::If":
                            if len(value.get("Fn::If")) == 3 and isinstance(
                                value.get("Fn::If"), list
                            ):
                                for i in [1, 2]:
                                    (
                                        new_count_parameters,
                                        new_matches,
                                    ) = self.check_count(
                                        value.get("Fn::If")[i],
                                        path=path[:] + [index_key, i],
                                    )
                                    count_parameters.extend(new_count_parameters)
                                    matches.extend(new_matches)
                        else:
                            message = "Cidr count should be Int, Ref, or Select for {0}"
                            matches.append(
                                RuleMatch(
                                    path, message.format("/".join(map(str, path)))
                                )
                            )
                    if index_key == "Ref":
                        count_parameters.append(index_value)
        elif not isinstance(value, int):
            message = "Cidr count should be a int for {0}"
            extra_args = {
                "actual_type": type(value).__name__,
                "expected_type": int.__name__,
            }
            matches.append(
                RuleMatch(path, message.format("/".join(map(str, path))), **extra_args)
            )

        return count_parameters, matches

    def check_size_mask(self, value, path):
        matches = []
        size_mask_parameters = []
        if isinstance(value, dict):
            if len(value) == 1:
                for index_key, index_value in value.items():
                    if index_key not in self.supported_functions:
                        if index_key == "Fn::If":
                            if len(value.get("Fn::If")) == 3 and isinstance(
                                value.get("Fn::If"), list
                            ):
                                matches.extend(
                                    self.check_size_mask(
                                        value.get("Fn::If")[1],
                                        path=path[:] + [index_key, 1],
                                    )
                                )
                                matches.extend(
                                    self.check_size_mask(
                                        value.get("Fn::If")[2],
                                        path=path[:] + [index_key, 2],
                                    )
                                )
                        else:
                            message = (
                                "Cidr sizeMask should be Int, Ref, or Select for {0}"
                            )
                            matches.append(
                                RuleMatch(
                                    path, message.format("/".join(map(str, path)))
                                )
                            )
                    if index_key == "Ref":
                        size_mask_parameters.append(index_value)
        elif not isinstance(value, int):
            message = "Cidr sizeMask should be a int for {0}"
            extra_args = {
                "actual_type": type(value).__name__,
                "expected_type": int.__name__,
            }
            matches.append(
                RuleMatch(path, message.format("/".join(map(str, path))), **extra_args)
            )

        return size_mask_parameters, matches

    def check_parameter_count(self, cfn, parameter_name):
        """Check Count Parameter if used"""
        matches = []
        parameter_obj = cfn.get_parameters().get(parameter_name, {})
        if parameter_obj:
            tree = ["Parameters", parameter_name]
            parameter_type = parameter_obj.get("Type")
            if parameter_type == "Number":
                max_value = parameter_obj.get("MaxValue")
                min_value = parameter_obj.get("MinValue")
                if (not min_value) or min_value < 1 or min_value > 256:
                    message = (
                        "Parameter for Cidr count have MinValue between 1 and 256"
                        " at {0}"
                    )
                    matches.append(
                        RuleMatch(
                            tree + ["MinValue"],
                            message.format("/".join(map(str, tree + ["MinValue"]))),
                        )
                    )
                if (not max_value) or max_value < 1 or max_value > 256:
                    message = (
                        "Parameter for Cidr count have MaxValue between 1 and 256"
                        " at {0}"
                    )
                    matches.append(
                        RuleMatch(
                            tree + ["MaxValue"],
                            message.format("/".join(map(str, tree + ["MaxValue"]))),
                        )
                    )
            else:
                message = "Parameter for Cidr count have be of Type Number at {0}"
                matches.append(
                    RuleMatch(tree, message.format("/".join(map(str, tree))))
                )

        return matches

    def check_parameter_size_mask(self, cfn, parameter_name):
        """Check SizeMask Parameter if used"""
        matches = []
        parameter_obj = cfn.get_parameters().get(parameter_name, {})
        if parameter_obj:
            tree = ["Parameters", parameter_name]
            parameter_type = parameter_obj.get("Type")
            if parameter_type == "Number":
                max_value = parameter_obj.get("MaxValue")
                min_value = parameter_obj.get("MinValue")
                if (not min_value) or min_value < 1 or min_value > 256:
                    message = (
                        "Parameter for Cidr sizeMask have MinValue between 1 and "
                        "128 (for ipv6) and 32 (for ipv4) at {0}"
                    )
                    matches.append(
                        RuleMatch(
                            tree + ["MinValue"],
                            message.format("/".join(map(str, tree + ["MinValue"]))),
                        )
                    )
                if (not max_value) or max_value < 1 or max_value > 256:
                    message = (
                        "Parameter for Cidr count have MaxValue between 1 and "
                        "128 (for ipv6) and 32 (for ipv4) at {0}"
                    )
                    matches.append(
                        RuleMatch(
                            tree + ["MaxValue"],
                            message.format("/".join(map(str, tree + ["MaxValue"]))),
                        )
                    )
            else:
                message = "Parameter for Cidr count have be of Type Number at {0}"
                matches.append(
                    RuleMatch(tree, message.format("/".join(map(str, tree))))
                )

        return matches

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

        cidr_objs = cfn.search_deep_keys("Fn::Cidr")

        count_parameters = []
        size_mask_parameters = []

        for cidr_obj in cidr_objs:
            cidr_value_obj = cidr_obj[-1]
            tree = cidr_obj[:-1]
            if isinstance(cidr_value_obj, list):
                if len(cidr_value_obj) in [2, 3]:
                    ip_block_obj = cidr_value_obj[0]
                    count_obj = cidr_value_obj[1]
                    if len(cidr_value_obj) == 3:
                        size_mask_obj = cidr_value_obj[2]
                    else:
                        size_mask_obj = None

                    matches.extend(self.check_ip_block(ip_block_obj, tree[:] + [0]))

                    new_count_parameters, new_matches = self.check_count(
                        count_obj, tree[:] + [1]
                    )
                    count_parameters.extend(new_count_parameters)
                    matches.extend(new_matches)
                    new_size_mask_parameters, new_matches = self.check_size_mask(
                        size_mask_obj, tree[:] + [2]
                    )
                    size_mask_parameters.extend(new_size_mask_parameters)
                    matches.extend(new_matches)

                else:
                    message = "Cidr should be a list of 2 or 3 elements for {0}"
                    matches.append(
                        RuleMatch(tree, message.format("/".join(map(str, tree))))
                    )
            else:
                message = "Cidr should be a list of 2 or 3 elements for {0}"
                matches.append(
                    RuleMatch(tree, message.format("/".join(map(str, tree))))
                )

        for count_parameter in set(count_parameters):
            matches.extend(self.check_parameter_count(cfn, count_parameter))
        for size_mask_parameter in set(size_mask_parameters):
            matches.extend(self.check_parameter_size_mask(cfn, size_mask_parameter))

        return matches
