#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: Contributors to the Ansible project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = r"""
---
module: autoscaling_group
version_added: 5.0.0
short_description: Create or delete AWS AutoScaling Groups (ASGs)
description:
  - Can create or delete AWS AutoScaling Groups.
  - Can be used with the M(community.aws.autoscaling_launch_config) module to manage Launch Configurations.
  - Prior to release 5.0.0 this module was called C(community.aws.ec2_asg).
    The usage did not change.
  - This module was originally added to C(community.aws) in release 1.0.0.
author:
  - "Gareth Rushgrove (@garethr)"
options:
  state:
    description:
      - Register or deregister the instance.
    choices: ['present', 'absent']
    default: present
    type: str
  name:
    description:
      - Unique name for group to be created or deleted.
    required: true
    type: str
  load_balancers:
    description:
      - List of ELB names to use for the group. Use for classic load balancers.
    type: list
    elements: str
  target_group_arns:
    description:
      - List of target group ARNs to use for the group. Use for application load balancers.
    type: list
    elements: str
  availability_zones:
    description:
      - List of availability zone names in which to create the group.
      - Defaults to all the availability zones in the region if I(vpc_zone_identifier) is not set.
    type: list
    elements: str
  launch_config_name:
    description:
      - Name of the Launch configuration to use for the group. See the community.aws.autoscaling_launch_config) module for managing these.
      - If unspecified then the current group value will be used.  One of I(launch_config_name) or I(launch_template) must be provided.
    type: str
  launch_template:
    description:
      - Dictionary describing the Launch Template to use
    suboptions:
      version:
        description:
          - The version number of the launch template to use.
          - Defaults to latest version if not provided.
        type: str
      launch_template_name:
        description:
          - The name of the launch template. Only one of I(launch_template_name) or I(launch_template_id) is required.
        type: str
      launch_template_id:
        description:
          - The id of the launch template. Only one of I(launch_template_name) or I(launch_template_id) is required.
        type: str
    type: dict
  min_size:
    description:
      - Minimum number of instances in group, if unspecified then the current group value will be used.
    type: int
  max_size:
    description:
      - Maximum number of instances in group, if unspecified then the current group value will be used.
    type: int
  max_instance_lifetime:
    description:
      - The maximum amount of time, in seconds, that an instance can be in service.
      - Maximum instance lifetime must be equal to 0, between 604800 and 31536000 seconds (inclusive), or not specified.
      - Value of 0 removes lifetime restriction.
    type: int
  mixed_instances_policy:
    description:
      - A mixed instance policy to use for the ASG.
      - Only used when the ASG is configured to use a Launch Template (I(launch_template)).
      - 'See also U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-autoscalinggroup-mixedinstancespolicy.html)'
    required: false
    suboptions:
      instance_types:
        description:
          - A list of instance_types.
        type: list
        elements: str
        required: false
      instances_distribution:
        description:
          - >-
            Specifies the distribution of On-Demand Instances and Spot Instances, the maximum price
            to pay for Spot Instances, and how the Auto Scaling group allocates instance types
            to fulfill On-Demand and Spot capacity.
          - 'See also U(https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_InstancesDistribution.html)'
        required: false
        type: dict
        version_added: 1.5.0
        version_added_collection: community.aws
        suboptions:
          on_demand_allocation_strategy:
            description:
              - Indicates how to allocate instance types to fulfill On-Demand capacity.
            type: str
            required: false
            version_added: 1.5.0
            version_added_collection: community.aws
          on_demand_base_capacity:
            description:
              - >-
                The minimum amount of the Auto Scaling group's capacity that must be fulfilled by On-Demand
                Instances. This base portion is provisioned first as your group scales.
              - >-
                Default if not set is 0. If you leave it set to 0, On-Demand Instances are launched as a
                percentage of the Auto Scaling group's desired capacity, per the OnDemandPercentageAboveBaseCapacity setting.
            type: int
            required: false
            version_added: 1.5.0
            version_added_collection: community.aws
          on_demand_percentage_above_base_capacity:
            description:
              - Controls the percentages of On-Demand Instances and Spot Instances for your additional capacity beyond OnDemandBaseCapacity.
              - Default if not set is 100. If you leave it set to 100, the percentages are 100% for On-Demand Instances and 0% for Spot Instances.
              - 'Valid range: 0 to 100'
            type: int
            required: false
            version_added: 1.5.0
            version_added_collection: community.aws
          spot_allocation_strategy:
            description:
              - Indicates how to allocate instances across Spot Instance pools.
            type: str
            required: false
            version_added: 1.5.0
            version_added_collection: community.aws
          spot_instance_pools:
            description:
              - >-
                The number of Spot Instance pools across which to allocate your Spot Instances. The Spot pools are determined from
                the different instance types in the Overrides array of LaunchTemplate. Default if not set is 2.
              - Used only when the Spot allocation strategy is lowest-price.
              - 'Valid Range: Minimum value of 1. Maximum value of 20.'
            type: int
            required: false
            version_added: 1.5.0
            version_added_collection: community.aws
          spot_max_price:
            description:
              - The maximum price per unit hour that you are willing to pay for a Spot Instance.
              - If you leave the value of this parameter blank (which is the default), the maximum Spot price is set at the On-Demand price.
              - To remove a value that you previously set, include the parameter but leave the value blank.
            type: str
            required: false
            version_added: 1.5.0
            version_added_collection: community.aws
    type: dict
  placement_group:
    description:
      - Physical location of your cluster placement group created in Amazon EC2.
    type: str
  desired_capacity:
    description:
      - Desired number of instances in group, if unspecified then the current group value will be used.
    type: int
  replace_all_instances:
    description:
      - In a rolling fashion, replace all instances that used the old launch configuration with one from the new launch configuration.
        It increases the ASG size by I(replace_batch_size), waits for the new instances to be up and running.
        After that, it terminates a batch of old instances, waits for the replacements, and repeats, until all old instances are replaced.
        Once that's done the ASG size is reduced back to the expected size.
    default: false
    type: bool
  replace_batch_size:
    description:
      - Number of instances you'd like to replace at a time.  Used with I(replace_all_instances).
    required: false
    default: 1
    type: int
  replace_instances:
    description:
      - List of I(instance_ids) belonging to the named AutoScalingGroup that you would like to terminate and be replaced with instances
        matching the current launch configuration.
    type: list
    elements: str
    default: []
  detach_instances:
    description:
      - Removes one or more instances from the specified AutoScalingGroup.
      - If I(decrement_desired_capacity) flag is not set, new instance(s) are launched to replace the detached instance(s).
      - If a Classic Load Balancer is attached to the AutoScalingGroup, the instances are also deregistered from the load balancer.
      - If there are target groups attached to the AutoScalingGroup, the instances are also deregistered from the target groups.
    type: list
    elements: str
    default: []
    version_added: 3.2.0
    version_added_collection: community.aws
  decrement_desired_capacity:
    description:
      - Indicates whether the AutoScalingGroup decrements the desired capacity value by the number of instances detached.
    default: false
    type: bool
    version_added: 3.2.0
    version_added_collection: community.aws
  lc_check:
    description:
      - Check to make sure instances that are being replaced with I(replace_instances) do not already have the current I(launch_config).
    default: true
    type: bool
  lt_check:
    description:
      - Check to make sure instances that are being replaced with I(replace_instances) do not already have the current
        I(launch_template or I(launch_template) I(version).
    default: true
    type: bool
  vpc_zone_identifier:
    description:
      - List of VPC subnets to use
    type: list
    elements: str
  tags:
    description:
      - A list of tags to add to the Auto Scale Group.
      - Optional key is I(propagate_at_launch), which defaults to true.
      - When I(propagate_at_launch) is true the tags will be propagated to the Instances created.
    type: list
    elements: dict
    default: []
  purge_tags:
    description:
      - If C(true), existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter.
      - If the I(tags) parameter is not set then tags will not be modified.
    default: false
    type: bool
    version_added: 3.2.0
    version_added_collection: community.aws
  health_check_period:
    description:
      - Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health.
    required: false
    default: 300
    type: int
  health_check_type:
    description:
      - The service you want the health status from, Amazon EC2 or Elastic Load Balancer.
    required: false
    default: EC2
    choices: ['EC2', 'ELB']
    type: str
  default_cooldown:
    description:
      - The number of seconds after a scaling activity completes before another can begin.
    default: 300
    type: int
  wait_timeout:
    description:
      - How long to wait for instances to become viable when replaced.  If you experience the error "Waited too long for ELB instances to be healthy",
        try increasing this value.
    default: 300
    type: int
  wait_for_instances:
    description:
      - Wait for the ASG instances to be in a ready state before exiting.  If instances are behind an ELB, it will wait until the ELB determines all
        instances have a lifecycle_state of  "InService" and  a health_status of "Healthy".
    default: true
    type: bool
  termination_policies:
    description:
        - An ordered list of criteria used for selecting instances to be removed from the Auto Scaling group when reducing capacity.
        - Using I(termination_policies=Default) when modifying an existing AutoScalingGroup will result in the existing policy being retained
          instead of changed to C(Default).
        - 'Valid values include: C(Default), C(OldestInstance), C(NewestInstance), C(OldestLaunchConfiguration), C(ClosestToNextInstanceHour)'
        - 'Full documentation of valid values can be found in the AWS documentation:'
        - 'U(https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-instance-termination.html#custom-termination-policy)'
    default: Default
    type: list
    elements: str
  notification_topic:
    description:
      - A SNS topic ARN to send auto scaling notifications to.
    type: str
  notification_types:
    description:
      - A list of auto scaling events to trigger notifications on.
    default:
      - 'autoscaling:EC2_INSTANCE_LAUNCH'
      - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR'
      - 'autoscaling:EC2_INSTANCE_TERMINATE'
      - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR'
    required: false
    type: list
    elements: str
  suspend_processes:
    description:
      - A list of scaling processes to suspend.
      - 'Valid values include:'
      - C(Launch), C(Terminate), C(HealthCheck), C(ReplaceUnhealthy), C(AZRebalance), C(AlarmNotification), C(ScheduledActions), C(AddToLoadBalancer)
      - 'Full documentation of valid values can be found in the AWS documentation:'
      - 'U(https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html)'
    default: []
    type: list
    elements: str
  metrics_collection:
    description:
      - Enable ASG metrics collection.
    type: bool
    default: false
  metrics_granularity:
    description:
      - When I(metrics_collection=true) this will determine the granularity of metrics collected by CloudWatch.
    default: "1Minute"
    type: str
  metrics_list:
    description:
      - List of autoscaling metrics to collect when I(metrics_collection=true).
    default:
      - 'GroupMinSize'
      - 'GroupMaxSize'
      - 'GroupDesiredCapacity'
      - 'GroupInServiceInstances'
      - 'GroupPendingInstances'
      - 'GroupStandbyInstances'
      - 'GroupTerminatingInstances'
      - 'GroupTotalInstances'
    type: list
    elements: str
extends_documentation_fragment:
  - amazon.aws.common.modules
  - amazon.aws.region.modules
  - amazon.aws.boto3
"""

EXAMPLES = r"""
# Basic configuration with Launch Configuration

- amazon.aws.autoscaling_group:
    name: special
    load_balancers: ['lb1', 'lb2']
    availability_zones: ['eu-west-1a', 'eu-west-1b']
    launch_config_name: 'lc-1'
    min_size: 1
    max_size: 10
    desired_capacity: 5
    vpc_zone_identifier: ['subnet-abcd1234', 'subnet-1a2b3c4d']
    tags:
      - environment: production
        propagate_at_launch: false

# Rolling ASG Updates

# Below is an example of how to assign a new launch config to an ASG and terminate old instances.
#
# All instances in "myasg" that do not have the launch configuration named "my_new_lc" will be terminated in
# a rolling fashion with instances using the current launch configuration, "my_new_lc".
#
# This could also be considered a rolling deploy of a pre-baked AMI.
#
# If this is a newly created group, the instances will not be replaced since all instances
# will have the current launch configuration.

- name: create launch config
  community.aws.autoscaling_launch_config:
    name: my_new_lc
    image_id: ami-lkajsf
    key_name: mykey
    region: us-east-1
    security_groups: sg-23423
    instance_type: m1.small
    assign_public_ip: true

- amazon.aws.autoscaling_group:
    name: myasg
    launch_config_name: my_new_lc
    health_check_period: 60
    health_check_type: ELB
    replace_all_instances: true
    min_size: 5
    max_size: 5
    desired_capacity: 5
    region: us-east-1

# To only replace a couple of instances instead of all of them, supply a list
# to "replace_instances":

- amazon.aws.autoscaling_group:
    name: myasg
    launch_config_name: my_new_lc
    health_check_period: 60
    health_check_type: ELB
    replace_instances:
      - i-b345231
      - i-24c2931
    min_size: 5
    max_size: 5
    desired_capacity: 5
    region: us-east-1

# Basic Configuration with Launch Template

- amazon.aws.autoscaling_group:
    name: special
    load_balancers: ['lb1', 'lb2']
    availability_zones: ['eu-west-1a', 'eu-west-1b']
    launch_template:
      version: '1'
      launch_template_name: 'lt-example'
      launch_template_id: 'lt-123456'
    min_size: 1
    max_size: 10
    desired_capacity: 5
    vpc_zone_identifier: ['subnet-abcd1234', 'subnet-1a2b3c4d']
    tags:
      - environment: production
        propagate_at_launch: false

# Basic Configuration with Launch Template using mixed instance policy

- amazon.aws.autoscaling_group:
    name: special
    load_balancers: ['lb1', 'lb2']
    availability_zones: ['eu-west-1a', 'eu-west-1b']
    launch_template:
      version: '1'
      launch_template_name: 'lt-example'
      launch_template_id: 'lt-123456'
    mixed_instances_policy:
      instance_types:
        - t3a.large
        - t3.large
        - t2.large
      instances_distribution:
        on_demand_percentage_above_base_capacity: 0
        spot_allocation_strategy: capacity-optimized
    min_size: 1
    max_size: 10
    desired_capacity: 5
    vpc_zone_identifier: ['subnet-abcd1234', 'subnet-1a2b3c4d']
    tags:
      - environment: production
        propagate_at_launch: false
"""

RETURN = r"""
---
auto_scaling_group_name:
    description: The unique name of the auto scaling group
    returned: success
    type: str
    sample: "myasg"
auto_scaling_group_arn:
    description: The unique ARN of the autoscaling group
    returned: success
    type: str
    sample: "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:6a09ad6d-eeee-1234-b987-ee123ced01ad:autoScalingGroupName/myasg"
availability_zones:
    description: The availability zones for the auto scaling group
    returned: success
    type: list
    sample: [
        "us-east-1d"
    ]
created_time:
    description: Timestamp of create time of the auto scaling group
    returned: success
    type: str
    sample: "2017-11-08T14:41:48.272000+00:00"
default_cooldown:
    description: The default cooldown time in seconds.
    returned: success
    type: int
    sample: 300
desired_capacity:
    description: The number of EC2 instances that should be running in this group.
    returned: success
    type: int
    sample: 3
healthcheck_period:
    description: Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health.
    returned: success
    type: int
    sample: 30
healthcheck_type:
    description: The service you want the health status from, one of "EC2" or "ELB".
    returned: success
    type: str
    sample: "ELB"
healthy_instances:
    description: Number of instances in a healthy state
    returned: success
    type: int
    sample: 5
in_service_instances:
    description: Number of instances in service
    returned: success
    type: int
    sample: 3
instance_facts:
    description: Dictionary of EC2 instances and their status as it relates to the ASG.
    returned: success
    type: dict
    sample: {
        "i-0123456789012": {
            "health_status": "Healthy",
            "launch_config_name": "public-webapp-production-1",
            "lifecycle_state": "InService"
        }
    }
instances:
    description: list of instance IDs in the ASG
    returned: success
    type: list
    sample: [
        "i-0123456789012"
    ]
launch_config_name:
    description: >
      Name of launch configuration associated with the ASG. Same as launch_configuration_name,
      provided for compatibility with M(amazon.aws.autoscaling_group) module.
    returned: success
    type: str
    sample: "public-webapp-production-1"
load_balancers:
    description: List of load balancers names attached to the ASG.
    returned: success
    type: list
    sample: ["elb-webapp-prod"]
max_instance_lifetime:
    description: The maximum amount of time, in seconds, that an instance can be in service.
    returned: success
    type: int
    sample: 604800
max_size:
    description: Maximum size of group
    returned: success
    type: int
    sample: 3
min_size:
    description: Minimum size of group
    returned: success
    type: int
    sample: 1
mixed_instances_policy:
    description: Returns the list of instance types if a mixed instances policy is set.
    returned: success
    type: list
    sample: ["t3.micro", "t3a.micro"]
mixed_instances_policy_full:
    description: Returns the full dictionary representation of the mixed instances policy if a mixed instances policy is set.
    returned: success
    type: dict
    sample: {
        "instances_distribution": {
            "on_demand_allocation_strategy": "prioritized",
            "on_demand_base_capacity": 0,
            "on_demand_percentage_above_base_capacity": 0,
            "spot_allocation_strategy": "capacity-optimized"
        },
        "launch_template": {
            "launch_template_specification": {
                "launch_template_id": "lt-53c2425cffa544c23",
                "launch_template_name": "random-LaunchTemplate",
                "version": "2"
            },
            "overrides": [
                {
                    "instance_type": "m5.xlarge"
                },
                {
                    "instance_type": "m5a.xlarge"
                },
            ]
        }
    }
pending_instances:
    description: Number of instances in pending state
    returned: success
    type: int
    sample: 1
tags:
    description: List of tags for the ASG, and whether or not each tag propagates to instances at launch.
    returned: success
    type: list
    sample: [
        {
            "key": "Name",
            "value": "public-webapp-production-1",
            "resource_id": "public-webapp-production-1",
            "resource_type": "auto-scaling-group",
            "propagate_at_launch": "true"
        },
        {
            "key": "env",
            "value": "production",
            "resource_id": "public-webapp-production-1",
            "resource_type": "auto-scaling-group",
            "propagate_at_launch": "true"
        }
    ]
target_group_arns:
    description: List of ARNs of the target groups that the ASG populates
    returned: success
    type: list
    sample: [
        "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-host-hello/1a2b3c4d5e6f1a2b",
        "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-path-world/abcd1234abcd1234"
    ]
target_group_names:
    description: List of names of the target groups that the ASG populates
    returned: success
    type: list
    sample: [
        "target-group-host-hello",
        "target-group-path-world"
    ]
termination_policies:
    description: A list of termination policies for the group.
    returned: success
    type: list
    sample: ["Default"]
unhealthy_instances:
    description: Number of instances in an unhealthy state
    returned: success
    type: int
    sample: 0
viable_instances:
    description: Number of instances in a viable state
    returned: success
    type: int
    sample: 1
vpc_zone_identifier:
    description: VPC zone ID / subnet id for the auto scaling group
    returned: success
    type: str
    sample: "subnet-a31ef45f"
metrics_collection:
    description: List of enabled AutosSalingGroup metrics
    returned: success
    type: list
    sample: [
        {
            "Granularity": "1Minute",
            "Metric": "GroupInServiceInstances"
        }
    ]
"""

import time

try:
    import botocore
except ImportError:
    pass  # Handled by AnsibleAWSModule

from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict

from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.transformation import scrub_none_parameters

backoff_params = dict(retries=10, delay=3, backoff=1.5)


@AWSRetry.jittered_backoff(**backoff_params)
def describe_autoscaling_groups(connection, group_name):
    pg = connection.get_paginator("describe_auto_scaling_groups")
    return pg.paginate(AutoScalingGroupNames=[group_name]).build_full_result().get("AutoScalingGroups", [])


@AWSRetry.jittered_backoff(**backoff_params)
def deregister_lb_instances(connection, lb_name, instance_id):
    connection.deregister_instances_from_load_balancer(
        LoadBalancerName=lb_name, Instances=[dict(InstanceId=instance_id)]
    )


@AWSRetry.jittered_backoff(**backoff_params)
def describe_instance_health(connection, lb_name, instances):
    params = dict(LoadBalancerName=lb_name)
    if instances:
        params.update(Instances=instances)
    return connection.describe_instance_health(**params)


@AWSRetry.jittered_backoff(**backoff_params)
def describe_target_health(connection, target_group_arn, instances):
    return connection.describe_target_health(TargetGroupArn=target_group_arn, Targets=instances)


@AWSRetry.jittered_backoff(**backoff_params)
def suspend_asg_processes(connection, asg_name, processes):
    connection.suspend_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes)


@AWSRetry.jittered_backoff(**backoff_params)
def resume_asg_processes(connection, asg_name, processes):
    connection.resume_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes)


@AWSRetry.jittered_backoff(**backoff_params)
def describe_launch_configurations(connection, launch_config_name):
    pg = connection.get_paginator("describe_launch_configurations")
    return pg.paginate(LaunchConfigurationNames=[launch_config_name]).build_full_result()


@AWSRetry.jittered_backoff(**backoff_params)
def describe_launch_templates(connection, launch_template):
    if launch_template["launch_template_id"] is not None:
        try:
            lt = connection.describe_launch_templates(LaunchTemplateIds=[launch_template["launch_template_id"]])
            return lt
        except is_boto3_error_code("InvalidLaunchTemplateName.NotFoundException"):
            module.fail_json(msg=f"No launch template found matching: {launch_template}")
    else:
        try:
            lt = connection.describe_launch_templates(LaunchTemplateNames=[launch_template["launch_template_name"]])
            return lt
        except is_boto3_error_code("InvalidLaunchTemplateName.NotFoundException"):
            module.fail_json(msg=f"No launch template found matching: {launch_template}")


@AWSRetry.jittered_backoff(**backoff_params)
def create_asg(connection, **params):
    connection.create_auto_scaling_group(**params)


@AWSRetry.jittered_backoff(**backoff_params)
def put_notification_config(connection, asg_name, topic_arn, notification_types):
    connection.put_notification_configuration(
        AutoScalingGroupName=asg_name, TopicARN=topic_arn, NotificationTypes=notification_types
    )


@AWSRetry.jittered_backoff(**backoff_params)
def del_notification_config(connection, asg_name, topic_arn):
    connection.delete_notification_configuration(AutoScalingGroupName=asg_name, TopicARN=topic_arn)


@AWSRetry.jittered_backoff(**backoff_params)
def attach_load_balancers(connection, asg_name, load_balancers):
    connection.attach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers)


@AWSRetry.jittered_backoff(**backoff_params)
def detach_load_balancers(connection, asg_name, load_balancers):
    connection.detach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers)


@AWSRetry.jittered_backoff(**backoff_params)
def attach_lb_target_groups(connection, asg_name, target_group_arns):
    connection.attach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns)


@AWSRetry.jittered_backoff(**backoff_params)
def detach_lb_target_groups(connection, asg_name, target_group_arns):
    connection.detach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns)


@AWSRetry.jittered_backoff(**backoff_params)
def update_asg(connection, **params):
    connection.update_auto_scaling_group(**params)


@AWSRetry.jittered_backoff(catch_extra_error_codes=["ScalingActivityInProgress"], **backoff_params)
def delete_asg(connection, asg_name, force_delete):
    connection.delete_auto_scaling_group(AutoScalingGroupName=asg_name, ForceDelete=force_delete)


@AWSRetry.jittered_backoff(**backoff_params)
def terminate_asg_instance(connection, instance_id, decrement_capacity):
    connection.terminate_instance_in_auto_scaling_group(
        InstanceId=instance_id, ShouldDecrementDesiredCapacity=decrement_capacity
    )


@AWSRetry.jittered_backoff(**backoff_params)
def detach_asg_instances(connection, instance_ids, as_group_name, decrement_capacity):
    connection.detach_instances(
        InstanceIds=instance_ids, AutoScalingGroupName=as_group_name, ShouldDecrementDesiredCapacity=decrement_capacity
    )


def enforce_required_arguments_for_create():
    """As many arguments are not required for autoscale group deletion
    they cannot be mandatory arguments for the module, so we enforce
    them here"""
    missing_args = []
    if module.params.get("launch_config_name") is None and module.params.get("launch_template") is None:
        module.fail_json(msg="Missing either launch_config_name or launch_template for autoscaling group create")
    for arg in ("min_size", "max_size"):
        if module.params[arg] is None:
            missing_args.append(arg)
    if missing_args:
        module.fail_json(msg=f"Missing required arguments for autoscaling group create: {','.join(missing_args)}")


def get_properties(autoscaling_group):
    properties = dict(
        healthy_instances=0,
        in_service_instances=0,
        unhealthy_instances=0,
        pending_instances=0,
        viable_instances=0,
        terminating_instances=0,
    )
    instance_facts = dict()
    autoscaling_group_instances = autoscaling_group.get("Instances")

    if autoscaling_group_instances:
        properties["instances"] = [i["InstanceId"] for i in autoscaling_group_instances]
        for i in autoscaling_group_instances:
            instance_facts[i["InstanceId"]] = {
                "health_status": i["HealthStatus"],
                "lifecycle_state": i["LifecycleState"],
            }
            if "LaunchConfigurationName" in i:
                instance_facts[i["InstanceId"]]["launch_config_name"] = i["LaunchConfigurationName"]
            elif "LaunchTemplate" in i:
                instance_facts[i["InstanceId"]]["launch_template"] = i["LaunchTemplate"]

            if i["HealthStatus"] == "Healthy" and i["LifecycleState"] == "InService":
                properties["viable_instances"] += 1

            if i["HealthStatus"] == "Healthy":
                properties["healthy_instances"] += 1
            else:
                properties["unhealthy_instances"] += 1

            if i["LifecycleState"] == "InService":
                properties["in_service_instances"] += 1
            if i["LifecycleState"] == "Terminating":
                properties["terminating_instances"] += 1
            if i["LifecycleState"] == "Pending":
                properties["pending_instances"] += 1
    else:
        properties["instances"] = []

    properties["auto_scaling_group_name"] = autoscaling_group.get("AutoScalingGroupName")
    properties["auto_scaling_group_arn"] = autoscaling_group.get("AutoScalingGroupARN")
    properties["availability_zones"] = autoscaling_group.get("AvailabilityZones")
    properties["created_time"] = autoscaling_group.get("CreatedTime")
    properties["instance_facts"] = instance_facts
    properties["load_balancers"] = autoscaling_group.get("LoadBalancerNames")
    if "LaunchConfigurationName" in autoscaling_group:
        properties["launch_config_name"] = autoscaling_group.get("LaunchConfigurationName")
    else:
        properties["launch_template"] = autoscaling_group.get("LaunchTemplate")
    properties["tags"] = autoscaling_group.get("Tags")
    properties["max_instance_lifetime"] = autoscaling_group.get("MaxInstanceLifetime")
    properties["min_size"] = autoscaling_group.get("MinSize")
    properties["max_size"] = autoscaling_group.get("MaxSize")
    properties["desired_capacity"] = autoscaling_group.get("DesiredCapacity")
    properties["default_cooldown"] = autoscaling_group.get("DefaultCooldown")
    properties["healthcheck_grace_period"] = autoscaling_group.get("HealthCheckGracePeriod")
    properties["healthcheck_type"] = autoscaling_group.get("HealthCheckType")
    properties["default_cooldown"] = autoscaling_group.get("DefaultCooldown")
    properties["termination_policies"] = autoscaling_group.get("TerminationPolicies")
    properties["target_group_arns"] = autoscaling_group.get("TargetGroupARNs")
    properties["vpc_zone_identifier"] = autoscaling_group.get("VPCZoneIdentifier")
    raw_mixed_instance_object = autoscaling_group.get("MixedInstancesPolicy")
    if raw_mixed_instance_object:
        properties["mixed_instances_policy_full"] = camel_dict_to_snake_dict(raw_mixed_instance_object)
        properties["mixed_instances_policy"] = [
            x["InstanceType"] for x in raw_mixed_instance_object.get("LaunchTemplate").get("Overrides")
        ]

    metrics = autoscaling_group.get("EnabledMetrics")
    if metrics:
        metrics.sort(key=lambda x: x["Metric"])
    properties["metrics_collection"] = metrics

    if properties["target_group_arns"]:
        elbv2_connection = module.client("elbv2")
        tg_paginator = elbv2_connection.get_paginator("describe_target_groups")
        # Limit of 20 similar to https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeLoadBalancers.html
        tg_chunk_size = 20
        properties["target_group_names"] = []
        tg_chunks = [
            properties["target_group_arns"][i: i + tg_chunk_size]
            for i in range(0, len(properties["target_group_arns"]), tg_chunk_size)
        ]  # fmt: skip
        for chunk in tg_chunks:
            tg_result = tg_paginator.paginate(TargetGroupArns=chunk).build_full_result()
            properties["target_group_names"].extend([tg["TargetGroupName"] for tg in tg_result["TargetGroups"]])
    else:
        properties["target_group_names"] = []

    return properties


def get_launch_object(connection, ec2_connection):
    launch_object = dict()
    launch_config_name = module.params.get("launch_config_name")
    launch_template = module.params.get("launch_template")
    mixed_instances_policy = module.params.get("mixed_instances_policy")
    if launch_config_name is None and launch_template is None:
        return launch_object
    elif launch_config_name:
        try:
            launch_configs = describe_launch_configurations(connection, launch_config_name)
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            module.fail_json_aws(e, msg="Failed to describe launch configurations")
        if len(launch_configs["LaunchConfigurations"]) == 0:
            module.fail_json(msg=f"No launch config found with name {launch_config_name}")
        launch_object = {
            "LaunchConfigurationName": launch_configs["LaunchConfigurations"][0]["LaunchConfigurationName"]
        }
        return launch_object
    elif launch_template:
        lt = describe_launch_templates(ec2_connection, launch_template)["LaunchTemplates"][0]
        if launch_template["version"] is not None:
            launch_object = {
                "LaunchTemplate": {"LaunchTemplateId": lt["LaunchTemplateId"], "Version": launch_template["version"]}
            }
        else:
            launch_object = {
                "LaunchTemplate": {
                    "LaunchTemplateId": lt["LaunchTemplateId"],
                    "Version": str(lt["LatestVersionNumber"]),
                }
            }

        if mixed_instances_policy:
            instance_types = mixed_instances_policy.get("instance_types", [])
            instances_distribution = mixed_instances_policy.get("instances_distribution", {})
            policy = {"LaunchTemplate": {"LaunchTemplateSpecification": launch_object["LaunchTemplate"]}}
            if instance_types:
                policy["LaunchTemplate"]["Overrides"] = []
                for instance_type in instance_types:
                    instance_type_dict = {"InstanceType": instance_type}
                    policy["LaunchTemplate"]["Overrides"].append(instance_type_dict)
            if instances_distribution:
                instances_distribution_params = scrub_none_parameters(instances_distribution)
                policy["InstancesDistribution"] = snake_dict_to_camel_dict(
                    instances_distribution_params, capitalize_first=True
                )
            launch_object["MixedInstancesPolicy"] = policy
        return launch_object


def elb_dreg(asg_connection, group_name, instance_id):
    as_group = describe_autoscaling_groups(asg_connection, group_name)[0]
    wait_timeout = module.params.get("wait_timeout")
    count = 1
    if as_group["LoadBalancerNames"] and as_group["HealthCheckType"] == "ELB":
        elb_connection = module.client("elb")
    else:
        return

    for lb in as_group["LoadBalancerNames"]:
        deregister_lb_instances(elb_connection, lb, instance_id)
        module.debug(f"De-registering {instance_id} from ELB {lb}")

    wait_timeout = time.time() + wait_timeout
    while wait_timeout > time.time() and count > 0:
        count = 0
        for lb in as_group["LoadBalancerNames"]:
            lb_instances = describe_instance_health(elb_connection, lb, [])
            for i in lb_instances["InstanceStates"]:
                if i["InstanceId"] == instance_id and i["State"] == "InService":
                    count += 1
                    module.debug(f"{i['InstanceId']}: {i['State']}, {i['Description']}")
        time.sleep(10)

    if wait_timeout <= time.time():
        # waiting took too long
        module.fail_json(msg=f"Waited too long for instance to deregister. {time.asctime()}")


def elb_healthy(asg_connection, elb_connection, group_name):
    healthy_instances = set()
    as_group = describe_autoscaling_groups(asg_connection, group_name)[0]
    props = get_properties(as_group)
    # get healthy, inservice instances from ASG
    instances = []
    for instance, settings in props["instance_facts"].items():
        if settings["lifecycle_state"] == "InService" and settings["health_status"] == "Healthy":
            instances.append(dict(InstanceId=instance))
    module.debug(f"ASG considers the following instances InService and Healthy: {instances}")
    module.debug("ELB instance status:")
    lb_instances = list()
    for lb in as_group.get("LoadBalancerNames"):
        # we catch a race condition that sometimes happens if the instance exists in the ASG
        # but has not yet show up in the ELB
        try:
            lb_instances = describe_instance_health(elb_connection, lb, instances)
        except is_boto3_error_code("InvalidInstance"):
            return None
        except (
            botocore.exceptions.ClientError,
            botocore.exceptions.BotoCoreError,
        ) as e:  # pylint: disable=duplicate-except
            module.fail_json_aws(e, msg="Failed to get load balancer.")

        for i in lb_instances.get("InstanceStates"):
            if i["State"] == "InService":
                healthy_instances.add(i["InstanceId"])
            module.debug(f"ELB Health State {i['InstanceId']}: {i['State']}")
    return len(healthy_instances)


def tg_healthy(asg_connection, elbv2_connection, group_name):
    healthy_instances = set()
    as_group = describe_autoscaling_groups(asg_connection, group_name)[0]
    props = get_properties(as_group)
    # get healthy, inservice instances from ASG
    instances = []
    for instance, settings in props["instance_facts"].items():
        if settings["lifecycle_state"] == "InService" and settings["health_status"] == "Healthy":
            instances.append(dict(Id=instance))
    module.debug(f"ASG considers the following instances InService and Healthy: {instances}")
    module.debug("Target Group instance status:")
    tg_instances = list()
    for tg in as_group.get("TargetGroupARNs"):
        # we catch a race condition that sometimes happens if the instance exists in the ASG
        # but has not yet show up in the ELB
        try:
            tg_instances = describe_target_health(elbv2_connection, tg, instances)
        except is_boto3_error_code("InvalidInstance"):
            return None
        except (
            botocore.exceptions.ClientError,
            botocore.exceptions.BotoCoreError,
        ) as e:  # pylint: disable=duplicate-except
            module.fail_json_aws(e, msg="Failed to get target group.")

        for i in tg_instances.get("TargetHealthDescriptions"):
            if i["TargetHealth"]["State"] == "healthy":
                healthy_instances.add(i["Target"]["Id"])
            module.debug(f"Target Group Health State {i['Target']['Id']}: {i['TargetHealth']['State']}")
    return len(healthy_instances)


def wait_for_elb(asg_connection, group_name):
    wait_timeout = module.params.get("wait_timeout")

    # if the health_check_type is ELB, we want to query the ELBs directly for instance
    # status as to avoid health_check_grace period that is awarded to ASG instances
    as_group = describe_autoscaling_groups(asg_connection, group_name)[0]

    if as_group.get("LoadBalancerNames") and as_group.get("HealthCheckType") == "ELB":
        module.debug("Waiting for ELB to consider instances healthy.")
        elb_connection = module.client("elb")

        wait_timeout = time.time() + wait_timeout
        healthy_instances = elb_healthy(asg_connection, elb_connection, group_name)

        while healthy_instances < as_group.get("MinSize") and wait_timeout > time.time():
            healthy_instances = elb_healthy(asg_connection, elb_connection, group_name)
            module.debug(f"ELB thinks {healthy_instances} instances are healthy.")
            time.sleep(10)
        if wait_timeout <= time.time():
            # waiting took too long
            module.fail_json(msg=f"Waited too long for ELB instances to be healthy. {time.asctime()}")
        module.debug(f"Waiting complete. ELB thinks {healthy_instances} instances are healthy.")


def wait_for_target_group(asg_connection, group_name):
    wait_timeout = module.params.get("wait_timeout")

    # if the health_check_type is ELB, we want to query the ELBs directly for instance
    # status as to avoid health_check_grace period that is awarded to ASG instances
    as_group = describe_autoscaling_groups(asg_connection, group_name)[0]

    if as_group.get("TargetGroupARNs") and as_group.get("HealthCheckType") == "ELB":
        module.debug("Waiting for Target Group to consider instances healthy.")
        elbv2_connection = module.client("elbv2")

        wait_timeout = time.time() + wait_timeout
        healthy_instances = tg_healthy(asg_connection, elbv2_connection, group_name)

        while healthy_instances < as_group.get("MinSize") and wait_timeout > time.time():
            healthy_instances = tg_healthy(asg_connection, elbv2_connection, group_name)
            module.debug(f"Target Group thinks {healthy_instances} instances are healthy.")
            time.sleep(10)
        if wait_timeout <= time.time():
            # waiting took too long
            module.fail_json(msg=f"Waited too long for ELB instances to be healthy. {time.asctime()}")
        module.debug(f"Waiting complete. Target Group thinks {healthy_instances} instances are healthy.")


def suspend_processes(ec2_connection, as_group):
    processes_to_suspend = set(module.params.get("suspend_processes"))

    try:
        suspended_processes = set([p["ProcessName"] for p in as_group["SuspendedProcesses"]])
    except AttributeError:
        # New ASG being created, no suspended_processes defined yet
        suspended_processes = set()

    if processes_to_suspend == suspended_processes:
        return False

    resume_processes = list(suspended_processes - processes_to_suspend)
    if resume_processes:
        resume_asg_processes(ec2_connection, module.params.get("name"), resume_processes)

    if processes_to_suspend:
        suspend_asg_processes(ec2_connection, module.params.get("name"), list(processes_to_suspend))

    return True


def create_autoscaling_group(connection):
    group_name = module.params.get("name")
    load_balancers = module.params["load_balancers"]
    target_group_arns = module.params["target_group_arns"]
    availability_zones = module.params["availability_zones"]
    launch_template = module.params.get("launch_template")
    min_size = module.params["min_size"]
    max_size = module.params["max_size"]
    max_instance_lifetime = module.params.get("max_instance_lifetime")
    placement_group = module.params.get("placement_group")
    desired_capacity = module.params.get("desired_capacity")
    vpc_zone_identifier = module.params.get("vpc_zone_identifier")
    set_tags = module.params.get("tags")
    purge_tags = module.params.get("purge_tags")
    health_check_period = module.params.get("health_check_period")
    health_check_type = module.params.get("health_check_type")
    default_cooldown = module.params.get("default_cooldown")
    wait_for_instances = module.params.get("wait_for_instances")
    wait_timeout = module.params.get("wait_timeout")
    termination_policies = module.params.get("termination_policies")
    notification_topic = module.params.get("notification_topic")
    notification_types = module.params.get("notification_types")
    metrics_collection = module.params.get("metrics_collection")
    metrics_granularity = module.params.get("metrics_granularity")
    metrics_list = module.params.get("metrics_list")

    try:
        as_groups = describe_autoscaling_groups(connection, group_name)
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Failed to describe auto scaling groups.")

    ec2_connection = module.client("ec2")

    if vpc_zone_identifier:
        vpc_zone_identifier = ",".join(vpc_zone_identifier)

    asg_tags = []
    for tag in set_tags:
        for k, v in tag.items():
            if k != "propagate_at_launch":
                asg_tags.append(
                    dict(
                        Key=k,
                        Value=to_native(v),
                        PropagateAtLaunch=bool(tag.get("propagate_at_launch", True)),
                        ResourceType="auto-scaling-group",
                        ResourceId=group_name,
                    )
                )
    if not as_groups:
        if module.check_mode:
            module.exit_json(changed=True, msg="Would have created AutoScalingGroup if not in check_mode.")

        if not vpc_zone_identifier and not availability_zones:
            availability_zones = module.params["availability_zones"] = [
                zone["ZoneName"] for zone in ec2_connection.describe_availability_zones()["AvailabilityZones"]
            ]

        enforce_required_arguments_for_create()

        if desired_capacity is None:
            desired_capacity = min_size
        ag = dict(
            AutoScalingGroupName=group_name,
            MinSize=min_size,
            MaxSize=max_size,
            DesiredCapacity=desired_capacity,
            Tags=asg_tags,
            HealthCheckGracePeriod=health_check_period,
            HealthCheckType=health_check_type,
            DefaultCooldown=default_cooldown,
            TerminationPolicies=termination_policies,
        )
        if vpc_zone_identifier:
            ag["VPCZoneIdentifier"] = vpc_zone_identifier
        if availability_zones:
            ag["AvailabilityZones"] = availability_zones
        if placement_group:
            ag["PlacementGroup"] = placement_group
        if load_balancers:
            ag["LoadBalancerNames"] = load_balancers
        if target_group_arns:
            ag["TargetGroupARNs"] = target_group_arns
        if max_instance_lifetime:
            ag["MaxInstanceLifetime"] = max_instance_lifetime

        launch_object = get_launch_object(connection, ec2_connection)
        if "LaunchConfigurationName" in launch_object:
            ag["LaunchConfigurationName"] = launch_object["LaunchConfigurationName"]
        elif "LaunchTemplate" in launch_object:
            if "MixedInstancesPolicy" in launch_object:
                ag["MixedInstancesPolicy"] = launch_object["MixedInstancesPolicy"]
            else:
                ag["LaunchTemplate"] = launch_object["LaunchTemplate"]
        else:
            module.fail_json(msg="Missing LaunchConfigurationName or LaunchTemplate")

        try:
            create_asg(connection, **ag)
            if metrics_collection:
                connection.enable_metrics_collection(
                    AutoScalingGroupName=group_name, Granularity=metrics_granularity, Metrics=metrics_list
                )

            all_ag = describe_autoscaling_groups(connection, group_name)
            if len(all_ag) == 0:
                module.fail_json(msg=f"No auto scaling group found with the name {group_name}")
            as_group = all_ag[0]
            suspend_processes(connection, as_group)
            if wait_for_instances:
                wait_for_new_inst(connection, group_name, wait_timeout, desired_capacity, "viable_instances")
                if load_balancers:
                    wait_for_elb(connection, group_name)
                # Wait for target group health if target group(s)defined
                if target_group_arns:
                    wait_for_target_group(connection, group_name)
            if notification_topic:
                put_notification_config(connection, group_name, notification_topic, notification_types)
            as_group = describe_autoscaling_groups(connection, group_name)[0]
            asg_properties = get_properties(as_group)
            changed = True
            return changed, asg_properties
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            module.fail_json_aws(e, msg="Failed to create Autoscaling Group.")
    else:
        if module.check_mode:
            module.exit_json(changed=True, msg="Would have modified AutoScalingGroup if required if not in check_mode.")

        as_group = as_groups[0]
        initial_asg_properties = get_properties(as_group)
        changed = False

        if suspend_processes(connection, as_group):
            changed = True

        # process tag changes
        have_tags = as_group.get("Tags")
        want_tags = asg_tags
        if purge_tags and not want_tags and have_tags:
            connection.delete_tags(Tags=list(have_tags))

        if len(set_tags) > 0:
            if have_tags:
                have_tags.sort(key=lambda x: x["Key"])
            if want_tags:
                want_tags.sort(key=lambda x: x["Key"])
            dead_tags = []
            have_tag_keyvals = [x["Key"] for x in have_tags]
            want_tag_keyvals = [x["Key"] for x in want_tags]

            for dead_tag in set(have_tag_keyvals).difference(want_tag_keyvals):
                changed = True
                if purge_tags:
                    dead_tags.append(
                        dict(
                            ResourceId=as_group["AutoScalingGroupName"], ResourceType="auto-scaling-group", Key=dead_tag
                        )
                    )
                have_tags = [have_tag for have_tag in have_tags if have_tag["Key"] != dead_tag]

            if dead_tags:
                connection.delete_tags(Tags=dead_tags)

            zipped = zip(have_tags, want_tags)
            if len(have_tags) != len(want_tags) or not all(x == y for x, y in zipped):
                changed = True
                connection.create_or_update_tags(Tags=asg_tags)

        # Handle load balancer attachments/detachments
        # Attach load balancers if they are specified but none currently exist
        if load_balancers and not as_group["LoadBalancerNames"]:
            changed = True
            try:
                attach_load_balancers(connection, group_name, load_balancers)
            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                module.fail_json_aws(e, msg="Failed to update Autoscaling Group.")

        # Update load balancers if they are specified and one or more already exists
        elif as_group["LoadBalancerNames"]:
            change_load_balancers = load_balancers is not None
            # Get differences
            if not load_balancers:
                load_balancers = list()
            wanted_elbs = set(load_balancers)

            has_elbs = set(as_group["LoadBalancerNames"])
            # check if all requested are already existing
            if has_elbs - wanted_elbs and change_load_balancers:
                # if wanted contains less than existing, then we need to delete some
                elbs_to_detach = has_elbs.difference(wanted_elbs)
                if elbs_to_detach:
                    changed = True
                    try:
                        detach_load_balancers(connection, group_name, list(elbs_to_detach))
                    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                        module.fail_json_aws(e, msg=f"Failed to detach load balancers {elbs_to_detach}")
            if wanted_elbs - has_elbs:
                # if has contains less than wanted, then we need to add some
                elbs_to_attach = wanted_elbs.difference(has_elbs)
                if elbs_to_attach:
                    changed = True
                    try:
                        attach_load_balancers(connection, group_name, list(elbs_to_attach))
                    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                        module.fail_json_aws(e, msg=f"Failed to attach load balancers {elbs_to_attach}")

        # Handle target group attachments/detachments
        # Attach target groups if they are specified but none currently exist
        if target_group_arns and not as_group["TargetGroupARNs"]:
            changed = True
            try:
                attach_lb_target_groups(connection, group_name, target_group_arns)
            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                module.fail_json_aws(e, msg="Failed to update Autoscaling Group.")
        # Update target groups if they are specified and one or more already exists
        elif target_group_arns is not None and as_group["TargetGroupARNs"]:
            # Get differences
            wanted_tgs = set(target_group_arns)
            has_tgs = set(as_group["TargetGroupARNs"])

            tgs_to_detach = has_tgs.difference(wanted_tgs)
            if tgs_to_detach:
                changed = True
                try:
                    detach_lb_target_groups(connection, group_name, list(tgs_to_detach))
                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                    module.fail_json_aws(e, msg=f"Failed to detach load balancer target groups {tgs_to_detach}")

            tgs_to_attach = wanted_tgs.difference(has_tgs)
            if tgs_to_attach:
                changed = True
                try:
                    attach_lb_target_groups(connection, group_name, list(tgs_to_attach))
                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                    module.fail_json(msg=f"Failed to attach load balancer target groups {tgs_to_attach}")

        # check for attributes that aren't required for updating an existing ASG
        # check if min_size/max_size/desired capacity have been specified and if not use ASG values
        if min_size is None:
            min_size = as_group["MinSize"]
        if max_size is None:
            max_size = as_group["MaxSize"]
        if desired_capacity is None:
            desired_capacity = as_group["DesiredCapacity"]
        ag = dict(
            AutoScalingGroupName=group_name,
            MinSize=min_size,
            MaxSize=max_size,
            DesiredCapacity=desired_capacity,
            HealthCheckGracePeriod=health_check_period,
            HealthCheckType=health_check_type,
            DefaultCooldown=default_cooldown,
            TerminationPolicies=termination_policies,
        )

        # Get the launch object (config or template) if one is provided in args or use the existing one attached to ASG if not.
        launch_object = get_launch_object(connection, ec2_connection)
        if "LaunchConfigurationName" in launch_object:
            ag["LaunchConfigurationName"] = launch_object["LaunchConfigurationName"]
        elif "LaunchTemplate" in launch_object:
            if "MixedInstancesPolicy" in launch_object:
                ag["MixedInstancesPolicy"] = launch_object["MixedInstancesPolicy"]
            else:
                ag["LaunchTemplate"] = launch_object["LaunchTemplate"]
        else:
            try:
                ag["LaunchConfigurationName"] = as_group["LaunchConfigurationName"]
            except KeyError:
                launch_template = as_group["LaunchTemplate"]
                # Prefer LaunchTemplateId over Name as it's more specific.  Only one can be used for update_asg.
                ag["LaunchTemplate"] = {
                    "LaunchTemplateId": launch_template["LaunchTemplateId"],
                    "Version": launch_template["Version"],
                }

        if availability_zones:
            ag["AvailabilityZones"] = availability_zones
        if vpc_zone_identifier:
            ag["VPCZoneIdentifier"] = vpc_zone_identifier
        if max_instance_lifetime is not None:
            ag["MaxInstanceLifetime"] = max_instance_lifetime

        try:
            update_asg(connection, **ag)

            if metrics_collection:
                connection.enable_metrics_collection(
                    AutoScalingGroupName=group_name, Granularity=metrics_granularity, Metrics=metrics_list
                )
            else:
                connection.disable_metrics_collection(AutoScalingGroupName=group_name, Metrics=metrics_list)

        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            module.fail_json_aws(e, msg="Failed to update autoscaling group")

        if notification_topic:
            try:
                put_notification_config(connection, group_name, notification_topic, notification_types)
            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                module.fail_json_aws(e, msg="Failed to update Autoscaling Group notifications.")
        if wait_for_instances:
            wait_for_new_inst(connection, group_name, wait_timeout, desired_capacity, "viable_instances")
            # Wait for ELB health if ELB(s)defined
            if load_balancers:
                module.debug("\tWAITING FOR ELB HEALTH")
                wait_for_elb(connection, group_name)
            # Wait for target group health if target group(s)defined

            if target_group_arns:
                module.debug("\tWAITING FOR TG HEALTH")
                wait_for_target_group(connection, group_name)

        try:
            as_group = describe_autoscaling_groups(connection, group_name)[0]
            asg_properties = get_properties(as_group)
            if asg_properties != initial_asg_properties:
                changed = True
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            module.fail_json_aws(e, msg="Failed to read existing Autoscaling Groups.")
        return changed, asg_properties


def delete_autoscaling_group(connection):
    group_name = module.params.get("name")
    notification_topic = module.params.get("notification_topic")
    wait_for_instances = module.params.get("wait_for_instances")
    wait_timeout = module.params.get("wait_timeout")

    if notification_topic:
        del_notification_config(connection, group_name, notification_topic)
    groups = describe_autoscaling_groups(connection, group_name)
    if groups:
        if module.check_mode:
            module.exit_json(changed=True, msg="Would have deleted AutoScalingGroup if not in check_mode.")
        wait_timeout = time.time() + wait_timeout
        if not wait_for_instances:
            delete_asg(connection, group_name, force_delete=True)
        else:
            updated_params = dict(AutoScalingGroupName=group_name, MinSize=0, MaxSize=0, DesiredCapacity=0)
            update_asg(connection, **updated_params)
            instances = True
            while instances and wait_for_instances and wait_timeout >= time.time():
                tmp_groups = describe_autoscaling_groups(connection, group_name)
                if tmp_groups:
                    tmp_group = tmp_groups[0]
                    if not tmp_group.get("Instances"):
                        instances = False
                time.sleep(10)

            if wait_timeout <= time.time():
                # waiting took too long
                module.fail_json(msg=f"Waited too long for old instances to terminate. {time.asctime()}")

            delete_asg(connection, group_name, force_delete=False)
        while describe_autoscaling_groups(connection, group_name) and wait_timeout >= time.time():
            time.sleep(5)
        if wait_timeout <= time.time():
            # waiting took too long
            module.fail_json(msg=f"Waited too long for ASG to delete. {time.asctime()}")
        return True

    return False


def get_chunks(l, n):
    for i in range(0, len(l), n):
        yield l[i:i + n]  # fmt: skip


def update_size(connection, group, max_size, min_size, dc):
    module.debug("setting ASG sizes")
    module.debug(f"minimum size: {min_size}, desired_capacity: {dc}, max size: {max_size}")
    updated_group = dict()
    updated_group["AutoScalingGroupName"] = group["AutoScalingGroupName"]
    updated_group["MinSize"] = min_size
    updated_group["MaxSize"] = max_size
    updated_group["DesiredCapacity"] = dc
    update_asg(connection, **updated_group)


def replace(connection):
    batch_size = module.params.get("replace_batch_size")
    wait_timeout = module.params.get("wait_timeout")
    wait_for_instances = module.params.get("wait_for_instances")
    group_name = module.params.get("name")
    max_size = module.params.get("max_size")
    min_size = module.params.get("min_size")
    desired_capacity = module.params.get("desired_capacity")
    launch_config_name = module.params.get("launch_config_name")

    # Required to maintain the default value being set to 'true'
    if launch_config_name:
        lc_check = module.params.get("lc_check")
    else:
        lc_check = False
    # Mirror above behavior for Launch Templates
    launch_template = module.params.get("launch_template")
    if launch_template:
        lt_check = module.params.get("lt_check")
    else:
        lt_check = False
    replace_instances = module.params.get("replace_instances")
    replace_all_instances = module.params.get("replace_all_instances")

    as_group = describe_autoscaling_groups(connection, group_name)[0]
    if desired_capacity is None:
        desired_capacity = as_group["DesiredCapacity"]

    if wait_for_instances:
        wait_for_new_inst(connection, group_name, wait_timeout, as_group["MinSize"], "viable_instances")

    props = get_properties(as_group)
    instances = props["instances"]
    if replace_all_instances:
        # If replacing all instances, then set replace_instances to current set
        # This allows replace_instances and replace_all_instances to behave same
        replace_instances = instances
    if replace_instances:
        instances = replace_instances

    # check to see if instances are replaceable if checking launch configs
    if launch_config_name:
        new_instances, old_instances = get_instances_by_launch_config(props, lc_check, instances)
    elif launch_template:
        new_instances, old_instances = get_instances_by_launch_template(props, lt_check, instances)

    num_new_inst_needed = desired_capacity - len(new_instances)

    if lc_check or lt_check:
        if num_new_inst_needed == 0 and old_instances:
            module.debug("No new instances needed, but old instances are present. Removing old instances")
            terminate_batch(connection, old_instances, instances, True)
            as_group = describe_autoscaling_groups(connection, group_name)[0]
            props = get_properties(as_group)
            changed = True
            return changed, props

        #  we don't want to spin up extra instances if not necessary
        if num_new_inst_needed < batch_size:
            module.debug(f"Overriding batch size to {num_new_inst_needed}")
            batch_size = num_new_inst_needed

    if not old_instances:
        changed = False
        return changed, props

    # check if min_size/max_size/desired capacity have been specified and if not use ASG values
    if min_size is None:
        min_size = as_group["MinSize"]
    if max_size is None:
        max_size = as_group["MaxSize"]

    # set temporary settings and wait for them to be reached
    # This should get overwritten if the number of instances left is less than the batch size.

    as_group = describe_autoscaling_groups(connection, group_name)[0]
    update_size(connection, as_group, max_size + batch_size, min_size + batch_size, desired_capacity + batch_size)

    if wait_for_instances:
        wait_for_new_inst(connection, group_name, wait_timeout, as_group["MinSize"] + batch_size, "viable_instances")
        wait_for_elb(connection, group_name)
        wait_for_target_group(connection, group_name)

    as_group = describe_autoscaling_groups(connection, group_name)[0]
    props = get_properties(as_group)
    instances = props["instances"]
    if replace_instances:
        instances = replace_instances

    module.debug("beginning main loop")
    for i in get_chunks(instances, batch_size):
        # break out of this loop if we have enough new instances
        break_early, desired_size, term_instances = terminate_batch(connection, i, instances, False)

        if wait_for_instances:
            wait_for_term_inst(connection, term_instances)
            wait_for_new_inst(connection, group_name, wait_timeout, desired_size, "viable_instances")
            wait_for_elb(connection, group_name)
            wait_for_target_group(connection, group_name)

        if break_early:
            module.debug("breaking loop")
            break

    update_size(connection, as_group, max_size, min_size, desired_capacity)
    as_group = describe_autoscaling_groups(connection, group_name)[0]
    asg_properties = get_properties(as_group)
    module.debug("Rolling update complete.")
    changed = True
    return changed, asg_properties


def detach(connection):
    group_name = module.params.get("name")
    detach_instances = module.params.get("detach_instances")
    as_group = describe_autoscaling_groups(connection, group_name)[0]
    decrement_desired_capacity = module.params.get("decrement_desired_capacity")
    min_size = module.params.get("min_size")
    props = get_properties(as_group)
    instances = props["instances"]

    # check if provided instance exists in asg, create list of instances to detach which exist in asg
    instances_to_detach = []
    for instance_id in detach_instances:
        if instance_id in instances:
            instances_to_detach.append(instance_id)

    # check if setting decrement_desired_capacity will make desired_capacity smaller
    # than the currently set minimum size in ASG configuration
    if decrement_desired_capacity:
        decremented_desired_capacity = len(instances) - len(instances_to_detach)
        if min_size and min_size > decremented_desired_capacity:
            module.fail_json(
                msg=(
                    "Detaching instance(s) with 'decrement_desired_capacity' flag set reduces number of instances to"
                    f" {decremented_desired_capacity} which is below current min_size {min_size}, please update"
                    " AutoScalingGroup Sizes properly."
                )
            )

    if instances_to_detach:
        try:
            detach_asg_instances(connection, instances_to_detach, group_name, decrement_desired_capacity)
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            module.fail_json_aws(e, msg="Failed to detach instances from AutoScaling Group")

    asg_properties = get_properties(as_group)
    return True, asg_properties


def get_instances_by_launch_config(props, lc_check, initial_instances):
    new_instances = []
    old_instances = []
    # old instances are those that have the old launch config
    if lc_check:
        for i in props["instances"]:
            # Check if migrating from launch_template to launch_config first
            if "launch_template" in props["instance_facts"][i]:
                old_instances.append(i)
            elif props["instance_facts"][i].get("launch_config_name") == props["launch_config_name"]:
                new_instances.append(i)
            else:
                old_instances.append(i)

    else:
        module.debug(f"Comparing initial instances with current: {*initial_instances, }")
        for i in props["instances"]:
            if i not in initial_instances:
                new_instances.append(i)
            else:
                old_instances.append(i)

    module.debug(f"New instances: {len(new_instances)}, {*new_instances, }")
    module.debug(f"Old instances: {len(old_instances)}, {*old_instances, }")

    return new_instances, old_instances


def get_instances_by_launch_template(props, lt_check, initial_instances):
    new_instances = []
    old_instances = []
    # old instances are those that have the old launch template or version of the same launch template
    if lt_check:
        for i in props["instances"]:
            # Check if migrating from launch_config_name to launch_template_name first
            if "launch_config_name" in props["instance_facts"][i]:
                old_instances.append(i)
            elif props["instance_facts"][i].get("launch_template") == props["launch_template"]:
                new_instances.append(i)
            else:
                old_instances.append(i)
    else:
        module.debug(f"Comparing initial instances with current: {*initial_instances, }")
        for i in props["instances"]:
            if i not in initial_instances:
                new_instances.append(i)
            else:
                old_instances.append(i)

    module.debug(f"New instances: {len(new_instances)}, {*new_instances, }")
    module.debug(f"Old instances: {len(old_instances)}, {*old_instances, }")

    return new_instances, old_instances


def list_purgeable_instances(props, lc_check, lt_check, replace_instances, initial_instances):
    instances_to_terminate = []
    instances = (inst_id for inst_id in replace_instances if inst_id in props["instances"])
    # check to make sure instances given are actually in the given ASG
    # and they have a non-current launch config
    if "launch_config_name" in module.params:
        if lc_check:
            for i in instances:
                if (
                    "launch_template" in props["instance_facts"][i]
                    or props["instance_facts"][i]["launch_config_name"] != props["launch_config_name"]
                ):
                    instances_to_terminate.append(i)
        else:
            for i in instances:
                if i in initial_instances:
                    instances_to_terminate.append(i)
    elif "launch_template" in module.params:
        if lt_check:
            for i in instances:
                if (
                    "launch_config_name" in props["instance_facts"][i]
                    or props["instance_facts"][i]["launch_template"] != props["launch_template"]
                ):
                    instances_to_terminate.append(i)
        else:
            for i in instances:
                if i in initial_instances:
                    instances_to_terminate.append(i)

    return instances_to_terminate


def terminate_batch(connection, replace_instances, initial_instances, leftovers=False):
    batch_size = module.params.get("replace_batch_size")
    min_size = module.params.get("min_size")
    desired_capacity = module.params.get("desired_capacity")
    group_name = module.params.get("name")
    lc_check = module.params.get("lc_check")
    lt_check = module.params.get("lt_check")
    decrement_capacity = False
    break_loop = False

    as_group = describe_autoscaling_groups(connection, group_name)[0]
    if desired_capacity is None:
        desired_capacity = as_group["DesiredCapacity"]

    props = get_properties(as_group)
    desired_size = as_group["MinSize"]
    if module.params.get("launch_config_name"):
        new_instances, old_instances = get_instances_by_launch_config(props, lc_check, initial_instances)
    else:
        new_instances, old_instances = get_instances_by_launch_template(props, lt_check, initial_instances)
    num_new_inst_needed = desired_capacity - len(new_instances)

    # check to make sure instances given are actually in the given ASG
    # and they have a non-current launch config
    instances_to_terminate = list_purgeable_instances(props, lc_check, lt_check, replace_instances, initial_instances)

    module.debug(f"new instances needed: {num_new_inst_needed}")
    module.debug(f"new instances: {*new_instances, }")
    module.debug(f"old instances: {*old_instances, }")
    module.debug(f"batch instances: {*instances_to_terminate, }")

    if num_new_inst_needed == 0:
        decrement_capacity = True
        if as_group["MinSize"] != min_size:
            if min_size is None:
                min_size = as_group["MinSize"]
            updated_params = dict(AutoScalingGroupName=as_group["AutoScalingGroupName"], MinSize=min_size)
            update_asg(connection, **updated_params)
            module.debug(f"Updating minimum size back to original of {min_size}")
        # if are some leftover old instances, but we are already at capacity with new ones
        # we don't want to decrement capacity
        if leftovers:
            decrement_capacity = False
        break_loop = True
        instances_to_terminate = old_instances
        desired_size = min_size
        module.debug("No new instances needed")

    if num_new_inst_needed < batch_size and num_new_inst_needed != 0:
        instances_to_terminate = instances_to_terminate[:num_new_inst_needed]
        decrement_capacity = False
        break_loop = False
        module.debug(f"{num_new_inst_needed} new instances needed")

    module.debug(f"decrementing capacity: {decrement_capacity}")

    for instance_id in instances_to_terminate:
        elb_dreg(connection, group_name, instance_id)
        module.debug(f"terminating instance: {instance_id}")
        terminate_asg_instance(connection, instance_id, decrement_capacity)

    # we wait to make sure the machines we marked as Unhealthy are
    # no longer in the list

    return break_loop, desired_size, instances_to_terminate


def wait_for_term_inst(connection, term_instances):
    wait_timeout = module.params.get("wait_timeout")
    group_name = module.params.get("name")
    as_group = describe_autoscaling_groups(connection, group_name)[0]
    count = 1
    wait_timeout = time.time() + wait_timeout
    while wait_timeout > time.time() and count > 0:
        module.debug("waiting for instances to terminate")
        count = 0
        as_group = describe_autoscaling_groups(connection, group_name)[0]
        props = get_properties(as_group)
        instance_facts = props["instance_facts"]
        instances = (i for i in instance_facts if i in term_instances)
        for i in instances:
            lifecycle = instance_facts[i]["lifecycle_state"]
            health = instance_facts[i]["health_status"]
            module.debug(f"Instance {i} has state of {lifecycle},{health}")
            if lifecycle.startswith("Terminating") or health == "Unhealthy":
                count += 1
        time.sleep(10)

    if wait_timeout <= time.time():
        # waiting took too long
        module.fail_json(msg=f"Waited too long for old instances to terminate. {time.asctime()}")


def wait_for_new_inst(connection, group_name, wait_timeout, desired_size, prop):
    # make sure we have the latest stats after that last loop.
    as_group = describe_autoscaling_groups(connection, group_name)[0]
    props = get_properties(as_group)
    module.debug(f"Waiting for {prop} = {desired_size}, currently {props[prop]}")
    # now we make sure that we have enough instances in a viable state
    wait_timeout = time.time() + wait_timeout
    while wait_timeout > time.time() and desired_size > props[prop]:
        module.debug(f"Waiting for {prop} = {desired_size}, currently {props[prop]}")
        time.sleep(10)
        as_group = describe_autoscaling_groups(connection, group_name)[0]
        props = get_properties(as_group)
    if wait_timeout <= time.time():
        # waiting took too long
        module.fail_json(msg=f"Waited too long for new instances to become viable. {time.asctime()}")
    module.debug(f"Reached {prop}: {desired_size}")
    return props


def asg_exists(connection):
    group_name = module.params.get("name")
    as_group = describe_autoscaling_groups(connection, group_name)
    return bool(len(as_group))


def main():
    argument_spec = dict(
        name=dict(required=True, type="str"),
        load_balancers=dict(type="list", elements="str"),
        target_group_arns=dict(type="list", elements="str"),
        availability_zones=dict(type="list", elements="str"),
        launch_config_name=dict(type="str"),
        launch_template=dict(
            type="dict",
            default=None,
            options=dict(
                version=dict(type="str"),
                launch_template_name=dict(type="str"),
                launch_template_id=dict(type="str"),
            ),
        ),
        min_size=dict(type="int"),
        max_size=dict(type="int"),
        max_instance_lifetime=dict(type="int"),
        mixed_instances_policy=dict(
            type="dict",
            default=None,
            options=dict(
                instance_types=dict(type="list", elements="str"),
                instances_distribution=dict(
                    type="dict",
                    default=None,
                    options=dict(
                        on_demand_allocation_strategy=dict(type="str"),
                        on_demand_base_capacity=dict(type="int"),
                        on_demand_percentage_above_base_capacity=dict(type="int"),
                        spot_allocation_strategy=dict(type="str"),
                        spot_instance_pools=dict(type="int"),
                        spot_max_price=dict(type="str"),
                    ),
                ),
            ),
        ),
        placement_group=dict(type="str"),
        desired_capacity=dict(type="int"),
        vpc_zone_identifier=dict(type="list", elements="str"),
        replace_batch_size=dict(type="int", default=1),
        replace_all_instances=dict(type="bool", default=False),
        replace_instances=dict(type="list", default=[], elements="str"),
        detach_instances=dict(type="list", default=[], elements="str"),
        decrement_desired_capacity=dict(type="bool", default=False),
        lc_check=dict(type="bool", default=True),
        lt_check=dict(type="bool", default=True),
        wait_timeout=dict(type="int", default=300),
        state=dict(default="present", choices=["present", "absent"]),
        tags=dict(type="list", default=[], elements="dict"),
        purge_tags=dict(type="bool", default=False),
        health_check_period=dict(type="int", default=300),
        health_check_type=dict(default="EC2", choices=["EC2", "ELB"]),
        default_cooldown=dict(type="int", default=300),
        wait_for_instances=dict(type="bool", default=True),
        termination_policies=dict(type="list", default="Default", elements="str"),
        notification_topic=dict(type="str", default=None),
        notification_types=dict(
            type="list",
            default=[
                "autoscaling:EC2_INSTANCE_LAUNCH",
                "autoscaling:EC2_INSTANCE_LAUNCH_ERROR",
                "autoscaling:EC2_INSTANCE_TERMINATE",
                "autoscaling:EC2_INSTANCE_TERMINATE_ERROR",
            ],
            elements="str",
        ),
        suspend_processes=dict(type="list", default=[], elements="str"),
        metrics_collection=dict(type="bool", default=False),
        metrics_granularity=dict(type="str", default="1Minute"),
        metrics_list=dict(
            type="list",
            default=[
                "GroupMinSize",
                "GroupMaxSize",
                "GroupDesiredCapacity",
                "GroupInServiceInstances",
                "GroupPendingInstances",
                "GroupStandbyInstances",
                "GroupTerminatingInstances",
                "GroupTotalInstances",
            ],
            elements="str",
        ),
    )

    global module
    module = AnsibleAWSModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
        mutually_exclusive=[
            ["replace_all_instances", "replace_instances"],
            ["replace_all_instances", "detach_instances"],
            ["launch_config_name", "launch_template"],
        ],
    )

    state = module.params.get("state")
    replace_instances = module.params.get("replace_instances")
    replace_all_instances = module.params.get("replace_all_instances")
    detach_instances = module.params.get("detach_instances")

    connection = module.client("autoscaling")
    changed = create_changed = replace_changed = detach_changed = False
    exists = asg_exists(connection)

    if state == "present":
        create_changed, asg_properties = create_autoscaling_group(connection)
    elif state == "absent":
        changed = delete_autoscaling_group(connection)
        module.exit_json(changed=changed)

    # Only replace instances if asg existed at start of call
    if (
        exists
        and (replace_all_instances or replace_instances)
        and (module.params.get("launch_config_name") or module.params.get("launch_template"))
    ):
        replace_changed, asg_properties = replace(connection)

    # Only detach instances if asg existed at start of call
    if (
        exists
        and (detach_instances)
        and (module.params.get("launch_config_name") or module.params.get("launch_template"))
    ):
        detach_changed, asg_properties = detach(connection)

    if create_changed or replace_changed or detach_changed:
        changed = True

    module.exit_json(changed=changed, **asg_properties)


if __name__ == "__main__":
    main()
