"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""
import hashlib
import json
import logging
from copy import copy

import cfnlint.helpers

LOGGER = logging.getLogger(__name__)


class ConditionParseError(Exception):
    """
    Error thrown when the template has poorly formatted condition
    """


def get_hash(obj):
    """Return a hash of an object"""
    return hashlib.sha1(json.dumps(obj, sort_keys=True).encode("utf-8")).hexdigest()


class EqualsValue:
    """holds the values of a equals"""

    Function = None
    String = None

    def __init__(self, value):
        if isinstance(value, dict):
            if len(value) == 1:
                # Save hashes of the dict for consistency and sorting
                self.Function = get_hash(value)
            else:
                LOGGER.debug("Length of the object needs to be 1")
                raise ConditionParseError
        elif isinstance(value, str):
            self.String = value
        elif isinstance(value, int):
            self.String = str(value)
        else:
            LOGGER.debug("Equals value has to be string or object")
            raise ConditionParseError

    def __eq__(self, other):
        return other in [self.Function, self.String]


class Equals:
    """Equals operator"""

    Left = None
    Right = None

    def __init__(self, equals):
        if isinstance(equals, list):
            if len(equals) == 2:
                self.Left = EqualsValue(equals[0])
                self.Right = EqualsValue(equals[1])
            else:
                LOGGER.debug("Length of Equals needs to be 2")
                raise ConditionParseError
        else:
            LOGGER.debug("Equals needs to be a list")
            raise ConditionParseError

    def test(self, scenarios):
        """Do an equals based on the provided scenario"""
        for scenario, value in scenarios.items():
            if scenario == self.Left:
                return value == self.Right
            if scenario == self.Right:
                return value == self.Left

        return None


class Condition:
    """Individual condition"""

    And = None
    Or = None
    Not = None
    Equals = None
    Influenced_Equals = None
    Name = None

    def __init__(self, template, name=None, sub_condition=None):
        self.And = []
        self.Or = []
        self.Not = []
        self.Influenced_Equals = {}
        self.Name = name
        if name is not None:
            value = template.get("Conditions", {}).get(name, {})
            try:
                self.process_condition(template, value)
            except ConditionParseError:
                LOGGER.debug("Error parsing condition: %s", name)
                self.Equals = None
        elif sub_condition is not None:
            try:
                self.process_condition(template, sub_condition)
            except ConditionParseError:
                LOGGER.debug("Error parsing condition: %s", name)
                self.Equals = None

    def __eq__(self, __o: object) -> bool:
        if self.Name is None:
            return False
        return self.Name == __o.Name

    def test(self, scenarios):
        """Test a condition based on a scenario"""
        if self.And:
            for a in self.And:
                if not a.test(scenarios):
                    return False
            return True
        if self.Or:
            for o in self.Or:
                if o.test(scenarios):
                    return True
            return False
        if self.Not:
            for n in self.Not:
                return not n.test(scenarios)
        return self.Equals.test(scenarios)

    def process_influenced_equal(self, equal):
        """Get influenced equals from sub conditions"""
        if equal.Left.Function:
            if not self.Influenced_Equals.get(equal.Left.Function):
                self.Influenced_Equals[equal.Left.Function] = set()
            if equal.Right.Function:
                self.Influenced_Equals[equal.Left.Function].add(equal.Right.Function)
            elif equal.Right.String is not None:
                self.Influenced_Equals[equal.Left.Function].add(equal.Right.String)
        if equal.Right.Function:
            if not self.Influenced_Equals.get(equal.Right.Function):
                self.Influenced_Equals[equal.Right.Function] = set()
            if equal.Left.Function:
                self.Influenced_Equals[equal.Right.Function].add(equal.Left.Function)
            elif equal.Left.String is not None:
                self.Influenced_Equals[equal.Right.Function].add(equal.Left.String)

    def process_condition(self, template, value):
        if isinstance(value, dict):
            if len(value) == 1:
                for func_name, func_value in value.items():
                    if func_name == cfnlint.helpers.FUNCTION_AND:
                        self.And = self.process_function(template, func_value)
                    elif func_name == cfnlint.helpers.FUNCTION_OR:
                        self.Or = self.process_function(template, func_value)
                    elif func_name == cfnlint.helpers.FUNCTION_NOT:
                        self.Not = self.process_function(template, func_value)
                    elif func_name == cfnlint.helpers.FUNCTION_EQUALS:
                        equal = Equals(func_value)
                        self.process_influenced_equal(equal)
                        self.Equals = equal
                    elif func_name == "Condition":
                        value = template.get("Conditions", {}).get(func_value, {})
                        try:
                            self.process_condition(template, value)
                        except ConditionParseError:
                            LOGGER.debug("Error parsing condition: %s", func_value)
            else:
                LOGGER.debug("Length of the object must be 1")
                raise ConditionParseError
        else:
            LOGGER.debug("Condition has to be an object")
            raise ConditionParseError

    def process_function(self, template, values):
        results = []
        for value in values:
            if isinstance(value, dict):
                if len(value) == 1:
                    for k, v in value.items():
                        if k == cfnlint.helpers.FUNCTION_EQUALS:
                            equal = Equals(v)
                            self.process_influenced_equal(equal)
                            results.append(equal)
                        elif k == "Condition":
                            condition = Condition(template, v)
                            results.append(condition)
                            for i_e_k, i_e_v in condition.Influenced_Equals.items():
                                if not self.Influenced_Equals.get(i_e_k):
                                    self.Influenced_Equals[i_e_k] = set()
                                for s_v in i_e_v:
                                    self.Influenced_Equals[i_e_k].add(s_v)
                        else:
                            condition = Condition(template, None, value)
                            results.append(condition)
                            for i_e_k, i_e_v in condition.Influenced_Equals.items():
                                if not self.Influenced_Equals.get(i_e_k):
                                    self.Influenced_Equals[i_e_k] = set()
                                for s_v in i_e_v:
                                    self.Influenced_Equals[i_e_k].add(s_v)

        return results


class Conditions:
    """All the conditions"""

    Conditions = None
    Equals = None
    Parameters = None

    def __init__(self, cfn):
        self.Conditions = {}
        self.Equals = {}
        self.Parameters = {}
        try:
            self.Equals = self._get_condition_equals(
                cfn.search_deep_keys(cfnlint.helpers.FUNCTION_EQUALS)
            )
            for condition_name in cfn.template.get("Conditions", {}):
                self.Conditions[condition_name] = Condition(
                    cfn.template, condition_name
                )
            # Configure parameters Allowed Values if they have them
            for parameter_name, parameter_values in cfn.template.get(
                "Parameters", {}
            ).items():
                # ALlowed Values must be a list so validate they are
                if isinstance(parameter_values.get("AllowedValues"), list):
                    # Any parameter in a condition could be used but would have to be done by
                    # Ref so build a ref to match for getting an equivalent hash
                    self.Parameters[
                        get_hash({"Ref": parameter_name})
                    ] = parameter_values.get("AllowedValues")
        except Exception as err:  # pylint: disable=W0703
            LOGGER.debug("While processing conditions got error: %s", err)

    def _get_condition_equals(self, equals):
        """
        Get Conditions based on Condition definitions
        Input: Cfn (Template)
        Output: List of hashes of Equal dict objects (Ref or FindInMap)
        """
        results = {}

        for equal in equals:
            if equal[0] == "Conditions":
                condition_name = equal[1]
                equals = equal[-1]
                if isinstance(equals, list):
                    if len(equals) == 2:
                        dict_hash_1 = None
                        value_1 = None
                        dict_hash_2 = None
                        value_2 = None
                        if isinstance(equals[0], dict):
                            dict_hash_1 = get_hash(equals[0])
                        elif isinstance(equals[0], str):
                            value_1 = equals[0]
                        elif isinstance(equals[0], int):
                            value_1 = str(equals[0])
                        if isinstance(equals[1], dict):
                            dict_hash_2 = get_hash(equals[1])
                        elif isinstance(equals[1], str):
                            value_2 = equals[1]
                        elif isinstance(equals[1], int):
                            value_2 = str(equals[1])

                        if dict_hash_1:
                            if dict_hash_1 not in results:
                                results[dict_hash_1] = []
                            if dict_hash_2:
                                results[dict_hash_1].append(
                                    {
                                        "Condition": condition_name,
                                        "Type": "dict",
                                        "Value": dict_hash_2,
                                    }
                                )
                            else:
                                results[dict_hash_1].append(
                                    {
                                        "Condition": condition_name,
                                        "Type": "string",
                                        "Value": value_2,
                                    }
                                )
                        if dict_hash_2:
                            if dict_hash_2 not in results:
                                results[dict_hash_2] = []
                            if dict_hash_1:
                                results[dict_hash_2].append(
                                    {
                                        "Condition": condition_name,
                                        "Type": "dict",
                                        "Value": dict_hash_1,
                                    }
                                )
                            else:
                                results[dict_hash_2].append(
                                    {
                                        "Condition": condition_name,
                                        "Type": "string",
                                        "Value": value_1,
                                    }
                                )
        return results

    def multiply_conditions(self, currents, condition, values):
        """Build out scenarios for when conditions don't match"""
        results = []
        if not currents:
            for value in values:
                new = {}
                new[condition] = value
                results.append(new)
        for current in currents:
            for value in values:
                new = copy(current)
                new[condition] = value
                results.append(new)

        return results

    def get_scenarios(self, conditions):
        """Get scenarios for all conditions provided"""
        matched_equals = {}
        matched_conditions = set()
        all_equals = {}
        results = []

        # When conditions don't properly get loaded (configuration error)
        # lets just return an empty list
        if not self.Conditions:
            return results

        for condition in conditions:
            # When one of the conditions don't exist we return an empty result
            if not self.Conditions.get(condition):
                return []

            for equal_key, equal_values in self.Conditions.get(
                condition
            ).Influenced_Equals.items():
                if not equal_key in all_equals:
                    all_equals[equal_key] = condition
                else:
                    if all_equals[equal_key] not in matched_conditions:
                        matched_conditions.add(all_equals[equal_key])
                    matched_conditions.add(condition)

        all_equals = {}
        for condition in matched_conditions:
            for equal_key, equal_values in self.Conditions.get(
                condition
            ).Influenced_Equals.items():
                if not equal_key in all_equals:
                    all_equals[equal_key] = set()
                else:
                    if not equal_key in matched_equals:
                        matched_equals[equal_key] = copy(all_equals[equal_key])
                for s_v in equal_values:
                    all_equals[equal_key].add(s_v)
                    if equal_key in matched_equals:
                        matched_equals[equal_key].add(s_v)

        diff = set(all_equals.keys()) - set(matched_equals.keys())
        if diff:
            diffKey = diff.pop()
            matched_equals[diffKey] = all_equals[diffKey]

        def multiply_equals(currents, s_hash, sets, parameter_values):
            """Multiply Equals when building scenarios"""
            results = []
            false_case = ""
            if not currents:
                # If the Parameter being REFed has Allowed Values use those instead
                if parameter_values:
                    for p_value in parameter_values:
                        # the allowed value must be an integer or string
                        # protecting against really badly formatted templates
                        if isinstance(p_value, (int, str)):
                            new = {}
                            # the allowed values could be numbers so force a string
                            new[s_hash] = str(p_value)
                            results.append(new)
                else:
                    for s_set in sets:
                        new = {}
                        new[s_hash] = s_set
                        false_case += s_set
                        results.append(new)
                    new = {}
                    new[s_hash] = false_case + ".bad"
                    results.append(new)
            for current in currents:
                # If the Parameter being REFed has Allowed Values use those instead
                if parameter_values:
                    for p_value in parameter_values:
                        # the allowed value must be an integer or string
                        # protecting against really badly formatted templates
                        if isinstance(p_value, (int, str)):
                            new = copy(current)
                            # the allowed values could be numbers so force a string
                            new[s_hash] = str(p_value)
                            results.append(new)
                else:
                    for s_set in sets:
                        new = copy(current)
                        new[s_hash] = s_set
                        false_case += s_set
                        results.append(new)
                    new = copy(current)
                    new[s_hash] = false_case + ".bad"
                    results.append(new)

            return results

        if not matched_conditions:
            # fail safe to not create a lot of unrelated scenarios. Just test if they are true/false
            # At this point this value is completely arbitrary and not configurable
            if len(conditions) > 4:
                LOGGER.info(
                    "Found %s conditions.  Limiting results to protect against heavy cpu time",
                    len(conditions),
                )
                true_results = []
                false_results = []
                for condition in conditions:
                    true_results = self.multiply_conditions(
                        true_results, condition, [True]
                    )
                    false_results = self.multiply_conditions(
                        false_results, condition, [False]
                    )
                results.extend(true_results)
                results.extend(false_results)
            else:
                for condition in conditions:
                    results = self.multiply_conditions(
                        results, condition, [True, False]
                    )

            return results

        if matched_conditions:
            scenarios = []
            for con_hash, sets in matched_equals.items():
                if len(scenarios) < 20:
                    scenarios = multiply_equals(
                        scenarios, con_hash, sets, self.Parameters.get(con_hash)
                    )

        for scenario in scenarios:
            r_condition = {}
            for condition in conditions:
                r_condition[condition] = self.Conditions.get(condition).test(scenario)

            if r_condition not in results:
                results.append(r_condition)

        return results
