from __future__ import unicode_literals

import random

from moto.packages.boto.ec2.blockdevicemapping import (
    BlockDeviceType,
    BlockDeviceMapping,
)
from moto.ec2.exceptions import InvalidInstanceIdError

from collections import OrderedDict
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import camelcase_to_underscores
from moto.ec2 import ec2_backends
from moto.elb import elb_backends
from moto.elbv2 import elbv2_backends
from moto.elb.exceptions import LoadBalancerNotFoundError
from .exceptions import (
    AutoscalingClientError,
    ResourceContentionError,
    InvalidInstanceError,
    ValidationError,
)

# http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/AS_Concepts.html#Cooldown
DEFAULT_COOLDOWN = 300

ASG_NAME_TAG = "aws:autoscaling:groupName"


class InstanceState(object):
    def __init__(
        self,
        instance,
        lifecycle_state="InService",
        health_status="Healthy",
        protected_from_scale_in=False,
    ):
        self.instance = instance
        self.lifecycle_state = lifecycle_state
        self.health_status = health_status
        self.protected_from_scale_in = protected_from_scale_in


class FakeScalingPolicy(BaseModel):
    def __init__(
        self,
        name,
        policy_type,
        adjustment_type,
        as_name,
        scaling_adjustment,
        cooldown,
        autoscaling_backend,
    ):
        self.name = name
        self.policy_type = policy_type
        self.adjustment_type = adjustment_type
        self.as_name = as_name
        self.scaling_adjustment = scaling_adjustment
        if cooldown is not None:
            self.cooldown = cooldown
        else:
            self.cooldown = DEFAULT_COOLDOWN
        self.autoscaling_backend = autoscaling_backend

    def execute(self):
        if self.adjustment_type == "ExactCapacity":
            self.autoscaling_backend.set_desired_capacity(
                self.as_name, self.scaling_adjustment
            )
        elif self.adjustment_type == "ChangeInCapacity":
            self.autoscaling_backend.change_capacity(
                self.as_name, self.scaling_adjustment
            )
        elif self.adjustment_type == "PercentChangeInCapacity":
            self.autoscaling_backend.change_capacity_percent(
                self.as_name, self.scaling_adjustment
            )


class FakeLaunchConfiguration(CloudFormationModel):
    def __init__(
        self,
        name,
        image_id,
        key_name,
        ramdisk_id,
        kernel_id,
        security_groups,
        user_data,
        instance_type,
        instance_monitoring,
        instance_profile_name,
        spot_price,
        ebs_optimized,
        associate_public_ip_address,
        block_device_mapping_dict,
    ):
        self.name = name
        self.image_id = image_id
        self.key_name = key_name
        self.ramdisk_id = ramdisk_id
        self.kernel_id = kernel_id
        self.security_groups = security_groups if security_groups else []
        self.user_data = user_data
        self.instance_type = instance_type
        self.instance_monitoring = instance_monitoring
        self.instance_profile_name = instance_profile_name
        self.spot_price = spot_price
        self.ebs_optimized = ebs_optimized
        self.associate_public_ip_address = associate_public_ip_address
        self.block_device_mapping_dict = block_device_mapping_dict

    @classmethod
    def create_from_instance(cls, name, instance, backend):
        config = backend.create_launch_configuration(
            name=name,
            image_id=instance.image_id,
            kernel_id="",
            ramdisk_id="",
            key_name=instance.key_name,
            security_groups=instance.security_groups,
            user_data=instance.user_data,
            instance_type=instance.instance_type,
            instance_monitoring=False,
            instance_profile_name=None,
            spot_price=None,
            ebs_optimized=instance.ebs_optimized,
            associate_public_ip_address=instance.associate_public_ip,
            block_device_mappings=instance.block_device_mapping,
        )
        return config

    @staticmethod
    def cloudformation_name_type():
        return "LaunchConfigurationName"

    @staticmethod
    def cloudformation_type():
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-launchconfiguration.html
        return "AWS::AutoScaling::LaunchConfiguration"

    @classmethod
    def create_from_cloudformation_json(
        cls, resource_name, cloudformation_json, region_name
    ):
        properties = cloudformation_json["Properties"]

        instance_profile_name = properties.get("IamInstanceProfile")

        backend = autoscaling_backends[region_name]
        config = backend.create_launch_configuration(
            name=resource_name,
            image_id=properties.get("ImageId"),
            kernel_id=properties.get("KernelId"),
            ramdisk_id=properties.get("RamdiskId"),
            key_name=properties.get("KeyName"),
            security_groups=properties.get("SecurityGroups"),
            user_data=properties.get("UserData"),
            instance_type=properties.get("InstanceType"),
            instance_monitoring=properties.get("InstanceMonitoring"),
            instance_profile_name=instance_profile_name,
            spot_price=properties.get("SpotPrice"),
            ebs_optimized=properties.get("EbsOptimized"),
            associate_public_ip_address=properties.get("AssociatePublicIpAddress"),
            block_device_mappings=properties.get("BlockDeviceMapping.member"),
        )
        return config

    @classmethod
    def update_from_cloudformation_json(
        cls, original_resource, new_resource_name, cloudformation_json, region_name
    ):
        cls.delete_from_cloudformation_json(
            original_resource.name, cloudformation_json, region_name
        )
        return cls.create_from_cloudformation_json(
            new_resource_name, cloudformation_json, region_name
        )

    @classmethod
    def delete_from_cloudformation_json(
        cls, resource_name, cloudformation_json, region_name
    ):
        backend = autoscaling_backends[region_name]
        try:
            backend.delete_launch_configuration(resource_name)
        except KeyError:
            pass

    def delete(self, region_name):
        backend = autoscaling_backends[region_name]
        backend.delete_launch_configuration(self.name)

    @property
    def physical_resource_id(self):
        return self.name

    @property
    def block_device_mappings(self):
        if not self.block_device_mapping_dict:
            return None
        else:
            return self._parse_block_device_mappings()

    @property
    def instance_monitoring_enabled(self):
        if self.instance_monitoring:
            return "true"
        return "false"

    def _parse_block_device_mappings(self):
        block_device_map = BlockDeviceMapping()
        for mapping in self.block_device_mapping_dict:
            block_type = BlockDeviceType()
            mount_point = mapping.get("device_name")
            if "ephemeral" in mapping.get("virtual_name", ""):
                block_type.ephemeral_name = mapping.get("virtual_name")
            else:
                block_type.volume_type = mapping.get("ebs._volume_type")
                block_type.snapshot_id = mapping.get("ebs._snapshot_id")
                block_type.delete_on_termination = mapping.get(
                    "ebs._delete_on_termination"
                )
                block_type.size = mapping.get("ebs._volume_size")
                block_type.iops = mapping.get("ebs._iops")
            block_device_map[mount_point] = block_type
        return block_device_map


class FakeAutoScalingGroup(CloudFormationModel):
    def __init__(
        self,
        name,
        availability_zones,
        desired_capacity,
        max_size,
        min_size,
        launch_config_name,
        launch_template,
        vpc_zone_identifier,
        default_cooldown,
        health_check_period,
        health_check_type,
        load_balancers,
        target_group_arns,
        placement_group,
        termination_policies,
        autoscaling_backend,
        ec2_backend,
        tags,
        new_instances_protected_from_scale_in=False,
    ):
        self.autoscaling_backend = autoscaling_backend
        self.ec2_backend = ec2_backend
        self.name = name

        self._set_azs_and_vpcs(availability_zones, vpc_zone_identifier)

        self.max_size = max_size
        self.min_size = min_size

        self.launch_template = None
        self.launch_config = None

        self._set_launch_configuration(launch_config_name, launch_template)

        self.default_cooldown = (
            default_cooldown if default_cooldown else DEFAULT_COOLDOWN
        )
        self.health_check_period = health_check_period
        self.health_check_type = health_check_type if health_check_type else "EC2"
        self.load_balancers = load_balancers
        self.target_group_arns = target_group_arns
        self.placement_group = placement_group
        self.termination_policies = termination_policies
        self.new_instances_protected_from_scale_in = (
            new_instances_protected_from_scale_in
        )

        self.suspended_processes = []
        self.instance_states = []
        self.tags = tags if tags else []
        self.set_desired_capacity(desired_capacity)

    def active_instances(self):
        return [x for x in self.instance_states if x.lifecycle_state == "InService"]

    def _set_azs_and_vpcs(self, availability_zones, vpc_zone_identifier, update=False):
        # for updates, if only AZs are provided, they must not clash with
        # the AZs of existing VPCs
        if update and availability_zones and not vpc_zone_identifier:
            vpc_zone_identifier = self.vpc_zone_identifier

        if vpc_zone_identifier:
            # extract azs for vpcs
            subnet_ids = vpc_zone_identifier.split(",")
            subnets = self.autoscaling_backend.ec2_backend.get_all_subnets(
                subnet_ids=subnet_ids
            )
            vpc_zones = [subnet.availability_zone for subnet in subnets]

            if availability_zones and set(availability_zones) != set(vpc_zones):
                raise AutoscalingClientError(
                    "ValidationError",
                    "The availability zones of the specified subnets and the Auto Scaling group do not match",
                )
            availability_zones = vpc_zones
        elif not availability_zones:
            if not update:
                raise AutoscalingClientError(
                    "ValidationError",
                    "At least one Availability Zone or VPC Subnet is required.",
                )
            return

        self.availability_zones = availability_zones
        self.vpc_zone_identifier = vpc_zone_identifier

    def _set_launch_configuration(self, launch_config_name, launch_template):
        if launch_config_name:
            self.launch_config = self.autoscaling_backend.launch_configurations[
                launch_config_name
            ]
            self.launch_config_name = launch_config_name

        if launch_template:
            launch_template_id = launch_template.get("launch_template_id")
            launch_template_name = launch_template.get("launch_template_name")

            if not (launch_template_id or launch_template_name) or (
                launch_template_id and launch_template_name
            ):
                raise ValidationError(
                    "Valid requests must contain either launchTemplateId or LaunchTemplateName"
                )

            if launch_template_id:
                self.launch_template = self.ec2_backend.get_launch_template(
                    launch_template_id
                )
            elif launch_template_name:
                self.launch_template = self.ec2_backend.get_launch_template_by_name(
                    launch_template_name
                )
            self.launch_template_version = int(launch_template["version"])

    @staticmethod
    def __set_string_propagate_at_launch_booleans_on_tags(tags):
        bool_to_string = {True: "true", False: "false"}
        for tag in tags:
            if "PropagateAtLaunch" in tag:
                tag["PropagateAtLaunch"] = bool_to_string[tag["PropagateAtLaunch"]]
        return tags

    @staticmethod
    def cloudformation_name_type():
        return "AutoScalingGroupName"

    @staticmethod
    def cloudformation_type():
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-autoscaling-autoscalinggroup.html
        return "AWS::AutoScaling::AutoScalingGroup"

    @classmethod
    def create_from_cloudformation_json(
        cls, resource_name, cloudformation_json, region_name
    ):
        properties = cloudformation_json["Properties"]

        launch_config_name = properties.get("LaunchConfigurationName")
        launch_template = {
            camelcase_to_underscores(k): v
            for k, v in properties.get("LaunchTemplate", {}).items()
        }
        load_balancer_names = properties.get("LoadBalancerNames", [])
        target_group_arns = properties.get("TargetGroupARNs", [])

        backend = autoscaling_backends[region_name]
        group = backend.create_auto_scaling_group(
            name=resource_name,
            availability_zones=properties.get("AvailabilityZones", []),
            desired_capacity=properties.get("DesiredCapacity"),
            max_size=properties.get("MaxSize"),
            min_size=properties.get("MinSize"),
            launch_config_name=launch_config_name,
            launch_template=launch_template,
            vpc_zone_identifier=(
                ",".join(properties.get("VPCZoneIdentifier", [])) or None
            ),
            default_cooldown=properties.get("Cooldown"),
            health_check_period=properties.get("HealthCheckGracePeriod"),
            health_check_type=properties.get("HealthCheckType"),
            load_balancers=load_balancer_names,
            target_group_arns=target_group_arns,
            placement_group=None,
            termination_policies=properties.get("TerminationPolicies", []),
            tags=cls.__set_string_propagate_at_launch_booleans_on_tags(
                properties.get("Tags", [])
            ),
            new_instances_protected_from_scale_in=properties.get(
                "NewInstancesProtectedFromScaleIn", False
            ),
        )
        return group

    @classmethod
    def update_from_cloudformation_json(
        cls, original_resource, new_resource_name, cloudformation_json, region_name
    ):
        cls.delete_from_cloudformation_json(
            original_resource.name, cloudformation_json, region_name
        )
        return cls.create_from_cloudformation_json(
            new_resource_name, cloudformation_json, region_name
        )

    @classmethod
    def delete_from_cloudformation_json(
        cls, resource_name, cloudformation_json, region_name
    ):
        backend = autoscaling_backends[region_name]
        try:
            backend.delete_auto_scaling_group(resource_name)
        except KeyError:
            pass

    def delete(self, region_name):
        backend = autoscaling_backends[region_name]
        backend.delete_auto_scaling_group(self.name)

    @property
    def physical_resource_id(self):
        return self.name

    @property
    def image_id(self):
        if self.launch_template:
            version = self.launch_template.get_version(self.launch_template_version)
            return version.image_id

        return self.launch_config.image_id

    @property
    def instance_type(self):
        if self.launch_template:
            version = self.launch_template.get_version(self.launch_template_version)
            return version.instance_type

        return self.launch_config.instance_type

    @property
    def user_data(self):
        if self.launch_template:
            version = self.launch_template.get_version(self.launch_template_version)
            return version.user_data

        return self.launch_config.user_data

    @property
    def security_groups(self):
        if self.launch_template:
            version = self.launch_template.get_version(self.launch_template_version)
            return version.security_groups

        return self.launch_config.security_groups

    def update(
        self,
        availability_zones,
        desired_capacity,
        max_size,
        min_size,
        launch_config_name,
        launch_template,
        vpc_zone_identifier,
        default_cooldown,
        health_check_period,
        health_check_type,
        placement_group,
        termination_policies,
        new_instances_protected_from_scale_in=None,
    ):
        self._set_azs_and_vpcs(availability_zones, vpc_zone_identifier, update=True)

        if max_size is not None:
            self.max_size = max_size
        if min_size is not None:
            self.min_size = min_size

        if desired_capacity is None:
            if min_size is not None and min_size > len(self.instance_states):
                desired_capacity = min_size
            if max_size is not None and max_size < len(self.instance_states):
                desired_capacity = max_size

        self._set_launch_configuration(launch_config_name, launch_template)

        if health_check_period is not None:
            self.health_check_period = health_check_period
        if health_check_type is not None:
            self.health_check_type = health_check_type
        if new_instances_protected_from_scale_in is not None:
            self.new_instances_protected_from_scale_in = (
                new_instances_protected_from_scale_in
            )

        if desired_capacity is not None:
            self.set_desired_capacity(desired_capacity)

    def set_desired_capacity(self, new_capacity):
        if new_capacity is None:
            self.desired_capacity = self.min_size
        else:
            self.desired_capacity = new_capacity

        curr_instance_count = len(self.active_instances())

        if self.desired_capacity == curr_instance_count:
            pass  # Nothing to do here
        elif self.desired_capacity > curr_instance_count:
            # Need more instances
            count_needed = int(self.desired_capacity) - int(curr_instance_count)

            propagated_tags = self.get_propagated_tags()
            self.replace_autoscaling_group_instances(count_needed, propagated_tags)
        else:
            # Need to remove some instances
            count_to_remove = curr_instance_count - self.desired_capacity
            instances_to_remove = [  # only remove unprotected
                state
                for state in self.instance_states
                if not state.protected_from_scale_in
            ][:count_to_remove]
            if instances_to_remove:  # just in case not instances to remove
                instance_ids_to_remove = [
                    instance.instance.id for instance in instances_to_remove
                ]
                self.autoscaling_backend.ec2_backend.terminate_instances(
                    instance_ids_to_remove
                )
                self.instance_states = list(
                    set(self.instance_states) - set(instances_to_remove)
                )
        if self.name in self.autoscaling_backend.autoscaling_groups:
            self.autoscaling_backend.update_attached_elbs(self.name)
            self.autoscaling_backend.update_attached_target_groups(self.name)

    def get_propagated_tags(self):
        propagated_tags = {}
        for tag in self.tags:
            # boto uses 'propagate_at_launch
            # boto3 and cloudformation use PropagateAtLaunch
            if "propagate_at_launch" in tag and tag["propagate_at_launch"] == "true":
                propagated_tags[tag["key"]] = tag["value"]
            if "PropagateAtLaunch" in tag and tag["PropagateAtLaunch"] == "true":
                propagated_tags[tag["Key"]] = tag["Value"]
        return propagated_tags

    def replace_autoscaling_group_instances(self, count_needed, propagated_tags):
        propagated_tags[ASG_NAME_TAG] = self.name

        reservation = self.autoscaling_backend.ec2_backend.add_instances(
            self.image_id,
            count_needed,
            self.user_data,
            self.security_groups,
            instance_type=self.instance_type,
            tags={"instance": propagated_tags},
            placement=random.choice(self.availability_zones),
        )
        for instance in reservation.instances:
            instance.autoscaling_group = self
            self.instance_states.append(
                InstanceState(
                    instance,
                    protected_from_scale_in=self.new_instances_protected_from_scale_in,
                )
            )

    def append_target_groups(self, target_group_arns):
        append = [x for x in target_group_arns if x not in self.target_group_arns]
        self.target_group_arns.extend(append)


class AutoScalingBackend(BaseBackend):
    def __init__(self, ec2_backend, elb_backend, elbv2_backend):
        self.autoscaling_groups = OrderedDict()
        self.launch_configurations = OrderedDict()
        self.policies = {}
        self.ec2_backend = ec2_backend
        self.elb_backend = elb_backend
        self.elbv2_backend = elbv2_backend

    def reset(self):
        ec2_backend = self.ec2_backend
        elb_backend = self.elb_backend
        elbv2_backend = self.elbv2_backend
        self.__dict__ = {}
        self.__init__(ec2_backend, elb_backend, elbv2_backend)

    def create_launch_configuration(
        self,
        name,
        image_id,
        key_name,
        kernel_id,
        ramdisk_id,
        security_groups,
        user_data,
        instance_type,
        instance_monitoring,
        instance_profile_name,
        spot_price,
        ebs_optimized,
        associate_public_ip_address,
        block_device_mappings,
        instance_id=None,
    ):
        valid_requests = [
            instance_id is not None,
            image_id is not None and instance_type is not None,
        ]
        if not any(valid_requests):
            raise ValidationError(
                "Valid requests must contain either the InstanceID parameter or both the ImageId and InstanceType parameters."
            )
        if instance_id is not None:
            # TODO: https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-lc-with-instanceID.html
            pass
        launch_configuration = FakeLaunchConfiguration(
            name=name,
            image_id=image_id,
            key_name=key_name,
            kernel_id=kernel_id,
            ramdisk_id=ramdisk_id,
            security_groups=security_groups,
            user_data=user_data,
            instance_type=instance_type,
            instance_monitoring=instance_monitoring,
            instance_profile_name=instance_profile_name,
            spot_price=spot_price,
            ebs_optimized=ebs_optimized,
            associate_public_ip_address=associate_public_ip_address,
            block_device_mapping_dict=block_device_mappings,
        )
        self.launch_configurations[name] = launch_configuration
        return launch_configuration

    def describe_launch_configurations(self, names):
        configurations = self.launch_configurations.values()
        if names:
            return [
                configuration
                for configuration in configurations
                if configuration.name in names
            ]
        else:
            return list(configurations)

    def delete_launch_configuration(self, launch_configuration_name):
        self.launch_configurations.pop(launch_configuration_name, None)

    def create_auto_scaling_group(
        self,
        name,
        availability_zones,
        desired_capacity,
        max_size,
        min_size,
        launch_config_name,
        launch_template,
        vpc_zone_identifier,
        default_cooldown,
        health_check_period,
        health_check_type,
        load_balancers,
        target_group_arns,
        placement_group,
        termination_policies,
        tags,
        new_instances_protected_from_scale_in=False,
        instance_id=None,
    ):
        def make_int(value):
            return int(value) if value is not None else value

        max_size = make_int(max_size)
        min_size = make_int(min_size)
        desired_capacity = make_int(desired_capacity)
        default_cooldown = make_int(default_cooldown)
        if health_check_period is None:
            health_check_period = 300
        else:
            health_check_period = make_int(health_check_period)

        # TODO: Add MixedInstancesPolicy once implemented.
        # Verify only a single launch config-like parameter is provided.
        params = [launch_config_name, launch_template, instance_id]
        num_params = sum([1 for param in params if param])

        if num_params != 1:
            raise ValidationError(
                "Valid requests must contain either LaunchTemplate, LaunchConfigurationName, "
                "InstanceId or MixedInstancesPolicy parameter."
            )

        if instance_id:
            try:
                instance = self.ec2_backend.get_instance(instance_id)
                launch_config_name = name
                FakeLaunchConfiguration.create_from_instance(
                    launch_config_name, instance, self
                )
            except InvalidInstanceIdError:
                raise InvalidInstanceError(instance_id)

        group = FakeAutoScalingGroup(
            name=name,
            availability_zones=availability_zones,
            desired_capacity=desired_capacity,
            max_size=max_size,
            min_size=min_size,
            launch_config_name=launch_config_name,
            launch_template=launch_template,
            vpc_zone_identifier=vpc_zone_identifier,
            default_cooldown=default_cooldown,
            health_check_period=health_check_period,
            health_check_type=health_check_type,
            load_balancers=load_balancers,
            target_group_arns=target_group_arns,
            placement_group=placement_group,
            termination_policies=termination_policies,
            autoscaling_backend=self,
            ec2_backend=self.ec2_backend,
            tags=tags,
            new_instances_protected_from_scale_in=new_instances_protected_from_scale_in,
        )

        self.autoscaling_groups[name] = group
        self.update_attached_elbs(group.name)
        self.update_attached_target_groups(group.name)
        return group

    def update_auto_scaling_group(
        self,
        name,
        availability_zones,
        desired_capacity,
        max_size,
        min_size,
        launch_config_name,
        launch_template,
        vpc_zone_identifier,
        default_cooldown,
        health_check_period,
        health_check_type,
        placement_group,
        termination_policies,
        new_instances_protected_from_scale_in=None,
    ):
        # TODO: Add MixedInstancesPolicy once implemented.
        # Verify only a single launch config-like parameter is provided.
        if launch_config_name and launch_template:
            raise ValidationError(
                "Valid requests must contain either LaunchTemplate, LaunchConfigurationName "
                "or MixedInstancesPolicy parameter."
            )

        group = self.autoscaling_groups[name]
        group.update(
            availability_zones=availability_zones,
            desired_capacity=desired_capacity,
            max_size=max_size,
            min_size=min_size,
            launch_config_name=launch_config_name,
            launch_template=launch_template,
            vpc_zone_identifier=vpc_zone_identifier,
            default_cooldown=default_cooldown,
            health_check_period=health_check_period,
            health_check_type=health_check_type,
            placement_group=placement_group,
            termination_policies=termination_policies,
            new_instances_protected_from_scale_in=new_instances_protected_from_scale_in,
        )
        return group

    def describe_auto_scaling_groups(self, names):
        groups = self.autoscaling_groups.values()
        if names:
            return [group for group in groups if group.name in names]
        else:
            return list(groups)

    def delete_auto_scaling_group(self, group_name):
        self.set_desired_capacity(group_name, 0)
        self.autoscaling_groups.pop(group_name, None)

    def describe_auto_scaling_instances(self, instance_ids):
        instance_states = []
        for group in self.autoscaling_groups.values():
            instance_states.extend(
                [
                    x
                    for x in group.instance_states
                    if not instance_ids or x.instance.id in instance_ids
                ]
            )
        return instance_states

    def attach_instances(self, group_name, instance_ids):
        group = self.autoscaling_groups[group_name]
        original_size = len(group.instance_states)

        if (original_size + len(instance_ids)) > group.max_size:
            raise ResourceContentionError
        else:
            group.desired_capacity = original_size + len(instance_ids)
            new_instances = [
                InstanceState(
                    self.ec2_backend.get_instance(x),
                    protected_from_scale_in=group.new_instances_protected_from_scale_in,
                )
                for x in instance_ids
            ]
            for instance in new_instances:
                self.ec2_backend.create_tags(
                    [instance.instance.id], {ASG_NAME_TAG: group.name}
                )
            group.instance_states.extend(new_instances)
            self.update_attached_elbs(group.name)
            self.update_attached_target_groups(group.name)

    def set_instance_health(
        self, instance_id, health_status, should_respect_grace_period
    ):
        instance = self.ec2_backend.get_instance(instance_id)
        instance_state = next(
            instance_state
            for group in self.autoscaling_groups.values()
            for instance_state in group.instance_states
            if instance_state.instance.id == instance.id
        )
        instance_state.health_status = health_status

    def detach_instances(self, group_name, instance_ids, should_decrement):
        group = self.autoscaling_groups[group_name]
        original_size = group.desired_capacity

        detached_instances = [
            x for x in group.instance_states if x.instance.id in instance_ids
        ]
        for instance in detached_instances:
            self.ec2_backend.delete_tags(
                [instance.instance.id], {ASG_NAME_TAG: group.name}
            )

        new_instance_state = [
            x for x in group.instance_states if x.instance.id not in instance_ids
        ]
        group.instance_states = new_instance_state

        if should_decrement:
            group.desired_capacity = original_size - len(instance_ids)

        group.set_desired_capacity(group.desired_capacity)
        return detached_instances

    def set_desired_capacity(self, group_name, desired_capacity):
        group = self.autoscaling_groups[group_name]
        group.set_desired_capacity(desired_capacity)
        self.update_attached_elbs(group_name)

    def change_capacity(self, group_name, scaling_adjustment):
        group = self.autoscaling_groups[group_name]
        desired_capacity = group.desired_capacity + scaling_adjustment
        self.set_desired_capacity(group_name, desired_capacity)

    def change_capacity_percent(self, group_name, scaling_adjustment):
        """http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-scale-based-on-demand.html
        If PercentChangeInCapacity returns a value between 0 and 1,
        Auto Scaling will round it off to 1. If the PercentChangeInCapacity
        returns a value greater than 1, Auto Scaling will round it off to the
        lower value. For example, if PercentChangeInCapacity returns 12.5,
        then Auto Scaling will round it off to 12."""
        group = self.autoscaling_groups[group_name]
        percent_change = 1 + (scaling_adjustment / 100.0)
        desired_capacity = group.desired_capacity * percent_change
        if group.desired_capacity < desired_capacity < group.desired_capacity + 1:
            desired_capacity = group.desired_capacity + 1
        else:
            desired_capacity = int(desired_capacity)
        self.set_desired_capacity(group_name, desired_capacity)

    def create_autoscaling_policy(
        self, name, policy_type, adjustment_type, as_name, scaling_adjustment, cooldown
    ):
        policy = FakeScalingPolicy(
            name,
            policy_type,
            adjustment_type,
            as_name,
            scaling_adjustment,
            cooldown,
            self,
        )

        self.policies[name] = policy
        return policy

    def describe_policies(
        self, autoscaling_group_name=None, policy_names=None, policy_types=None
    ):
        return [
            policy
            for policy in self.policies.values()
            if (not autoscaling_group_name or policy.as_name == autoscaling_group_name)
            and (not policy_names or policy.name in policy_names)
            and (not policy_types or policy.policy_type in policy_types)
        ]

    def delete_policy(self, group_name):
        self.policies.pop(group_name, None)

    def execute_policy(self, group_name):
        policy = self.policies[group_name]
        policy.execute()

    def update_attached_elbs(self, group_name):
        group = self.autoscaling_groups[group_name]
        group_instance_ids = set(
            state.instance.id for state in group.active_instances()
        )

        # skip this if group.load_balancers is empty
        # otherwise elb_backend.describe_load_balancers returns all available load balancers
        if not group.load_balancers:
            return
        try:
            elbs = self.elb_backend.describe_load_balancers(names=group.load_balancers)
        except LoadBalancerNotFoundError:
            # ELBs can be deleted before their autoscaling group
            return

        for elb in elbs:
            elb_instace_ids = set(elb.instance_ids)
            self.elb_backend.register_instances(
                elb.name, group_instance_ids - elb_instace_ids, from_autoscaling=True,
            )
            self.elb_backend.deregister_instances(
                elb.name, elb_instace_ids - group_instance_ids, from_autoscaling=True,
            )

    def update_attached_target_groups(self, group_name):
        group = self.autoscaling_groups[group_name]
        group_instance_ids = set(state.instance.id for state in group.instance_states)

        # no action necessary if target_group_arns is empty
        if not group.target_group_arns:
            return

        target_groups = self.elbv2_backend.describe_target_groups(
            target_group_arns=group.target_group_arns,
            load_balancer_arn=None,
            names=None,
        )

        for target_group in target_groups:
            asg_targets = [
                {"id": x, "port": target_group.port} for x in group_instance_ids
            ]
            self.elbv2_backend.register_targets(target_group.arn, (asg_targets))

    def create_or_update_tags(self, tags):
        for tag in tags:
            group_name = tag["resource_id"]
            group = self.autoscaling_groups[group_name]
            old_tags = group.tags

            new_tags = []
            # if key was in old_tags, update old tag
            for old_tag in old_tags:
                if old_tag["key"] == tag["key"]:
                    new_tags.append(tag)
                else:
                    new_tags.append(old_tag)

            # if key was never in old_tag's add it (create tag)
            if not any(new_tag["key"] == tag["key"] for new_tag in new_tags):
                new_tags.append(tag)

            group.tags = new_tags

    def delete_tags(self, tags):
        for tag_to_delete in tags:
            group_name = tag_to_delete["resource_id"]
            key_to_delete = tag_to_delete["key"]
            group = self.autoscaling_groups[group_name]
            old_tags = group.tags
            group.tags = [x for x in old_tags if x["key"] != key_to_delete]

    def attach_load_balancers(self, group_name, load_balancer_names):
        group = self.autoscaling_groups[group_name]
        group.load_balancers.extend(
            [x for x in load_balancer_names if x not in group.load_balancers]
        )
        self.update_attached_elbs(group_name)

    def describe_load_balancers(self, group_name):
        return self.autoscaling_groups[group_name].load_balancers

    def detach_load_balancers(self, group_name, load_balancer_names):
        group = self.autoscaling_groups[group_name]
        group_instance_ids = set(state.instance.id for state in group.instance_states)
        elbs = self.elb_backend.describe_load_balancers(names=group.load_balancers)
        for elb in elbs:
            self.elb_backend.deregister_instances(
                elb.name, group_instance_ids, from_autoscaling=True
            )
        group.load_balancers = [
            x for x in group.load_balancers if x not in load_balancer_names
        ]

    def attach_load_balancer_target_groups(self, group_name, target_group_arns):
        group = self.autoscaling_groups[group_name]
        group.append_target_groups(target_group_arns)
        self.update_attached_target_groups(group_name)

    def describe_load_balancer_target_groups(self, group_name):
        return self.autoscaling_groups[group_name].target_group_arns

    def detach_load_balancer_target_groups(self, group_name, target_group_arns):
        group = self.autoscaling_groups[group_name]
        group.target_group_arns = [
            x for x in group.target_group_arns if x not in target_group_arns
        ]
        for target_group in target_group_arns:
            asg_targets = [{"id": x.instance.id} for x in group.instance_states]
            self.elbv2_backend.deregister_targets(target_group, (asg_targets))

    def suspend_processes(self, group_name, scaling_processes):
        all_proc_names = [
            "Launch",
            "Terminate",
            "AddToLoadBalancer",
            "AlarmNotification",
            "AZRebalance",
            "HealthCheck",
            "InstanceRefresh",
            "ReplaceUnhealthy",
            "ScheduledActions",
        ]
        group = self.autoscaling_groups[group_name]
        set_to_add = set(scaling_processes or all_proc_names)
        group.suspended_processes = list(
            set(group.suspended_processes).union(set_to_add)
        )

    def resume_processes(self, group_name, scaling_processes):
        group = self.autoscaling_groups[group_name]
        if scaling_processes:
            group.suspended_processes = list(
                set(group.suspended_processes).difference(set(scaling_processes))
            )
        else:
            group.suspended_processes = []

    def set_instance_protection(
        self, group_name, instance_ids, protected_from_scale_in
    ):
        group = self.autoscaling_groups[group_name]
        protected_instances = [
            x for x in group.instance_states if x.instance.id in instance_ids
        ]
        for instance in protected_instances:
            instance.protected_from_scale_in = protected_from_scale_in

    def notify_terminate_instances(self, instance_ids):
        for (
            autoscaling_group_name,
            autoscaling_group,
        ) in self.autoscaling_groups.items():
            original_active_instance_count = len(autoscaling_group.active_instances())
            autoscaling_group.instance_states = list(
                filter(
                    lambda i_state: i_state.instance.id not in instance_ids,
                    autoscaling_group.instance_states,
                )
            )
            difference = original_active_instance_count - len(
                autoscaling_group.active_instances()
            )
            if difference > 0:
                autoscaling_group.replace_autoscaling_group_instances(
                    difference, autoscaling_group.get_propagated_tags()
                )
                self.update_attached_elbs(autoscaling_group_name)

    def enter_standby_instances(self, group_name, instance_ids, should_decrement):
        group = self.autoscaling_groups[group_name]
        original_size = group.desired_capacity
        standby_instances = []
        for instance_state in group.instance_states:
            if instance_state.instance.id in instance_ids:
                instance_state.lifecycle_state = "Standby"
                standby_instances.append(instance_state)
        if should_decrement:
            group.desired_capacity = group.desired_capacity - len(instance_ids)
        group.set_desired_capacity(group.desired_capacity)
        return standby_instances, original_size, group.desired_capacity

    def exit_standby_instances(self, group_name, instance_ids):
        group = self.autoscaling_groups[group_name]
        original_size = group.desired_capacity
        standby_instances = []
        for instance_state in group.instance_states:
            if instance_state.instance.id in instance_ids:
                instance_state.lifecycle_state = "InService"
                standby_instances.append(instance_state)
        group.desired_capacity = group.desired_capacity + len(instance_ids)
        group.set_desired_capacity(group.desired_capacity)
        return standby_instances, original_size, group.desired_capacity

    def terminate_instance(self, instance_id, should_decrement):
        instance = self.ec2_backend.get_instance(instance_id)
        instance_state = next(
            instance_state
            for group in self.autoscaling_groups.values()
            for instance_state in group.instance_states
            if instance_state.instance.id == instance.id
        )
        group = instance.autoscaling_group
        original_size = group.desired_capacity
        self.detach_instances(group.name, [instance.id], should_decrement)
        self.ec2_backend.terminate_instances([instance.id])
        return instance_state, original_size, group.desired_capacity


autoscaling_backends = {}
for region, ec2_backend in ec2_backends.items():
    autoscaling_backends[region] = AutoScalingBackend(
        ec2_backend, elb_backends[region], elbv2_backends[region]
    )
