"""
Process and simplifies CloudFormation intrinsic properties such as FN::* and Ref
"""

import base64
import copy
import logging
import re
from collections import OrderedDict

from samcli.commands._utils.template import get_template_data
from samcli.lib.intrinsic_resolver.invalid_intrinsic_exception import InvalidIntrinsicException, InvalidSymbolException
from samcli.lib.intrinsic_resolver.invalid_intrinsic_validation import (
    verify_all_list_intrinsic_type,
    verify_in_bounds,
    verify_intrinsic_type_bool,
    verify_intrinsic_type_dict,
    verify_intrinsic_type_int,
    verify_intrinsic_type_list,
    verify_intrinsic_type_str,
    verify_non_null,
    verify_number_arguments,
)

LOG = logging.getLogger(__name__)


class IntrinsicResolver:
    AWS_INCLUDE = "AWS::Include"
    SUPPORTED_MACRO_TRANSFORMATIONS = [AWS_INCLUDE]
    _PSEUDO_REGEX = r"AWS::.*?"
    _ATTRIBUTE_REGEX = r"[a-zA-Z0-9]*?\.?[a-zA-Z0-9]*?"
    _REGEX_SUB_FUNCTION = r"\$\{(" + _PSEUDO_REGEX + "||" + _ATTRIBUTE_REGEX + r")\}"

    FN_JOIN = "Fn::Join"
    FN_SPLIT = "Fn::Split"
    FN_SUB = "Fn::Sub"
    FN_SELECT = "Fn::Select"
    FN_BASE64 = "Fn::Base64"
    FN_FIND_IN_MAP = "Fn::FindInMap"
    FN_TRANSFORM = "Fn::Transform"
    FN_GET_AZS = "Fn::GetAZs"
    REF = "Ref"
    FN_GET_ATT = "Fn::GetAtt"
    FN_IMPORT_VALUE = "Fn::ImportValue"

    SUPPORTED_INTRINSIC_FUNCTIONS = [
        FN_JOIN,
        FN_SPLIT,
        FN_SUB,
        FN_SELECT,
        FN_BASE64,
        FN_FIND_IN_MAP,
        FN_TRANSFORM,
        FN_GET_AZS,
        REF,
        FN_GET_ATT,
        FN_IMPORT_VALUE,
    ]

    FN_AND = "Fn::And"
    FN_OR = "Fn::Or"
    FN_IF = "Fn::If"
    FN_EQUALS = "Fn::Equals"
    FN_NOT = "Fn::Not"

    CONDITIONAL_FUNCTIONS = [FN_AND, FN_OR, FN_IF, FN_EQUALS, FN_NOT]

    def __init__(self, template, symbol_resolver):
        """
        Initializes the Intrinsic Property class with the default intrinsic_key_function_map and
        conditional_key_function_map.

        In the future, for items like Fn::ImportValue multiple templates can be provided
        into the function.
        """
        self._template = None
        self._resources = None
        self._mapping = None
        self._parameters = None
        self._conditions = None
        self._outputs = None
        self.init_template(template)

        self._symbol_resolver = symbol_resolver

        self.intrinsic_key_function_map = self.default_intrinsic_function_map()
        self.conditional_key_function_map = self.default_conditional_key_map()

    def init_template(self, template):
        self._template = copy.deepcopy(template or {})
        self._resources = self._template.get("Resources", {})
        self._mapping = self._template.get("Mappings", {})
        self._parameters = self._template.get("Parameters", {})
        self._conditions = self._template.get("Conditions", {})
        self._outputs = self._template.get("Outputs", {})

    def default_intrinsic_function_map(self):
        """
        Returns a dictionary containing the mapping from
            Intrinsic Function Key -> Intrinsic Resolver.
        The intrinsic_resolver function has the format lambda intrinsic: some_retun_value

        Return
        -------
        A dictionary containing the mapping from Intrinsic Function Key -> Intrinsic Resolver
        """
        return {
            IntrinsicResolver.FN_JOIN: self.handle_fn_join,
            IntrinsicResolver.FN_SPLIT: self.handle_fn_split,
            IntrinsicResolver.FN_SUB: self.handle_fn_sub,
            IntrinsicResolver.FN_SELECT: self.handle_fn_select,
            IntrinsicResolver.FN_BASE64: self.handle_fn_base64,
            IntrinsicResolver.FN_FIND_IN_MAP: self.handle_find_in_map,
            IntrinsicResolver.FN_TRANSFORM: self.handle_fn_transform,
            IntrinsicResolver.FN_GET_AZS: self.handle_fn_get_azs,
            IntrinsicResolver.REF: self.handle_fn_ref,
            IntrinsicResolver.FN_GET_ATT: self.handle_fn_getatt,
            IntrinsicResolver.FN_IMPORT_VALUE: self.handle_fn_import_value,
        }

    def default_conditional_key_map(self):
        """
        Returns a dictionary containing the mapping from Conditional
            Conditional Intrinsic Function Key -> Conditional Intrinsic Resolver.
        The intrinsic_resolver function has the format lambda intrinsic: some_retun_value

        The code was split between conditionals and other intrinsic keys for readability purposes.
        Return
        -------
        A dictionary containing the mapping from Intrinsic Function Key -> Intrinsic Resolver
        """
        return {
            IntrinsicResolver.FN_AND: self.handle_fn_and,
            IntrinsicResolver.FN_OR: self.handle_fn_or,
            IntrinsicResolver.FN_IF: self.handle_fn_if,
            IntrinsicResolver.FN_EQUALS: self.handle_fn_equals,
            IntrinsicResolver.FN_NOT: self.handle_fn_not,
        }

    def set_intrinsic_key_function_map(self, function_map):
        """
        Sets the mapping from
            Conditional Intrinsic Function Key -> Conditional Intrinsic Resolver.
        The intrinsic_resolver function has the format lambda intrinsic: some_retun_value

        A user of this function can set the function map directly or can get the default_conditional_key_map directly.


        """
        self.intrinsic_key_function_map = function_map

    def set_conditional_function_map(self, function_map):
        """
        Sets the mapping from
            Conditional Intrinsic Function Key -> Conditional Intrinsic Resolver.
        The intrinsic_resolver function has the format lambda intrinsic: some_retun_value

        A user of this function can set the function map directly or can get the default_intrinsic_function_map directly

        The code was split between conditionals and other intrinsic keys for readability purposes.

        """
        self.conditional_key_function_map = function_map

    def intrinsic_property_resolver(self, intrinsic, ignore_errors, parent_function="template"):
        """
        This resolves the intrinsic of the format
        {
            intrinsic: dict
        } by calling the function with the relevant intrinsic function resolver.

        This also supports returning a string, list, boolean, int since they may be intermediate steps in the recursion
        process. No transformations are done on these.

        By default this will just return the item if non of the types match. This is because of the function
        resolve_all_attributes which will recreate the resources by processing every aspect of resource.

        This code resolves in a top down depth first fashion in order to create a functional style recursion that
        doesn't mutate any of the properties.

        Parameters
        ----------
        intrinsic : dict, str, list, bool, int
            This is an intrinsic property or an intermediate step
        ignore_errors : bool
            Whether to ignore errors
        parent_function : str
            In case there is a missing property, this is used to figure out where the property resolved is missing.
        Return
        ---------
        The simplified version of the intrinsic function. This could be a list,str,dict depending on the format required
        """
        if intrinsic is None:
            raise InvalidIntrinsicException("Missing Intrinsic property in {}".format(parent_function))
        if isinstance(intrinsic, list):
            return [self.intrinsic_property_resolver(item, ignore_errors) for item in intrinsic]
        if not isinstance(intrinsic, dict) or intrinsic == {}:
            return intrinsic

        # `intrinsic` is a dict at this point.

        keys = list(intrinsic.keys())
        key = keys[0]

        if key in self.intrinsic_key_function_map:
            intrinsic_value = intrinsic.get(key)
            return self.intrinsic_key_function_map.get(key)(intrinsic_value, ignore_errors)

        if key in self.conditional_key_function_map:
            intrinsic_value = intrinsic.get(key)
            return self.conditional_key_function_map.get(key)(intrinsic_value, ignore_errors)

        # In this case, it is a dictionary that doesn't directly contain an intrinsic resolver, we must recursively
        # resolve each of it's sub properties.
        sanitized_dict = {}
        for key, val in intrinsic.items():
            try:
                sanitized_key = self.intrinsic_property_resolver(key, ignore_errors, parent_function=parent_function)
                sanitized_val = self.intrinsic_property_resolver(val, ignore_errors, parent_function=parent_function)
                verify_intrinsic_type_str(
                    sanitized_key,
                    message="The keys of the dictionary {} in {} must all resolve to a string".format(
                        sanitized_key, parent_function
                    ),
                )
                sanitized_dict[sanitized_key] = sanitized_val
            # On any exception, leave the key:val of the orginal intact and continue on.
            # https://github.com/awslabs/aws-sam-cli/issues/1386
            except Exception:
                if ignore_errors:
                    LOG.debug("Unable to resolve property %s: %s. Leaving as is.", key, val)
                    sanitized_dict[key] = val
                else:
                    raise

        return sanitized_dict

    def resolve_template(self, ignore_errors=False):
        """
        This resolves all the attributes of the CloudFormation dictionary Resources, Outputs, Mappings, Parameters,
        Conditions.

        Return
        -------
        Return a processed template
        """
        processed_template = self._template

        if self._resources:
            processed_template["Resources"] = self.resolve_attribute(self._resources, ignore_errors)
        if self._outputs:
            processed_template["Outputs"] = self.resolve_attribute(self._outputs, ignore_errors)

        return processed_template

    def resolve_attribute(self, cloud_formation_property, ignore_errors=False):
        """
        This will parse through every entry in a CloudFormation root key and resolve them based on the symbol_resolver.
        Customers can optionally ignore resource errors and default to whatever the resource provides.

        Parameters
        -----------
        cloud_formation_property: dict
            A high Level dictionary containg either the Mappings, Resources, Outputs, or Parameters Dictionary
        ignore_errors: bool
            An option to ignore errors that are InvalidIntrinsicException and InvalidSymbolException
        Return
        -------
        A resolved template with all references possible simplified
        """
        processed_dict = OrderedDict()
        for key, val in cloud_formation_property.items():
            processed_key = self._symbol_resolver.get_translation(key) or key
            try:
                processed_resource = self.intrinsic_property_resolver(val, ignore_errors, parent_function=processed_key)
                processed_dict[processed_key] = processed_resource
            except (InvalidIntrinsicException, InvalidSymbolException) as e:
                resource_type = val.get("Type", "")
                if ignore_errors:
                    LOG.error("Unable to process properties of %s.%s", key, resource_type)
                    processed_dict[key] = val
                else:
                    raise InvalidIntrinsicException(
                        "Exception with property of {}.{}".format(key, resource_type) + ": " + str(e.args)
                    ) from e
        return processed_dict

    def handle_fn_join(self, intrinsic_value, ignore_errors):
        """
        { "Fn::Join" : [ "delimiter", [ comma-delimited list of values ] ] }
        This function will join the items in the list together based on the string using the python join.

        This intrinsic function will resolve all the objects within the function's value and check their type.

        Parameter
        ----------
        intrinsic_value: list, dict
            This is the value of the object inside the Fn::Join intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_JOIN
        )

        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_JOIN)

        delimiter = arguments[0]

        verify_intrinsic_type_str(delimiter, IntrinsicResolver.FN_JOIN, position_in_list="first")

        value_list = self.intrinsic_property_resolver(
            arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_JOIN
        )

        verify_intrinsic_type_list(
            value_list,
            IntrinsicResolver.FN_JOIN,
            message="The list of values in {} after the " "delimiter must be a list".format(IntrinsicResolver.FN_JOIN),
        )

        sanitized_value_list = [
            self.intrinsic_property_resolver(item, ignore_errors, parent_function=IntrinsicResolver.FN_JOIN)
            for item in value_list
        ]
        verify_all_list_intrinsic_type(
            sanitized_value_list, verification_func=verify_intrinsic_type_str, property_type=IntrinsicResolver.FN_JOIN
        )

        return delimiter.join(sanitized_value_list)

    def handle_fn_split(self, intrinsic_value, ignore_errors):
        """
        { "Fn::Split" : [ "delimiter", "source string" ] }
        This function will then split the source_string based on the delimiter

        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
            This is the value of the object inside the Fn::Split intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_SPLIT
        )

        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_SPLIT)

        delimiter = arguments[0]

        verify_intrinsic_type_str(delimiter, IntrinsicResolver.FN_SPLIT, position_in_list="first")

        source_string = self.intrinsic_property_resolver(
            arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_SPLIT
        )

        verify_intrinsic_type_str(source_string, IntrinsicResolver.FN_SPLIT, position_in_list="second")

        return source_string.split(delimiter)

    def handle_fn_base64(self, intrinsic_value, ignore_errors):
        """
        { "Fn::Base64" : valueToEncode }
        This intrinsic function will then base64 encode the string using python's base64.

        This function will resolve all the intrinsic properties in valueToEncode
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::Base64 intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """
        data = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_BASE64
        )

        verify_intrinsic_type_str(data, IntrinsicResolver.FN_BASE64)
        # Encoding then decoding is required to return a string of the data
        return base64.b64encode(data.encode()).decode()

    def handle_fn_select(self, intrinsic_value, ignore_errors):
        """
        { "Fn::Select" : [ index, listOfObjects ] }
        It will select the item in the listOfObjects using python's base64.
        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::Select intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_SELECT
        )

        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_SELECT)

        index = self.intrinsic_property_resolver(
            arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_SELECT
        )

        verify_intrinsic_type_int(index, IntrinsicResolver.FN_SELECT)

        list_of_objects = self.intrinsic_property_resolver(
            arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_SELECT
        )
        verify_intrinsic_type_list(list_of_objects, IntrinsicResolver.FN_SELECT)

        sanitized_objects = [
            self.intrinsic_property_resolver(item, ignore_errors, parent_function=IntrinsicResolver.FN_SELECT)
            for item in list_of_objects
        ]

        verify_in_bounds(index=index, objects=sanitized_objects, property_type=IntrinsicResolver.FN_SELECT)

        return sanitized_objects[index]

    def handle_find_in_map(self, intrinsic_value, ignore_errors):
        """
        { "Fn::FindInMap" : [ "MapName", "TopLevelKey", "SecondLevelKey"] } This function will then lookup the
        specified dictionary in the Mappings dictionary as mappings[map_name][top_level_key][second_level_key].

        This intrinsic function will resolve all the objects within the function's value and check their type.

        The format of the Mappings dictionary is:
        "Mappings": {
            "map_name": {
                "top_level_key": {
                    "second_level_key": "value"
                    }
                }
            }
        }
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::FindInMap intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_FIND_IN_MAP
        )

        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_FIND_IN_MAP)

        verify_number_arguments(arguments, num=3, property_type=IntrinsicResolver.FN_FIND_IN_MAP)

        map_name = self.intrinsic_property_resolver(
            arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_FIND_IN_MAP
        )
        top_level_key = self.intrinsic_property_resolver(
            arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_FIND_IN_MAP
        )
        second_level_key = self.intrinsic_property_resolver(
            arguments[2], ignore_errors, parent_function=IntrinsicResolver.FN_FIND_IN_MAP
        )

        verify_intrinsic_type_str(map_name, IntrinsicResolver.FN_FIND_IN_MAP, position_in_list="first")
        verify_intrinsic_type_str(top_level_key, IntrinsicResolver.FN_FIND_IN_MAP, position_in_list="second")
        verify_intrinsic_type_str(second_level_key, IntrinsicResolver.FN_FIND_IN_MAP, position_in_list="third")

        map_value = self._mapping.get(map_name)
        verify_intrinsic_type_dict(
            map_value,
            IntrinsicResolver.FN_FIND_IN_MAP,
            position_in_list="first",
            message="The MapName is missing in the Mappings dictionary in Fn::FindInMap  for {}".format(map_name),
        )

        top_level_value = map_value.get(top_level_key)
        verify_intrinsic_type_dict(
            top_level_value,
            IntrinsicResolver.FN_FIND_IN_MAP,
            message="The TopLevelKey is missing in the Mappings dictionary in Fn::FindInMap "
            "for {}".format(top_level_key),
        )

        second_level_value = top_level_value.get(second_level_key)
        verify_non_null(
            second_level_value,
            IntrinsicResolver.FN_FIND_IN_MAP,
            message="The SecondLevelKey is missing in the Mappings dictionary in Fn::FindInMap  "
            "for {}".format(second_level_key),
        )

        return second_level_value

    def handle_fn_get_azs(self, intrinsic_value, ignore_errors):
        """
        { "Fn::GetAZs" : "" }
        { "Fn::GetAZs" : { "Ref" : "AWS::Region" } }
        { "Fn::GetAZs" : "us-east-1" }
        This intrinsic function will get the availability zones specified for the specified region. This is usually used
        with {"Ref": "AWS::Region"}. If it is an empty string, it will get the default region.

        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::GetAZs intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """
        intrinsic_value = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_GET_AZS
        )
        verify_intrinsic_type_str(intrinsic_value, IntrinsicResolver.FN_GET_AZS)

        if not intrinsic_value:
            intrinsic_value = self._symbol_resolver.handle_pseudo_region()

        if intrinsic_value not in self._symbol_resolver.REGIONS:
            raise InvalidIntrinsicException(
                "Invalid region string passed in to {}".format(IntrinsicResolver.FN_GET_AZS)
            )

        return self._symbol_resolver.REGIONS.get(intrinsic_value)

    def handle_fn_transform(self, intrinsic_value, ignore_errors):
        """
        { "Fn::Transform" : { "Name" : macro name, "Parameters" : {key : value, ... } } }
        This intrinsic function will transform the data with the body provided

        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::Transform intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """
        macro_name = intrinsic_value.get("Name")
        name = self.intrinsic_property_resolver(
            macro_name, ignore_errors, parent_function=IntrinsicResolver.FN_TRANSFORM
        )

        if name not in IntrinsicResolver.SUPPORTED_MACRO_TRANSFORMATIONS:
            raise InvalidIntrinsicException(
                "The type {} is not currently supported in {}".format(name, IntrinsicResolver.FN_TRANSFORM)
            )

        parameters = intrinsic_value.get("Parameters")
        verify_intrinsic_type_dict(
            parameters, IntrinsicResolver.FN_TRANSFORM, message=" Fn::Transform requires parameters section"
        )

        location = self.intrinsic_property_resolver(parameters.get("Location"), ignore_errors)
        location_data = get_template_data(location)

        return location_data

    @staticmethod
    def handle_fn_import_value(intrinsic_value, ignore_errors):
        """
        { "Fn::ImportValue" : sharedValueToImport }
        This intrinsic function requires handling multiple stacks, which is not currently supported by SAM-CLI.
        Thus, it will thrown an exception.

        Return
        -------
        An InvalidIntrinsicException
        """
        raise InvalidIntrinsicException("Fn::ImportValue is currently not supported by IntrinsicResolver")

    def handle_fn_getatt(self, intrinsic_value, ignore_errors):
        """
        { "Fn::GetAtt" : [ "logicalNameOfResource", "attributeName" ] }
        This intrinsic function gets the attribute for logical_resource specified. Each attribute might have a different
        functionality depending on the type.

        This intrinsic function will resolve all the objects within the function's value and check their type.
        This calls the symbol resolver in order to resolve the relevant attribute.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::GetAtt intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_GET_ATT
        )
        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_GET_ATT)
        verify_number_arguments(arguments, IntrinsicResolver.FN_GET_ATT, num=2)

        logical_id = self.intrinsic_property_resolver(
            arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_GET_ATT
        )
        resource_type = self.intrinsic_property_resolver(
            arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_GET_ATT
        )

        verify_intrinsic_type_str(logical_id, IntrinsicResolver.FN_GET_ATT)
        verify_intrinsic_type_str(resource_type, IntrinsicResolver.FN_GET_ATT)

        return self._symbol_resolver.resolve_symbols(logical_id, resource_type)

    def handle_fn_ref(self, intrinsic_value, ignore_errors):
        """
        {"Ref": "Logical ID"}
        This intrinsic function gets the reference to a certain attribute. Some Ref's have different functionality with
        different resource types.

        This intrinsic function will resolve all the objects within the function's value and check their type.
        This calls the symbol resolver in order to resolve the relevant attribute.
        Parameter
        ----------
        intrinsic_value: str
           This is the value of the object inside the Ref intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.REF
        )
        verify_intrinsic_type_str(arguments, IntrinsicResolver.REF)

        return self._symbol_resolver.resolve_symbols(arguments, IntrinsicResolver.REF)

    def handle_fn_sub(self, intrinsic_value, ignore_errors):
        """
        { "Fn::Sub" : [ String, { Var1Name: Var1Value, Var2Name: Var2Value } ] } or { "Fn::Sub" : String }
        This intrinsic function will substitute the variables specified in the list into the string provided. The string
        will also parse out pseudo properties and anything of the form ${}.

        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::Join intrinsic function property

        Return
        -------
        A string with the resolved attributes
        """

        def resolve_sub_attribute(intrinsic_item, symbol_resolver):
            if "." in intrinsic_item:
                (logical_id, attribute_type) = intrinsic_item.rsplit(".", 1)
            else:
                (logical_id, attribute_type) = intrinsic_item, IntrinsicResolver.REF
            return symbol_resolver.resolve_symbols(logical_id, attribute_type, ignore_errors=True)

        if isinstance(intrinsic_value, str):
            intrinsic_value = [intrinsic_value, {}]

        verify_intrinsic_type_list(
            intrinsic_value, IntrinsicResolver.FN_SUB, message="The arguments to a Fn::Sub must be a list or a string"
        )

        verify_number_arguments(intrinsic_value, IntrinsicResolver.FN_SUB, num=2)

        sub_str = self.intrinsic_property_resolver(
            intrinsic_value[0], ignore_errors, parent_function=IntrinsicResolver.FN_SUB
        )
        verify_intrinsic_type_str(sub_str, IntrinsicResolver.FN_SUB, position_in_list="first")

        variables = intrinsic_value[1]
        verify_intrinsic_type_dict(variables, IntrinsicResolver.FN_SUB, position_in_list="second")

        sanitized_variables = self.intrinsic_property_resolver(
            variables, ignore_errors, parent_function=IntrinsicResolver.FN_SUB
        )

        subable_props = re.findall(string=sub_str, pattern=IntrinsicResolver._REGEX_SUB_FUNCTION)
        for sub_item in subable_props:
            sanitized_item = sanitized_variables[sub_item] if sub_item in sanitized_variables else sub_item
            result = resolve_sub_attribute(sanitized_item, self._symbol_resolver)
            sub_str = re.sub(pattern=r"\$\{" + sub_item + r"\}", string=sub_str, repl=str(result))
        return sub_str

    def handle_fn_if(self, intrinsic_value, ignore_errors):
        """
        {"Fn::If": [condition_name, value_if_true, value_if_false]}
        This intrinsic function will evaluate the condition from the Conditions dictionary and then return value_if_true
        or value_if_false depending on the value.

        The Conditions dictionary will have the following format:
        {
            "Conditions": {
                "condition_name": True/False or "{Intrinsic Function}"
            }
        }

        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::Join intrinsic function property

        Return
        -------
        This will return value_if_true and value_if_false depending on how the condition is evaluated
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_IF
        )
        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_IF)
        verify_number_arguments(arguments, IntrinsicResolver.FN_IF, num=3)

        condition_name = self.intrinsic_property_resolver(
            arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_IF
        )
        verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_IF)

        value_if_true = self.intrinsic_property_resolver(
            arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_IF
        )
        value_if_false = self.intrinsic_property_resolver(
            arguments[2], ignore_errors, parent_function=IntrinsicResolver.FN_IF
        )

        condition = self._conditions.get(condition_name)
        verify_intrinsic_type_dict(
            condition,
            IntrinsicResolver.FN_IF,
            message="The condition is missing in the Conditions dictionary for {}".format(IntrinsicResolver.FN_IF),
        )

        condition_evaluated = self.intrinsic_property_resolver(
            condition, ignore_errors, parent_function=IntrinsicResolver.FN_IF
        )
        verify_intrinsic_type_bool(
            condition_evaluated,
            IntrinsicResolver.FN_IF,
            message="The result of {} must evaluate to bool".format(IntrinsicResolver.FN_IF),
        )

        return value_if_true if condition_evaluated else value_if_false

    def handle_fn_equals(self, intrinsic_value, ignore_errors):
        """
        {"Fn::Equals" : ["value_1", "value_2"]}
        This intrinsic function will verify that both items in the intrinsic function are equal after resolving them.

        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::Join intrinsic function property

        Return
        -------
        A boolean depending on if both arguments is equal
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_EQUALS
        )
        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_EQUALS)
        verify_number_arguments(arguments, IntrinsicResolver.FN_EQUALS, num=2)

        value_1 = self.intrinsic_property_resolver(
            arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_EQUALS
        )
        value_2 = self.intrinsic_property_resolver(
            arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_EQUALS
        )
        return value_1 == value_2

    def handle_fn_not(self, intrinsic_value, ignore_errors):
        """
        {"Fn::Not": [{condition}]}
        This intrinsic function will negate the evaluation of the condition specified.

        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::Join intrinsic function property

        Return
        -------
        A boolean that is the opposite of the condition evaluated
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_NOT
        )
        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_NOT)
        verify_number_arguments(arguments, IntrinsicResolver.FN_NOT, num=1)
        argument_sanitised = self.intrinsic_property_resolver(
            arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_NOT
        )
        if isinstance(argument_sanitised, dict) and "Condition" in arguments[0]:
            condition_name = argument_sanitised.get("Condition")
            verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_NOT)

            condition = self._conditions.get(condition_name)
            verify_non_null(condition, IntrinsicResolver.FN_NOT, position_in_list="first")

            argument_sanitised = self.intrinsic_property_resolver(
                condition, ignore_errors, parent_function=IntrinsicResolver.FN_NOT
            )

        verify_intrinsic_type_bool(
            argument_sanitised,
            IntrinsicResolver.FN_NOT,
            message="The result of {} must evaluate to bool".format(IntrinsicResolver.FN_NOT),
        )
        return not argument_sanitised

    @staticmethod
    def get_prefix_position_in_list(i):
        """
        Gets the prefix for the string "ith element of the list", handling first, second, and third.
        :param i:
        :return:
        """
        first, second, third = 1, 2, 3
        prefix = "{} th ".format(str(i))
        if i == first:
            prefix = "first "
        elif i == second:
            prefix = "second "
        elif i == third:
            prefix = "third "
        return prefix

    def handle_fn_and(self, intrinsic_value, ignore_errors):
        """
        {"Fn::And": [{condition}, {...}]}
        This intrinsic checks that every item in the list evaluates to a boolean. The items in the list can either
        be of the format {Condition: condition_name} which finds and evaluates the Conditions dictionary of another
        intrinsic function.

        The Conditions dictionary will have the following format:
        {
            "Conditions": {
                "condition_name": True/False or "{Intrinsic Function}"
            }
        }

        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::Join intrinsic function property

        Return
        -------
        A boolean depending on if all of the properties in Fn::And evaluate to True
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_AND
        )
        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_AND)

        for i, argument in enumerate(arguments):
            if isinstance(argument, dict) and "Condition" in argument:
                condition_name = argument.get("Condition")
                verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_AND)

                condition = self._conditions.get(condition_name)
                verify_non_null(
                    condition, IntrinsicResolver.FN_AND, position_in_list=self.get_prefix_position_in_list(i)
                )

                condition_evaluated = self.intrinsic_property_resolver(
                    condition, ignore_errors, parent_function=IntrinsicResolver.FN_AND
                )
                verify_intrinsic_type_bool(condition_evaluated, IntrinsicResolver.FN_AND)

                if not condition_evaluated:
                    return False
            else:
                condition = self.intrinsic_property_resolver(
                    argument, ignore_errors, parent_function=IntrinsicResolver.FN_AND
                )
                verify_intrinsic_type_bool(condition, IntrinsicResolver.FN_AND)

                if not condition:
                    return False

        return True

    def handle_fn_or(self, intrinsic_value, ignore_errors):
        """
        {"Fn::Or": [{condition}, {...}]}
        This intrinsic checks that a single item in the list evaluates to a boolean. The items in the list can either
        be of the format {Condition: condition_name} which finds and evaluates the Conditions dictionary of another
        intrinsic function.

        The Conditions dictionary will have the following format:
        {
            "Conditions": {
                "condition_name": True/False or "{Intrinsic Function}"
            }
        }

        This intrinsic function will resolve all the objects within the function's value and check their type.
        Parameter
        ----------
        intrinsic_value: list, dict
           This is the value of the object inside the Fn::Join intrinsic function property

        Return
        -------
        A boolean depending on if any of the properties in Fn::And evaluate to True
        """
        arguments = self.intrinsic_property_resolver(
            intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_OR
        )
        verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_OR)
        for i, argument in enumerate(arguments):
            if isinstance(argument, dict) and "Condition" in argument:
                condition_name = argument.get("Condition")
                verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_OR)

                condition = self._conditions.get(condition_name)
                verify_non_null(
                    condition, IntrinsicResolver.FN_OR, position_in_list=self.get_prefix_position_in_list(i)
                )

                condition_evaluated = self.intrinsic_property_resolver(
                    condition, ignore_errors, parent_function=IntrinsicResolver.FN_OR
                )
                verify_intrinsic_type_bool(condition_evaluated, IntrinsicResolver.FN_OR)
                if condition_evaluated:
                    return True
            else:
                condition = self.intrinsic_property_resolver(
                    argument, ignore_errors, parent_function=IntrinsicResolver.FN_OR
                )
                verify_intrinsic_type_bool(condition, IntrinsicResolver.FN_OR)
                if condition:
                    return True
        return False
