from typing import Any

from samtranslator.intrinsics.actions import RefAction
from samtranslator.intrinsics.resolver import IntrinsicsResolver
from samtranslator.policy_template_processor.exceptions import InsufficientParameterValues, InvalidParameterValues

POLICY_PARAMETER_DISAMBIGUATE_PREFIX = "___SAM_POLICY_PARAMETER_"


class Template:
    """
    Class representing a single policy template. It includes the name, parameters and template dictionary.
    """

    def __init__(self, template_name, parameters, template_definition):  # type: ignore[no-untyped-def]
        """
        Initialize a template.
        For simplicity, this method assumes that inputs have already been validated against the JSON Schema. So no
        further validation is performed.

        :param string template_name: Name of this template
        :param dict parameters: Dictionary representing parameters. Refer to the JSON Schema for structure of this dict
        :param template_definition: Template definition. Refer to JSON Schema for structure of this dict
        :raises ValueError: If one or more of the parameters are not referenced in the template definition
        """
        self.name = template_name
        self.parameters = parameters
        self.definition = template_definition

    def to_statement(self, parameter_values):  # type: ignore[no-untyped-def]
        """
        With the given values for each parameter, this method will return a policy statement that can be used
        directly with IAM.

        :param dict parameter_values: Dict containing values for each parameter defined in the template
        :return dict: Dictionary containing policy statement
        :raises InvalidParameterValues: If parameter values is not a valid dictionary or does not contain values
            for all parameters
        :raises InsufficientParameterValues: If the parameter values don't have values for all required parameters
        """

        missing = self.missing_parameter_values(parameter_values)  # type: ignore[no-untyped-call]
        if len(missing) > 0:
            # str() of elements of list to prevent any `u` prefix from being displayed in user-facing error message
            raise InsufficientParameterValues(  # type: ignore[no-untyped-call]
                f"Following required parameters of template '{self.name}' don't have values: {[str(m) for m in missing]}"
            )

        # Select only necessary parameter_values. this is to prevent malicious or accidental
        # injection of values for parameters not intended in the template. This is important because "Ref" resolution
        # will substitute any references for which a value is provided.
        necessary_parameter_values = {
            POLICY_PARAMETER_DISAMBIGUATE_PREFIX + name: value
            for name, value in parameter_values.items()
            if name in self.parameters
        }

        # Only "Ref" is supported
        supported_intrinsics = {RefAction.intrinsic_name: RefAction()}

        resolver = IntrinsicsResolver(necessary_parameter_values, supported_intrinsics)
        definition_copy = self._disambiguate_policy_parameter(self.definition)

        return resolver.resolve_parameter_refs(definition_copy)

    @staticmethod
    def _disambiguate_policy_parameter(policy_definition: Any) -> Any:
        """
        Return a deepcopy of policy definition where all parameters are
        renamed to avoid naming collision of normal CFN parameters.
        This is to avoid IntrinsicResolver.resolve_parameter_refs()
        will make infinitely recursion on this:
        ```
        - DynamoDBCrudPolicy:
          TableName:  <- this is the policy parameter
            Fn::ImportValue:
              Fn::Join:
              - '-'
              - - Ref: TableName <- this is the CFN parameter
                - hello
                - Ref: EnvironmentType
        ```
        Once IntrinsicResolver.resolve_parameter_refs() replace the "Ref: TableName"
        with "TableName: .... Ref: TableName - hello ---"
        There are "Ref: TableName" in it again (indefinitely).
        """

        def _traverse(node: Any) -> Any:
            if isinstance(node, dict):
                copy = {key: _traverse(value) for key, value in node.items()}
                if "Ref" in copy and isinstance(copy["Ref"], str):
                    copy["Ref"] = POLICY_PARAMETER_DISAMBIGUATE_PREFIX + copy["Ref"]
                return copy
            if isinstance(node, list):
                return [_traverse(item) for item in node]
            return node

        return _traverse(policy_definition)

    def missing_parameter_values(self, parameter_values):  # type: ignore[no-untyped-def]
        """
        Checks if the given input contains values for all parameters used by this template

        :param dict parameter_values: Dictionary of values for each parameter used in the template
        :return list: List of names of parameters that are missing.
        :raises InvalidParameterValues: When parameter values is not a valid dictionary
        """

        if not self._is_valid_parameter_values(parameter_values):  # type: ignore[no-untyped-call]
            raise InvalidParameterValues("Parameter values are required to process a policy template")  # type: ignore[no-untyped-call]

        return list(set(self.parameters.keys()) - set(parameter_values.keys()))

    @staticmethod
    def _is_valid_parameter_values(parameter_values):  # type: ignore[no-untyped-def]
        """
        Checks if the given parameter values dictionary is valid
        :param dict parameter_values:
        :return: True, if it is valid. False otherwise
        """
        return parameter_values is not None and isinstance(parameter_values, dict)

    @staticmethod
    def from_dict(template_name, template_values_dict):  # type: ignore[no-untyped-def]
        """
        Parses the input and returns an instance of this class.

        :param string template_name: Name of the template
        :param dict template_values_dict: Dictionary containing the value of the template. This dict must have passed
            the JSON Schema validation.
        :return Template: Instance of this class containing the values provided in this dictionary
        """

        parameters = template_values_dict.get("Parameters", {})
        definition = template_values_dict.get("Definition", {})

        return Template(template_name, parameters, definition)  # type: ignore[no-untyped-call]
