import copy
from typing import Any
from typing import Dict
from typing import List

__contracts__ = ["resource"]


async def present(
    hub,
    ctx,
    name: str,
    resource_id: str = None,
    description: str = "",
    key_usage: str = "ENCRYPT_DECRYPT",
    key_spec: str = "SYMMETRIC_DEFAULT",
    key_state: str = "Enabled",
    origin: str = "AWS_KMS",
    multi_region: bool = False,
    policy: str = None,
    bypass_policy_lockout_safety_check: bool = False,
    enable_key_rotation: bool = False,
    tags: List = None,
    timeout: Dict = None,
) -> Dict[str, Any]:
    r"""
    **Autogenerated function**

    Create or update AWS kms key.

    Update limitations:
    Policy can be updated, but cannot be cleared once set.
    multi_region, key_usage and key_spec cannot be updated
    enable_key_rotation, cannot be enabled on asymmetric KMS keys.

    Args:
        hub:
        ctx:
        name(str): A name of the kms key
        resource_id(str, Optional): AWS KMS Key ID.
        description(str, Optional): description of the key
        key_usage(str, Default: 'ENCRYPT_DECRYPT'): optional values: 'SIGN_VERIFY'|'ENCRYPT_DECRYPT'
        key_spec(str, Default: "SYMMETRIC_DEFAULT"): optional values: 'RSA_2048'|'RSA_3072'|
            'RSA_4096'|'ECC_NIST_P256'|'ECC_NIST_P384'|'ECC_NIST_P521'|'ECC_SECG_P256K1'|'SYMMETRIC_DEFAULT'
        key_state(str: Default: Enabled): by default key is enabled. Use 'Disabled' to disable the key.
        origin(str, Default: "AWS_KMS"): optional values: 'AWS_KMS'|'EXTERNAL'|'AWS_CLOUDHSM'
        multi_region(bool, Default: False):
        policy(str, Optional): a default policy is created for each key
        bypass_policy_lockout_safety_check(bool, Default = False): bypass policy safety check, should be true when
            policy is specified or key creation fails with this error:
            "The new key policy will not allow you to update the key policy in the future.
            This field is not returned by 'describe'.
        enable_key_rotation(bool, Default = False): by default new key created with key rotation disabled.
        tags(List, Optional): List of TagKey and TagValue pairs
        timeout(Dict, optional): Timeout configuration for update of AWS KMS key.
            * update (string) -- Timeout configuration for update of a KMS key
                * delay -- The amount of time in seconds to wait between attempts.
                * max_attempts -- Max attempts of waiting for change.

    Request Syntax:
        [key-resource-id]:
          aws.kms.key.present:
          - resource_id: 'string'
          - description: 'string'
          - key_state: 'string'
          - key_usage: 'string'
          - key_spec: 'string'
          - multi_region: 'boolean'
          - bypass_policy_lockout_safety_check: 'boolean'
          - policy: 'string'
          - tags:
            - TagKey: 'string'
              TagValue: 'string'

    Returns:
        Dict[str, Any]

    Examples:

        .. code-block:: sls

            new-key:
                aws.kms.key.present:
                    - key_state: Enabled
                    - description: key-with-policy-and-tags
                    - key_usage: ENCRYPT_DECRYPT
                    - key_spec: SYMMETRIC_DEFAULT
                    - multi_region: false
                    - bypass_policy_lockout_safety_check: true
                    - policy: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"EnableIAMUserPermissions\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::537227425989:root\"},\"Action\":[\"kms:Create*\",\"kms:Describe*\",\"kms:Enable*\"],\"Resource\":\"*\"}]}"
                    - tags:
                        - TagKey: test-key
                          TagValue: test-value
                        - TagKey: test-key-1
                          TagValue: test-key-1
    """

    result = dict(comment=(), old_state=None, new_state=None, name=name, result=True)

    updated_vals = {}
    before = None
    if resource_id is not None:
        before = await hub.exec.boto3.client.kms.describe_key(ctx, KeyId=resource_id)

    policy = hub.tool.aws.state_comparison_utils.standardise_json(policy)

    if before and before["result"]:
        result["comment"] = (f"aws.kms.key '{name}' already exists.",)
        try:
            result[
                "old_state"
            ] = await hub.tool.aws.kms.conversion_utils.convert_raw_key_to_present(
                ctx, raw_resource=before["ret"]["KeyMetadata"]
            )
        except Exception as e:
            result["comment"] = result["comment"] + (str(e),)
            result["result"] = False
            return result

        # Used for 'test'
        plan_state = copy.deepcopy(result["old_state"])

        # Update key tags if tags are specified
        if (
            tags is not None
            and not hub.tool.aws.state_comparison_utils.are_lists_identical(
                tags, result["old_state"].get("tags", None)
            )
        ):
            hub.log.debug(f"aws.kms.key '{name}' tags update")
            # For tag operations key_id is used and not resource_id
            update_ret = await hub.exec.aws.kms.key.update_key_tags(
                ctx=ctx,
                key_id=resource_id,
                old_tags=result["old_state"].get("tags", []),
                new_tags=tags,
            )

            result["result"] = update_ret["result"]
            result["comment"] = result["comment"] + update_ret["comment"]
            if not result["result"]:
                return result

            plan_state["tags"] = update_ret["ret"].get("tags")
            updated_vals["tags"] = tags

        # Enable/Disable key
        # No updates for key_state "PendingDeletion", which means the key is scheduled to be deleted.
        old_state = result["old_state"].get("key_state", "")
        if key_state != old_state:
            if ctx.get("test", False):
                result["comment"] = result["comment"] + (
                    f"Would update state on aws.kms.key '{name}'.",
                )
                plan_state["key_state"] = key_state
            else:
                update_ret = None
                if old_state == "Enabled" and key_state == "Disabled":
                    update_ret = await hub.exec.boto3.client.kms.disable_key(
                        ctx, KeyId=result["old_state"]["resource_id"]
                    )
                elif old_state == "Disabled" and key_state == "Enabled":
                    update_ret = await hub.exec.boto3.client.kms.enable_key(
                        ctx, KeyId=result["old_state"]["resource_id"]
                    )

                if update_ret:
                    hub.log.debug(
                        f"Updated the state of aws.kms.key '{name}' to '{key_state}'."
                    )
                    result["comment"] = result["comment"] + (
                        f"Updated aws.kms.key '{name}' state to '{key_state}'.",
                    )
                    updated_vals["key_state"] = key_state
                else:
                    hub.log.warning(
                        f"Failed to update the state of aws.kms.key '{name}' to '{key_state}'. {update_ret['comment']} "
                    )
                    result["result"] = update_ret["result"]
                    result["comment"] = result["comment"] + update_ret["comment"]
                    return result

        # Update policy and/or bypass flag if needed and if policy is set
        update_policy = (
            policy
            and not hub.tool.aws.state_comparison_utils.is_json_identical(
                result["old_state"].get("policy"), policy
            )
        )
        update_bypass_lockout = bypass_policy_lockout_safety_check != result[
            "old_state"
        ].get("bypass_policy_lockout_safety_check", False)
        if policy and (update_policy or update_bypass_lockout):
            if ctx.get("test", False):
                if update_policy:
                    result["comment"] = result["comment"] + (
                        f"Would update policy of aws.kms.key '{name}'.",
                    )
                if update_bypass_lockout:
                    result["comment"] = result["comment"] + (
                        f"Would update bypass_policy_lockout_safety_check of aws.kms.key '{name}'.",
                    )
                plan_state["policy"] = policy
                plan_state[
                    "bypass_policy_lockout_safety_check"
                ] = bypass_policy_lockout_safety_check
            else:
                update_ret = await hub.exec.boto3.client.kms.put_key_policy(
                    ctx,
                    KeyId=result["old_state"]["resource_id"],
                    Policy=policy,
                    PolicyName="default",
                    BypassPolicyLockoutSafetyCheck=bypass_policy_lockout_safety_check,
                )

                if update_ret["result"]:
                    if update_policy:
                        result["comment"] = result["comment"] + (
                            f"Updated aws.kms.key '{name}' policy.",
                        )
                        updated_vals["policy"] = policy
                    if update_bypass_lockout:
                        result["comment"] = result["comment"] + (
                            f"Updated aws.kms.key '{name}' bypass_policy_lockout_safety_check.",
                        )
                        updated_vals[
                            "bypass_policy_lockout_safety_check"
                        ] = bypass_policy_lockout_safety_check
                else:
                    hub.log.warning(
                        f"Failed to update the policy and/or bypass_policy_lockout_safety_check of aws.kms.key '{name}': {update_ret['comment']}"
                    )
                    result["result"] = update_ret["result"]
                    result["comment"] = result["comment"] + update_ret["comment"]
                    return result

        # Update description if needed
        if description and description != result["old_state"].get("description"):
            if ctx.get("test", False):
                result["comment"] = result["comment"] + (
                    f"Would update description of aws.kms.key '{name}'.",
                )
                plan_state["description"] = description
            else:
                update_ret = await hub.exec.boto3.client.kms.update_key_description(
                    ctx,
                    KeyId=result["old_state"]["resource_id"],
                    Description=description,
                )

                if update_ret["result"]:
                    result["comment"] = result["comment"] + (
                        f"Updated aws.kms.key '{name}' description.",
                    )
                    updated_vals["description"] = description
                else:
                    hub.log.warning(
                        f"Failed to update the description of aws.kms.key '{name}': {update_ret['comment']}"
                    )
                    result["result"] = update_ret["result"]
                    result["comment"] = result["comment"] + update_ret["comment"]
                    return result
    else:
        try:
            if ctx.get("test", False):
                result["new_state"] = hub.tool.aws.test_state_utils.generate_test_state(
                    enforced_state={},
                    desired_state={
                        "name": name,
                        "resource_id": f"key-{name}-resource_id",
                        "arn": f"key-{name}-arn",
                        "description": description,
                        "key_usage": key_usage,
                        "key_spec": key_spec,
                        "origin": origin,
                        "multi_region": multi_region,
                        "policy": policy,
                        "bypass_policy_lockout_safety_check": bypass_policy_lockout_safety_check,
                        "enable_key_rotation": enable_key_rotation,
                        "tags": tags,
                    },
                )
                result["comment"] = (f"Would create aws.kms.key {name}",)
                return result

            ret = await hub.exec.boto3.client.kms.create_key(
                ctx,
                Description=description,
                KeyUsage=key_usage,
                KeySpec=key_spec,
                Origin=origin,
                MultiRegion=multi_region,
                Policy=policy,
                BypassPolicyLockoutSafetyCheck=bypass_policy_lockout_safety_check,
                Tags=tags,
            )

            result["result"] = ret["result"]
            if not result["result"]:
                result["comment"] = ret["comment"]
                return result
            resource_id = ret["ret"]["KeyMetadata"]["KeyId"]
            result["comment"] = (
                f"Created aws.kms.key '{name}' with description '{description}'.",
            )
        except hub.tool.boto3.exception.ClientError as e:
            result["comment"] = f"{e.__class__.__name__}: {e}"
            result["result"] = False

    # Set key rotation:
    # Enable it on a newly created key or Enable/Disable on an existing key,
    # only if the key_state is Enabled
    if (enable_key_rotation and result.get("old_state") is None) or (
        result.get("old_state")
        and (
            key_state == "Enabled"
            or (key_state == None and result["old_state"].get("key_state") == "Enabled")
        )
        and enable_key_rotation != result["old_state"].get("enable_key_rotation", False)
    ):
        update_ret = await hub.exec.aws.kms.key.set_key_rotation(
            ctx=ctx,
            resource_id=resource_id,
            enable_key_rotation=enable_key_rotation,
        )
        plan_state["enable_kms_rotation"] = enable_key_rotation
        result["comment"] = result["comment"] + update_ret["comment"]
        if not update_ret["result"]:
            result["result"] = False
            return result

    if ctx.get("test", False):
        result["new_state"] = plan_state
    else:
        if updated_vals:
            wait_updates_res = await hub.exec.aws.kms.key.wait_for_updates(
                ctx,
                timeout=timeout.get("update") if timeout else None,
                resource_id=resource_id,
                updates=updated_vals,
            )
            if wait_updates_res["result"] is False:
                # Do not fail just add to comments
                result["comment"] = result["comment"] + wait_updates_res["comment"]

            result["new_state"] = wait_updates_res["ret"]
        else:
            try:
                after = await hub.exec.boto3.client.kms.describe_key(
                    ctx, KeyId=resource_id
                )
                if after.get("ret"):
                    result[
                        "new_state"
                    ] = await hub.tool.aws.kms.conversion_utils.convert_raw_key_to_present(
                        ctx, raw_resource=after["ret"].get("KeyMetadata", None)
                    )
            except Exception as e:
                result["comment"] = result["comment"] + (str(e),)
                result["result"] = False

    return result


async def absent(
    hub, ctx, name: str, resource_id: str = None, pending_window_in_days: int = 7
) -> Dict[str, Any]:
    r"""
    **Autogenerated function**

    Key cannot be immediately deleted but can be scheduled to be deleted.
    Once the key is set to be deleted in 'pending_window_in_days' a deletion date is
    set on the key and it cannot be modified.
    So 'deleting' again with a different 'pending_window_in_days' is ignored.
    Also key can be disabled using the present function with key_state = 'Disabled'.

    Args:
        hub:
        ctx:
        name(Text): Name of the key.
        resource_id(Text, Optional): AWS KMS Key ID. Idem automatically considers this resource being absent
         if this field is not specified.
        pending_window_in_days(int, Optional, Default: 7 days): How many days before key is deleted.

    Returns:
        Dict[str, Any]

    Examples:

        .. code-block:: sls

            resource_is_absent:
              aws.kms.key.absent:
                - resource_id: value
                - name: value
    """

    result = dict(comment=(), old_state=None, new_state=None, name=name, result=True)
    if not resource_id:
        result["comment"] = hub.tool.aws.comment_utils.already_absent_comment(
            resource_type="aws.kms.key", name=name
        )
        return result
    before = await hub.exec.boto3.client.kms.describe_key(ctx, KeyId=resource_id)

    if not before:
        result["comment"] = hub.tool.aws.comment_utils.already_absent_comment(
            resource_type="aws.kms.key", name=name
        )
        return result
    else:
        try:
            result[
                "old_state"
            ] = await hub.tool.aws.kms.conversion_utils.convert_raw_key_to_present(
                ctx, raw_resource=before["ret"]["KeyMetadata"]
            )
        except Exception as e:
            result["comment"] = (str(e),)
            result["result"] = False
            return result

        if before["ret"]["KeyMetadata"].get("DeletionDate", None):
            result["comment"] = (
                f"aws.kms.key '{resource_id}' already scheduled to be deleted in '{pending_window_in_days}' days",
            )
            return result

        if ctx.get("test", False):
            result["comment"] = (
                f"Would schedule deletion of aws.kms.key '{resource_id}' in {pending_window_in_days} days",
            )
            return result

        try:
            # Minimum deletion schedule 7 days (from 7 - 30)
            ret = await hub.exec.boto3.client.kms.schedule_key_deletion(
                ctx,
                KeyId=resource_id,
                PendingWindowInDays=pending_window_in_days,
            )
            result["result"] = ret["result"]
            if not result["result"]:
                result["comment"] = ret["comment"]
                result["result"] = False
                return result
            result["comment"] = (
                f"aws.kms.key '{resource_id}' is scheduled for deletion in {pending_window_in_days} days",
            )
        except hub.tool.boto3.exception.ClientError as e:
            result["comment"] = (f"{e.__class__.__name__}: {e}",)

    try:
        after = await hub.exec.boto3.client.kms.describe_key(
            ctx, KeyId=(resource_id if resource_id else name)
        )
        if after.get("ret"):
            result[
                "new_state"
            ] = await hub.tool.aws.kms.conversion_utils.convert_raw_key_to_present(
                ctx, raw_resource=after["ret"]["KeyMetadata"]
            )
    except Exception as e:
        result["comment"] = result["comment"] + (str(e),)
        result["result"] = False
    return result


async def describe(hub, ctx) -> Dict[str, Dict[str, Any]]:
    r"""
    Describe the resource in a way that can be recreated/managed with the corresponding "present" function


    Gets a list of keys in the caller's Amazon Web Services account and region. For more information about
    keys, see Createkey. By default, the Listkeys operation returns all keys in the account and region.
    To get only the keys associated with a particular KMS key, use the KeyId parameter. The Listkeys response
    can include keys that you created and associated with your customer managed keys, and keys that Amazon Web
    Services created and associated with Amazon Web Services managed keys in your account. You can recognize Amazon
    Web Services keys because their names have the format aws/<service-name>, such as aws/dynamodb. The response
    might also include keys that have no TargetKeyId field. These are predefined keys that Amazon Web Services
    has created but has not yet associated with a KMS key. keys that Amazon Web Services creates in your account,
    including predefined keys, do not count against your KMS keys quota.  Cross-account use: No. Listkeys
    does not return keys in other Amazon Web Services accounts.  Required permissions: kms:Listkeys (IAM
    policy) For details, see Controlling access to keys in the Key Management Service Developer Guide.  Related
    operations:     CreateKey


    Returns:
        Dict[str, Any]

    Examples:

        .. code-block:: bash

            $ idem describe aws.kms.key
    """

    result = {}
    ret = await hub.exec.boto3.client.kms.list_keys(ctx)

    if not ret["result"]:
        hub.log.debug(f"Could not describe aws.kms.keys {ret['comment']}")
        return {}

    for key in ret["ret"]["Keys"]:
        key_details = await hub.exec.boto3.client.kms.describe_key(
            ctx, KeyId=key.get("KeyId", None)
        )
        translated_resource = (
            await hub.tool.aws.kms.conversion_utils.convert_raw_key_to_present(
                ctx, raw_resource=key_details["ret"]["KeyMetadata"]
            )
        )
        result[translated_resource["resource_id"]] = {
            "aws.kms.key.present": [
                {parameter_key: parameter_value}
                for parameter_key, parameter_value in translated_resource.items()
            ]
        }

    return result
