# -*- coding: utf-8 -*-

# Copyright: (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

import copy

try:
    import botocore.waiter as core_waiter
except ImportError:
    pass  # caught by HAS_BOTO3

from ansible_collections.amazon.aws.plugins.module_utils.retries import RetryingBotoClientWrapper


ec2_data = {
    "version": 2,
    "waiters": {
        "ImageAvailable": {
            "operation": "DescribeImages",
            "maxAttempts": 80,
            "delay": 15,
            "acceptors": [
                {"state": "success", "matcher": "pathAll", "argument": "Images[].State", "expected": "available"},
                {"matcher": "error", "expected": "InvalidAMIID.NotFound", "state": "retry"},
                {"state": "failure", "matcher": "pathAny", "argument": "Images[].State", "expected": "failed"},
            ],
        },
        "InternetGatewayExists": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeInternetGateways",
            "acceptors": [
                {"matcher": "path", "expected": True, "argument": "length(InternetGateways) > `0`", "state": "success"},
                {"matcher": "error", "expected": "InvalidInternetGatewayID.NotFound", "state": "retry"},
            ],
        },
        "InternetGatewayAttached": {
            "operation": "DescribeInternetGateways",
            "delay": 5,
            "maxAttempts": 40,
            "acceptors": [
                {
                    "expected": "available",
                    "matcher": "pathAll",
                    "state": "success",
                    "argument": "InternetGateways[].Attachments[].State",
                },
                {"matcher": "error", "expected": "InvalidInternetGatewayID.NotFound", "state": "retry"},
            ],
        },
        "NetworkInterfaceAttached": {
            "operation": "DescribeNetworkInterfaces",
            "delay": 5,
            "maxAttempts": 40,
            "acceptors": [
                {
                    "expected": "attached",
                    "matcher": "pathAll",
                    "state": "success",
                    "argument": "NetworkInterfaces[].Attachment.Status",
                },
                {"expected": "InvalidNetworkInterfaceID.NotFound", "matcher": "error", "state": "failure"},
            ],
        },
        "NetworkInterfaceAvailable": {
            "operation": "DescribeNetworkInterfaces",
            "delay": 5,
            "maxAttempts": 40,
            "acceptors": [
                {
                    "expected": "available",
                    "matcher": "pathAll",
                    "state": "success",
                    "argument": "NetworkInterfaces[].Status",
                },
                {"expected": "InvalidNetworkInterfaceID.NotFound", "matcher": "error", "state": "retry"},
            ],
        },
        "NetworkInterfaceDeleted": {
            "operation": "DescribeNetworkInterfaces",
            "delay": 5,
            "maxAttempts": 40,
            "acceptors": [
                {
                    "matcher": "path",
                    "expected": True,
                    "argument": "length(NetworkInterfaces[]) > `0`",
                    "state": "retry",
                },
                {
                    "matcher": "path",
                    "expected": True,
                    "argument": "length(NetworkInterfaces[]) == `0`",
                    "state": "success",
                },
                {"expected": "InvalidNetworkInterfaceID.NotFound", "matcher": "error", "state": "success"},
            ],
        },
        "NetworkInterfaceDeleteOnTerminate": {
            "operation": "DescribeNetworkInterfaces",
            "delay": 5,
            "maxAttempts": 10,
            "acceptors": [
                {
                    "expected": True,
                    "matcher": "pathAll",
                    "state": "success",
                    "argument": "NetworkInterfaces[].Attachment.DeleteOnTermination",
                },
                {"expected": "InvalidNetworkInterfaceID.NotFound", "matcher": "error", "state": "failure"},
            ],
        },
        "NetworkInterfaceNoDeleteOnTerminate": {
            "operation": "DescribeNetworkInterfaces",
            "delay": 5,
            "maxAttempts": 10,
            "acceptors": [
                {
                    "expected": False,
                    "matcher": "pathAll",
                    "state": "success",
                    "argument": "NetworkInterfaces[].Attachment.DeleteOnTermination",
                },
                {"expected": "InvalidNetworkInterfaceID.NotFound", "matcher": "error", "state": "failure"},
            ],
        },
        "RouteTableExists": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeRouteTables",
            "acceptors": [
                {"matcher": "path", "expected": True, "argument": "length(RouteTables[]) > `0`", "state": "success"},
                {"matcher": "error", "expected": "InvalidRouteTableID.NotFound", "state": "retry"},
            ],
        },
        "SecurityGroupExists": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeSecurityGroups",
            "acceptors": [
                {"matcher": "path", "expected": True, "argument": "length(SecurityGroups[]) > `0`", "state": "success"},
                {"matcher": "error", "expected": "InvalidGroup.NotFound", "state": "retry"},
            ],
        },
        "SnapshotCompleted": {
            "delay": 15,
            "operation": "DescribeSnapshots",
            "maxAttempts": 40,
            "acceptors": [
                {"expected": "completed", "matcher": "pathAll", "state": "success", "argument": "Snapshots[].State"}
            ],
        },
        "SubnetAvailable": {
            "delay": 15,
            "operation": "DescribeSubnets",
            "maxAttempts": 40,
            "acceptors": [
                {"expected": "available", "matcher": "pathAll", "state": "success", "argument": "Subnets[].State"}
            ],
        },
        "SubnetExists": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeSubnets",
            "acceptors": [
                {"matcher": "path", "expected": True, "argument": "length(Subnets[]) > `0`", "state": "success"},
                {"matcher": "error", "expected": "InvalidSubnetID.NotFound", "state": "retry"},
            ],
        },
        "SubnetHasMapPublic": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeSubnets",
            "acceptors": [
                {
                    "matcher": "pathAll",
                    "expected": True,
                    "argument": "Subnets[].MapPublicIpOnLaunch",
                    "state": "success",
                },
            ],
        },
        "SubnetNoMapPublic": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeSubnets",
            "acceptors": [
                {
                    "matcher": "pathAll",
                    "expected": False,
                    "argument": "Subnets[].MapPublicIpOnLaunch",
                    "state": "success",
                },
            ],
        },
        "SubnetHasAssignIpv6": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeSubnets",
            "acceptors": [
                {
                    "matcher": "pathAll",
                    "expected": True,
                    "argument": "Subnets[].AssignIpv6AddressOnCreation",
                    "state": "success",
                },
            ],
        },
        "SubnetNoAssignIpv6": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeSubnets",
            "acceptors": [
                {
                    "matcher": "pathAll",
                    "expected": False,
                    "argument": "Subnets[].AssignIpv6AddressOnCreation",
                    "state": "success",
                },
            ],
        },
        "SubnetDeleted": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeSubnets",
            "acceptors": [
                {"matcher": "path", "expected": True, "argument": "length(Subnets[]) > `0`", "state": "retry"},
                {"matcher": "error", "expected": "InvalidSubnetID.NotFound", "state": "success"},
            ],
        },
        "VpcAvailable": {
            "delay": 15,
            "operation": "DescribeVpcs",
            "maxAttempts": 40,
            "acceptors": [
                {"expected": "available", "matcher": "pathAll", "state": "success", "argument": "Vpcs[].State"}
            ],
        },
        "VpcExists": {
            "operation": "DescribeVpcs",
            "delay": 1,
            "maxAttempts": 5,
            "acceptors": [
                {"matcher": "status", "expected": 200, "state": "success"},
                {"matcher": "error", "expected": "InvalidVpcID.NotFound", "state": "retry"},
            ],
        },
        "VpcEndpointExists": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeVpcEndpoints",
            "acceptors": [
                {"matcher": "path", "expected": True, "argument": "length(VpcEndpoints[]) > `0`", "state": "success"},
                {"matcher": "error", "expected": "InvalidVpcEndpointId.NotFound", "state": "retry"},
            ],
        },
        "VpnGatewayExists": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeVpnGateways",
            "acceptors": [
                {"matcher": "path", "expected": True, "argument": "length(VpnGateways[]) > `0`", "state": "success"},
                {"matcher": "error", "expected": "InvalidVpnGatewayID.NotFound", "state": "retry"},
            ],
        },
        "VpnGatewayDetached": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeVpnGateways",
            "acceptors": [
                {
                    "matcher": "path",
                    "expected": True,
                    "argument": "VpnGateways[0].State == 'available'",
                    "state": "success",
                },
            ],
        },
        "NatGatewayDeleted": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeNatGateways",
            "acceptors": [
                {"state": "success", "matcher": "pathAll", "expected": "deleted", "argument": "NatGateways[].State"},
                {"state": "success", "matcher": "error", "expected": "NatGatewayNotFound"},
            ],
        },
        "NatGatewayAvailable": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeNatGateways",
            "acceptors": [
                {"state": "success", "matcher": "pathAll", "expected": "available", "argument": "NatGateways[].State"},
                {"state": "retry", "matcher": "error", "expected": "NatGatewayNotFound"},
            ],
        },
    },
}


waf_data = {
    "version": 2,
    "waiters": {
        "ChangeTokenInSync": {
            "delay": 20,
            "maxAttempts": 60,
            "operation": "GetChangeTokenStatus",
            "acceptors": [
                {"matcher": "path", "expected": True, "argument": "ChangeTokenStatus == 'INSYNC'", "state": "success"},
                {"matcher": "error", "expected": "WAFInternalErrorException", "state": "retry"},
            ],
        }
    },
}

eks_data = {
    "version": 2,
    "waiters": {
        "ClusterActive": {
            "delay": 20,
            "maxAttempts": 60,
            "operation": "DescribeCluster",
            "acceptors": [
                {"state": "success", "matcher": "path", "argument": "cluster.status", "expected": "ACTIVE"},
                {"state": "retry", "matcher": "error", "expected": "ResourceNotFoundException"},
            ],
        },
        "ClusterDeleted": {
            "delay": 20,
            "maxAttempts": 60,
            "operation": "DescribeCluster",
            "acceptors": [
                {"state": "retry", "matcher": "path", "argument": "cluster.status != 'DELETED'", "expected": True},
                {"state": "success", "matcher": "error", "expected": "ResourceNotFoundException"},
            ],
        },
        "FargateProfileActive": {
            "delay": 20,
            "maxAttempts": 30,
            "operation": "DescribeFargateProfile",
            "acceptors": [
                {"state": "success", "matcher": "path", "argument": "fargateProfile.status", "expected": "ACTIVE"},
                {"state": "retry", "matcher": "error", "expected": "ResourceNotFoundException"},
            ],
        },
        "FargateProfileDeleted": {
            "delay": 20,
            "maxAttempts": 30,
            "operation": "DescribeFargateProfile",
            "acceptors": [
                {
                    "state": "retry",
                    "matcher": "path",
                    "argument": "fargateProfile.status == 'DELETING'",
                    "expected": True,
                },
                {"state": "success", "matcher": "error", "expected": "ResourceNotFoundException"},
            ],
        },
        "NodegroupActive": {
            "delay": 20,
            "maxAttempts": 60,
            "operation": "DescribeNodegroup",
            "acceptors": [
                {"state": "success", "matcher": "path", "argument": "nodegroup.status", "expected": "ACTIVE"},
                {"state": "retry", "matcher": "error", "expected": "ResourceNotFoundException"},
            ],
        },
        "NodegroupDeleted": {
            "delay": 20,
            "maxAttempts": 60,
            "operation": "DescribeNodegroup",
            "acceptors": [
                {"state": "retry", "matcher": "path", "argument": "nodegroup.status == 'DELETING'", "expected": True},
                {"state": "success", "matcher": "error", "expected": "ResourceNotFoundException"},
            ],
        },
    },
}


elb_data = {
    "version": 2,
    "waiters": {
        "AnyInstanceInService": {
            "acceptors": [
                {
                    "argument": "InstanceStates[].State",
                    "expected": "InService",
                    "matcher": "pathAny",
                    "state": "success",
                }
            ],
            "delay": 15,
            "maxAttempts": 40,
            "operation": "DescribeInstanceHealth",
        },
        "InstanceDeregistered": {
            "delay": 15,
            "operation": "DescribeInstanceHealth",
            "maxAttempts": 40,
            "acceptors": [
                {
                    "expected": "OutOfService",
                    "matcher": "pathAll",
                    "state": "success",
                    "argument": "InstanceStates[].State",
                },
                {"matcher": "error", "expected": "InvalidInstance", "state": "success"},
            ],
        },
        "InstanceInService": {
            "acceptors": [
                {
                    "argument": "InstanceStates[].State",
                    "expected": "InService",
                    "matcher": "pathAll",
                    "state": "success",
                },
                {"matcher": "error", "expected": "InvalidInstance", "state": "retry"},
            ],
            "delay": 15,
            "maxAttempts": 40,
            "operation": "DescribeInstanceHealth",
        },
        "LoadBalancerCreated": {
            "delay": 10,
            "maxAttempts": 60,
            "operation": "DescribeLoadBalancers",
            "acceptors": [
                {
                    "matcher": "path",
                    "expected": True,
                    "argument": "length(LoadBalancerDescriptions[]) > `0`",
                    "state": "success",
                },
                {
                    "matcher": "error",
                    "expected": "LoadBalancerNotFound",
                    "state": "retry",
                },
            ],
        },
        "LoadBalancerDeleted": {
            "delay": 10,
            "maxAttempts": 60,
            "operation": "DescribeLoadBalancers",
            "acceptors": [
                {
                    "matcher": "path",
                    "expected": True,
                    "argument": "length(LoadBalancerDescriptions[]) > `0`",
                    "state": "retry",
                },
                {
                    "matcher": "error",
                    "expected": "LoadBalancerNotFound",
                    "state": "success",
                },
            ],
        },
    },
}

elbv2_data = {
    "version": 2,
    "waiters": {
        "LoadBalancerAvailable": {
            "delay": 15,
            "operation": "DescribeLoadBalancers",
            "maxAttempts": 40,
            "acceptors": [
                {
                    "state": "success",
                    "matcher": "pathAll",
                    "argument": "LoadBalancers[].State.Code",
                    "expected": "active",
                },
                {
                    "state": "retry",
                    "matcher": "pathAny",
                    "argument": "LoadBalancers[].State.Code",
                    "expected": "provisioning",
                },
                {"state": "retry", "matcher": "error", "expected": "LoadBalancerNotFound"},
            ],
        },
        "LoadBalancerIpAddressTypeIpv4": {
            "delay": 15,
            "operation": "DescribeLoadBalancers",
            "maxAttempts": 40,
            "acceptors": [
                {
                    "state": "success",
                    "matcher": "pathAll",
                    "argument": "LoadBalancers[].IpAddressType",
                    "expected": "ipv4",
                },
                {
                    "state": "retry",
                    "matcher": "pathAny",
                    "argument": "LoadBalancers[].IpAddressType",
                    "expected": "dualstack",
                },
                {"state": "failure", "matcher": "error", "expected": "LoadBalancerNotFound"},
            ],
        },
        "LoadBalancerIpAddressTypeDualStack": {
            "delay": 15,
            "operation": "DescribeLoadBalancers",
            "maxAttempts": 40,
            "acceptors": [
                {
                    "state": "success",
                    "matcher": "pathAll",
                    "argument": "LoadBalancers[].IpAddressType",
                    "expected": "dualstack",
                },
                {
                    "state": "retry",
                    "matcher": "pathAny",
                    "argument": "LoadBalancers[].IpAddressType",
                    "expected": "ipv4",
                },
                {"state": "failure", "matcher": "error", "expected": "LoadBalancerNotFound"},
            ],
        },
        "LoadBalancersDeleted": {
            "delay": 15,
            "operation": "DescribeLoadBalancers",
            "maxAttempts": 40,
            "acceptors": [
                {
                    "state": "retry",
                    "matcher": "pathAll",
                    "argument": "LoadBalancers[].State.Code",
                    "expected": "active",
                },
                {"matcher": "error", "expected": "LoadBalancerNotFound", "state": "success"},
            ],
        },
    },
}


rds_data = {
    "version": 2,
    "waiters": {
        "DBInstanceStopped": {
            "delay": 20,
            "maxAttempts": 60,
            "operation": "DescribeDBInstances",
            "acceptors": [
                {
                    "state": "success",
                    "matcher": "pathAll",
                    "argument": "DBInstances[].DBInstanceStatus",
                    "expected": "stopped",
                },
            ],
        },
        "DBClusterAvailable": {
            "delay": 20,
            "maxAttempts": 60,
            "operation": "DescribeDBClusters",
            "acceptors": [
                {"state": "success", "matcher": "pathAll", "argument": "DBClusters[].Status", "expected": "available"},
                {"state": "retry", "matcher": "error", "expected": "DBClusterNotFoundFault"},
            ],
        },
        "DBClusterDeleted": {
            "delay": 20,
            "maxAttempts": 60,
            "operation": "DescribeDBClusters",
            "acceptors": [
                {"state": "success", "matcher": "pathAll", "argument": "DBClusters[].Status", "expected": "stopped"},
                {"state": "success", "matcher": "error", "expected": "DBClusterNotFoundFault"},
            ],
        },
        "ReadReplicaPromoted": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeDBInstances",
            "acceptors": [
                {
                    "state": "success",
                    "matcher": "path",
                    "argument": "length(DBInstances[].StatusInfos) == `0`",
                    "expected": True,
                },
                {
                    "state": "retry",
                    "matcher": "pathAny",
                    "argument": "DBInstances[].StatusInfos[].Status",
                    "expected": "replicating",
                },
            ],
        },
        "RoleAssociated": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeDBInstances",
            "acceptors": [
                {
                    "state": "success",
                    "matcher": "pathAll",
                    "argument": "DBInstances[].AssociatedRoles[].Status",
                    "expected": "ACTIVE",
                },
                {
                    "state": "retry",
                    "matcher": "pathAny",
                    "argument": "DBInstances[].AssociatedRoles[].Status",
                    "expected": "PENDING",
                },
            ],
        },
        "RoleDisassociated": {
            "delay": 5,
            "maxAttempts": 40,
            "operation": "DescribeDBInstances",
            "acceptors": [
                {
                    "state": "success",
                    "matcher": "pathAll",
                    "argument": "DBInstances[].AssociatedRoles[].Status",
                    "expected": "ACTIVE",
                },
                {
                    "state": "retry",
                    "matcher": "pathAny",
                    "argument": "DBInstances[].AssociatedRoles[].Status",
                    "expected": "PENDING",
                },
                {
                    "state": "success",
                    "matcher": "path",
                    "argument": "length(DBInstances[].AssociatedRoles[]) == `0`",
                    "expected": True,
                },
            ],
        },
    },
}


route53_data = {
    "version": 2,
    "waiters": {
        "ResourceRecordSetsChanged": {
            "delay": 30,
            "maxAttempts": 60,
            "operation": "GetChange",
            "acceptors": [
                {"matcher": "path", "expected": "INSYNC", "argument": "ChangeInfo.Status", "state": "success"}
            ],
        }
    },
}


def _inject_limit_retries(model):
    extra_retries = [
        "RequestLimitExceeded",
        "Unavailable",
        "ServiceUnavailable",
        "InternalFailure",
        "InternalError",
        "TooManyRequestsException",
        "Throttling",
    ]

    acceptors = []
    for error in extra_retries:
        acceptors.append({"state": "success", "matcher": "error", "expected": error})

    _model = copy.deepcopy(model)

    for waiter in model["waiters"]:
        _model["waiters"][waiter]["acceptors"].extend(acceptors)

    return _model


def ec2_model(name):
    ec2_models = core_waiter.WaiterModel(waiter_config=_inject_limit_retries(ec2_data))
    return ec2_models.get_waiter(name)


def waf_model(name):
    waf_models = core_waiter.WaiterModel(waiter_config=_inject_limit_retries(waf_data))
    return waf_models.get_waiter(name)


def eks_model(name):
    eks_models = core_waiter.WaiterModel(waiter_config=_inject_limit_retries(eks_data))
    return eks_models.get_waiter(name)


def elbv2_model(name):
    elbv2_models = core_waiter.WaiterModel(waiter_config=_inject_limit_retries(elbv2_data))
    return elbv2_models.get_waiter(name)


def elb_model(name):
    elb_models = core_waiter.WaiterModel(waiter_config=_inject_limit_retries(elb_data))
    return elb_models.get_waiter(name)


def rds_model(name):
    rds_models = core_waiter.WaiterModel(waiter_config=_inject_limit_retries(rds_data))
    return rds_models.get_waiter(name)


def route53_model(name):
    route53_models = core_waiter.WaiterModel(waiter_config=_inject_limit_retries(route53_data))
    return route53_models.get_waiter(name)


waiters_by_name = {
    ("EC2", "image_available"): lambda ec2: core_waiter.Waiter(
        "image_available", ec2_model("ImageAvailable"), core_waiter.NormalizedOperationMethod(ec2.describe_images)
    ),
    ("EC2", "internet_gateway_exists"): lambda ec2: core_waiter.Waiter(
        "internet_gateway_exists",
        ec2_model("InternetGatewayExists"),
        core_waiter.NormalizedOperationMethod(ec2.describe_internet_gateways),
    ),
    ("EC2", "internet_gateway_attached"): lambda ec2: core_waiter.Waiter(
        "internet_gateway_attached",
        ec2_model("InternetGatewayAttached"),
        core_waiter.NormalizedOperationMethod(ec2.describe_internet_gateways),
    ),
    ("EC2", "network_interface_attached"): lambda ec2: core_waiter.Waiter(
        "network_interface_attached",
        ec2_model("NetworkInterfaceAttached"),
        core_waiter.NormalizedOperationMethod(ec2.describe_network_interfaces),
    ),
    ("EC2", "network_interface_deleted"): lambda ec2: core_waiter.Waiter(
        "network_interface_deleted",
        ec2_model("NetworkInterfaceDeleted"),
        core_waiter.NormalizedOperationMethod(ec2.describe_network_interfaces),
    ),
    ("EC2", "network_interface_available"): lambda ec2: core_waiter.Waiter(
        "network_interface_available",
        ec2_model("NetworkInterfaceAvailable"),
        core_waiter.NormalizedOperationMethod(ec2.describe_network_interfaces),
    ),
    ("EC2", "network_interface_delete_on_terminate"): lambda ec2: core_waiter.Waiter(
        "network_interface_delete_on_terminate",
        ec2_model("NetworkInterfaceDeleteOnTerminate"),
        core_waiter.NormalizedOperationMethod(ec2.describe_network_interfaces),
    ),
    ("EC2", "network_interface_no_delete_on_terminate"): lambda ec2: core_waiter.Waiter(
        "network_interface_no_delete_on_terminate",
        ec2_model("NetworkInterfaceNoDeleteOnTerminate"),
        core_waiter.NormalizedOperationMethod(ec2.describe_network_interfaces),
    ),
    ("EC2", "route_table_exists"): lambda ec2: core_waiter.Waiter(
        "route_table_exists",
        ec2_model("RouteTableExists"),
        core_waiter.NormalizedOperationMethod(ec2.describe_route_tables),
    ),
    ("EC2", "security_group_exists"): lambda ec2: core_waiter.Waiter(
        "security_group_exists",
        ec2_model("SecurityGroupExists"),
        core_waiter.NormalizedOperationMethod(ec2.describe_security_groups),
    ),
    ("EC2", "snapshot_completed"): lambda ec2: core_waiter.Waiter(
        "snapshot_completed",
        ec2_model("SnapshotCompleted"),
        core_waiter.NormalizedOperationMethod(ec2.describe_snapshots),
    ),
    ("EC2", "subnet_available"): lambda ec2: core_waiter.Waiter(
        "subnet_available", ec2_model("SubnetAvailable"), core_waiter.NormalizedOperationMethod(ec2.describe_subnets)
    ),
    ("EC2", "subnet_exists"): lambda ec2: core_waiter.Waiter(
        "subnet_exists", ec2_model("SubnetExists"), core_waiter.NormalizedOperationMethod(ec2.describe_subnets)
    ),
    ("EC2", "subnet_has_map_public"): lambda ec2: core_waiter.Waiter(
        "subnet_has_map_public",
        ec2_model("SubnetHasMapPublic"),
        core_waiter.NormalizedOperationMethod(ec2.describe_subnets),
    ),
    ("EC2", "subnet_no_map_public"): lambda ec2: core_waiter.Waiter(
        "subnet_no_map_public",
        ec2_model("SubnetNoMapPublic"),
        core_waiter.NormalizedOperationMethod(ec2.describe_subnets),
    ),
    ("EC2", "subnet_has_assign_ipv6"): lambda ec2: core_waiter.Waiter(
        "subnet_has_assign_ipv6",
        ec2_model("SubnetHasAssignIpv6"),
        core_waiter.NormalizedOperationMethod(ec2.describe_subnets),
    ),
    ("EC2", "subnet_no_assign_ipv6"): lambda ec2: core_waiter.Waiter(
        "subnet_no_assign_ipv6",
        ec2_model("SubnetNoAssignIpv6"),
        core_waiter.NormalizedOperationMethod(ec2.describe_subnets),
    ),
    ("EC2", "subnet_deleted"): lambda ec2: core_waiter.Waiter(
        "subnet_deleted", ec2_model("SubnetDeleted"), core_waiter.NormalizedOperationMethod(ec2.describe_subnets)
    ),
    ("EC2", "vpc_available"): lambda ec2: core_waiter.Waiter(
        "vpc_available", ec2_model("VpcAvailable"), core_waiter.NormalizedOperationMethod(ec2.describe_vpcs)
    ),
    ("EC2", "vpc_exists"): lambda ec2: core_waiter.Waiter(
        "vpc_exists", ec2_model("VpcExists"), core_waiter.NormalizedOperationMethod(ec2.describe_vpcs)
    ),
    ("EC2", "vpc_endpoint_exists"): lambda ec2: core_waiter.Waiter(
        "vpc_endpoint_exists",
        ec2_model("VpcEndpointExists"),
        core_waiter.NormalizedOperationMethod(ec2.describe_vpc_endpoints),
    ),
    ("EC2", "vpn_gateway_exists"): lambda ec2: core_waiter.Waiter(
        "vpn_gateway_exists",
        ec2_model("VpnGatewayExists"),
        core_waiter.NormalizedOperationMethod(ec2.describe_vpn_gateways),
    ),
    ("EC2", "vpn_gateway_detached"): lambda ec2: core_waiter.Waiter(
        "vpn_gateway_detached",
        ec2_model("VpnGatewayDetached"),
        core_waiter.NormalizedOperationMethod(ec2.describe_vpn_gateways),
    ),
    ("EC2", "nat_gateway_deleted"): lambda ec2: core_waiter.Waiter(
        "nat_gateway_deleted",
        ec2_model("NatGatewayDeleted"),
        core_waiter.NormalizedOperationMethod(ec2.describe_nat_gateways),
    ),
    ("EC2", "nat_gateway_available"): lambda ec2: core_waiter.Waiter(
        "nat_gateway_available",
        ec2_model("NatGatewayAvailable"),
        core_waiter.NormalizedOperationMethod(ec2.describe_nat_gateways),
    ),
    ("WAF", "change_token_in_sync"): lambda waf: core_waiter.Waiter(
        "change_token_in_sync",
        waf_model("ChangeTokenInSync"),
        core_waiter.NormalizedOperationMethod(waf.get_change_token_status),
    ),
    ("WAFRegional", "change_token_in_sync"): lambda waf: core_waiter.Waiter(
        "change_token_in_sync",
        waf_model("ChangeTokenInSync"),
        core_waiter.NormalizedOperationMethod(waf.get_change_token_status),
    ),
    ("EKS", "cluster_active"): lambda eks: core_waiter.Waiter(
        "cluster_active", eks_model("ClusterActive"), core_waiter.NormalizedOperationMethod(eks.describe_cluster)
    ),
    ("EKS", "cluster_deleted"): lambda eks: core_waiter.Waiter(
        "cluster_deleted", eks_model("ClusterDeleted"), core_waiter.NormalizedOperationMethod(eks.describe_cluster)
    ),
    ("EKS", "fargate_profile_active"): lambda eks: core_waiter.Waiter(
        "fargate_profile_active",
        eks_model("FargateProfileActive"),
        core_waiter.NormalizedOperationMethod(eks.describe_fargate_profile),
    ),
    ("EKS", "fargate_profile_deleted"): lambda eks: core_waiter.Waiter(
        "fargate_profile_deleted",
        eks_model("FargateProfileDeleted"),
        core_waiter.NormalizedOperationMethod(eks.describe_fargate_profile),
    ),
    ("EKS", "nodegroup_active"): lambda eks: core_waiter.Waiter(
        "nodegroup_active", eks_model("NodegroupActive"), core_waiter.NormalizedOperationMethod(eks.describe_nodegroup)
    ),
    ("EKS", "nodegroup_deleted"): lambda eks: core_waiter.Waiter(
        "nodegroup_deleted",
        eks_model("NodegroupDeleted"),
        core_waiter.NormalizedOperationMethod(eks.describe_nodegroup),
    ),
    ("ElasticLoadBalancing", "any_instance_in_service"): lambda elb: core_waiter.Waiter(
        "any_instance_in_service",
        elb_model("AnyInstanceInService"),
        core_waiter.NormalizedOperationMethod(elb.describe_instance_health),
    ),
    ("ElasticLoadBalancing", "instance_deregistered"): lambda elb: core_waiter.Waiter(
        "instance_deregistered",
        elb_model("InstanceDeregistered"),
        core_waiter.NormalizedOperationMethod(elb.describe_instance_health),
    ),
    ("ElasticLoadBalancing", "instance_in_service"): lambda elb: core_waiter.Waiter(
        "load_balancer_created",
        elb_model("InstanceInService"),
        core_waiter.NormalizedOperationMethod(elb.describe_instance_health),
    ),
    ("ElasticLoadBalancing", "load_balancer_created"): lambda elb: core_waiter.Waiter(
        "load_balancer_created",
        elb_model("LoadBalancerCreated"),
        core_waiter.NormalizedOperationMethod(elb.describe_load_balancers),
    ),
    ("ElasticLoadBalancing", "load_balancer_deleted"): lambda elb: core_waiter.Waiter(
        "load_balancer_deleted",
        elb_model("LoadBalancerDeleted"),
        core_waiter.NormalizedOperationMethod(elb.describe_load_balancers),
    ),
    ("ElasticLoadBalancingv2", "load_balancer_available"): lambda elbv2: core_waiter.Waiter(
        "load_balancer_available",
        elbv2_model("LoadBalancerAvailable"),
        core_waiter.NormalizedOperationMethod(elbv2.describe_load_balancers),
    ),
    ("ElasticLoadBalancingv2", "load_balancer_ip_address_type_ipv4"): lambda elbv2: core_waiter.Waiter(
        "load_balancer_ip_address_type_ipv4",
        elbv2_model("LoadBalancerIpAddressTypeIpv4"),
        core_waiter.NormalizedOperationMethod(elbv2.describe_load_balancers),
    ),
    ("ElasticLoadBalancingv2", "load_balancer_ip_address_type_dualstack"): lambda elbv2: core_waiter.Waiter(
        "load_balancers_ip_address_type_dualstack",
        elbv2_model("LoadBalancerIpAddressTypeDualStack"),
        core_waiter.NormalizedOperationMethod(elbv2.describe_load_balancers),
    ),
    ("ElasticLoadBalancingv2", "load_balancers_deleted"): lambda elbv2: core_waiter.Waiter(
        "load_balancers_deleted",
        elbv2_model("LoadBalancersDeleted"),
        core_waiter.NormalizedOperationMethod(elbv2.describe_load_balancers),
    ),
    ("RDS", "db_instance_stopped"): lambda rds: core_waiter.Waiter(
        "db_instance_stopped",
        rds_model("DBInstanceStopped"),
        core_waiter.NormalizedOperationMethod(rds.describe_db_instances),
    ),
    ("RDS", "cluster_available"): lambda rds: core_waiter.Waiter(
        "cluster_available",
        rds_model("DBClusterAvailable"),
        core_waiter.NormalizedOperationMethod(rds.describe_db_clusters),
    ),
    ("RDS", "cluster_deleted"): lambda rds: core_waiter.Waiter(
        "cluster_deleted",
        rds_model("DBClusterDeleted"),
        core_waiter.NormalizedOperationMethod(rds.describe_db_clusters),
    ),
    ("RDS", "read_replica_promoted"): lambda rds: core_waiter.Waiter(
        "read_replica_promoted",
        rds_model("ReadReplicaPromoted"),
        core_waiter.NormalizedOperationMethod(rds.describe_db_instances),
    ),
    ("RDS", "role_associated"): lambda rds: core_waiter.Waiter(
        "role_associated", rds_model("RoleAssociated"), core_waiter.NormalizedOperationMethod(rds.describe_db_instances)
    ),
    ("RDS", "role_disassociated"): lambda rds: core_waiter.Waiter(
        "role_disassociated",
        rds_model("RoleDisassociated"),
        core_waiter.NormalizedOperationMethod(rds.describe_db_instances),
    ),
    ("Route53", "resource_record_sets_changed"): lambda route53: core_waiter.Waiter(
        "resource_record_sets_changed",
        route53_model("ResourceRecordSetsChanged"),
        core_waiter.NormalizedOperationMethod(route53.get_change),
    ),
}


def get_waiter(client, waiter_name):
    if isinstance(client, RetryingBotoClientWrapper):
        return get_waiter(client.client, waiter_name)
    try:
        return waiters_by_name[(client.__class__.__name__, waiter_name)](client)
    except KeyError:
        available_waiters = ", ".join(repr(k) for k in waiters_by_name.keys())
        raise NotImplementedError(
            f"Waiter {waiter_name} could not be found for client {type(client)}. Available waiters: {available_waiters}"
        )
