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

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

DOCUMENTATION = r"""
---
module: elb_target_group
version_added: 1.0.0
short_description: Manage a target group for an Application or Network load balancer
description:
  - Manage an AWS Elastic Load Balancer target group. See
    U(https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html) or
    U(https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html) for details.
author:
  - "Rob White (@wimnat)"
options:
  deregistration_delay_timeout:
    description:
      - The amount time for Elastic Load Balancing to wait before changing the state of a deregistering target from draining to unused.
        The range is 0-3600 seconds.
    type: int
  deregistration_connection_termination:
    description:
      - Indicates whether the load balancer terminates connections at the end of the deregistration timeout.
      - Using this option is only supported when attaching to a Network Load Balancer (NLB).
    type: bool
    default: false
    required: false
    version_added: 3.1.0
  health_check_protocol:
    description:
      - The protocol the load balancer uses when performing health checks on targets.
    required: false
    choices: [ 'http', 'https', 'tcp', 'tls', 'udp', 'tcp_udp', 'HTTP', 'HTTPS', 'TCP', 'TLS', 'UDP', 'TCP_UDP']
    type: str
  health_check_port:
    description:
      - The port the load balancer uses when performing health checks on targets.
        Can be set to 'traffic-port' to match target port.
      - When not defined will default to the port on which each target receives traffic from the load balancer.
    required: false
    type: str
  health_check_path:
    description:
      - The ping path that is the destination on the targets for health checks. The path must be defined in order to set a health check.
      - Requires the I(health_check_protocol) parameter to be set.
    required: false
    type: str
  health_check_interval:
    description:
      - The approximate amount of time, in seconds, between health checks of an individual target.
    required: false
    type: int
  health_check_timeout:
    description:
      - The amount of time, in seconds, during which no response from a target means a failed health check.
    required: false
    type: int
  healthy_threshold_count:
    description:
      - The number of consecutive health checks successes required before considering an unhealthy target healthy.
    required: false
    type: int
  modify_targets:
    description:
      - Whether or not to alter existing targets in the group to match what is passed with the module
    required: false
    default: true
    type: bool
  name:
    description:
      - The name of the target group.
    required: true
    type: str
  port:
    description:
      - The port on which the targets receive traffic. This port is used unless you specify a port override when registering the target.
      - Required when I(state) is C(present) and I(target_type) is C(instance), C(ip), or C(alb).
    required: false
    type: int
  protocol:
    description:
      - The protocol to use for routing traffic to the targets.
      - Required when I(state) is C(present) and I(target_type) is C(instance), C(ip), or C(alb).
    required: false
    choices: [ 'http', 'https', 'tcp', 'tls', 'udp', 'tcp_udp', 'HTTP', 'HTTPS', 'TCP', 'TLS', 'UDP', 'TCP_UDP']
    type: str
  protocol_version:
    description:
      - Specifies protocol version.
      - The protocol_version parameter is immutable and cannot be changed when updating an elb_target_group.
    required: false
    choices: ['GRPC', 'HTTP1', 'HTTP2']
    type: str
    version_added: 5.1.0
  state:
    description:
      - Create or destroy the target group.
    required: true
    choices: [ 'present', 'absent' ]
    type: str
  stickiness_enabled:
    description:
      - Indicates whether sticky sessions are enabled.
    type: bool
  stickiness_lb_cookie_duration:
    description:
      - The time period, in seconds, during which requests from a client should be routed to the same target. After this time period expires, the load
        balancer-generated cookie is considered stale. The range is 1 second to 1 week (604800 seconds).
    type: int
  stickiness_app_cookie_duration:
    description:
      - The time period, in seconds, during which requests from a client
        should be routed to the same target. After this time period expires,
        the application-generated cookie is considered stale. The range is 1 second to 1 week (604800 seconds).
    type: int
    version_added: 1.5.0
  stickiness_app_cookie_name:
    description:
      - The name of the application cookie. Required if I(stickiness_type=app_cookie).
    type: str
    version_added: 1.5.0
  stickiness_type:
    description:
      - The type of sticky sessions.
      - Valid values are C(lb_cookie), C(app_cookie) or C(source_ip).
      - If not set AWS will default to C(lb_cookie) for Application Load Balancers or C(source_ip) for Network Load Balancers.
    type: str
  load_balancing_algorithm_type:
    description:
      - The type of load balancing algorithm to use.
      - Changing the load balancing algorithm is only supported when used with Application Load Balancers (ALB).
      - If not set AWS will default to C(round_robin).
    choices: ['round_robin', 'least_outstanding_requests']
    type: str
    version_added: 3.2.0
  successful_response_codes:
    description:
      - The HTTP codes to use when checking for a successful response from a target.
      - Accepts multiple values (for example, "200,202") or a range of values (for example, "200-299").
      - Requires the I(health_check_protocol) parameter to be set.
    required: false
    type: str
  target_type:
    description:
      - The type of target that you must specify when registering targets with this target group. The possible values are
        C(instance) (targets are specified by instance ID), C(ip) (targets are specified by IP address), C(lambda) (target is specified by ARN),
        or C(alb) (target is specified by ARN).
        Note that you can't specify targets for a target group using more than one type. Target types lambda and alb only accept one target. When more than
        one target is specified, only the first one is used. All additional targets are ignored.
        If the target type is ip, specify IP addresses from the subnets of the virtual private cloud (VPC) for the target
        group, the RFC 1918 range (10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16), and the RFC 6598 range (100.64.0.0/10).
        You can't specify publicly routable IP addresses.
      - The default behavior is C(instance).
    required: false
    choices: ['instance', 'ip', 'lambda', 'alb']
    type: str
  targets:
    description:
      - A list of targets to assign to the target group. This parameter defaults to an empty list. Unless you set the 'modify_targets' parameter then
        all existing targets will be removed from the group. The list should be an Id and a Port parameter. See the Examples for detail.
    required: false
    type: list
    elements: dict
  unhealthy_threshold_count:
    description:
      - The number of consecutive health check failures required before considering a target unhealthy.
    required: false
    type: int
  vpc_id:
    description:
      - The identifier of the virtual private cloud (VPC).
      - Required when I(state) is C(present) and I(target_type) is C(instance), C(ip), or C(alb).
    required: false
    type: str
  preserve_client_ip_enabled:
    description:
      - Indicates whether client IP preservation is enabled.
      - The default is disabled if the target group type is C(ip) address and the target group protocol is C(tcp) or C(tls).
        Otherwise, the default is enabled. Client IP preservation cannot be disabled for C(udp) and C(tcp_udp) target groups.
      - I(preserve_client_ip_enabled) is supported only by Network Load Balancers.
    type: bool
    required: false
    version_added: 2.1.0
  proxy_protocol_v2_enabled:
    description:
      - Indicates whether Proxy Protocol version 2 is enabled.
      - The value is C(true) or C(false).
      - I(proxy_protocol_v2_enabled) is supported only by Network Load Balancers.
    type: bool
    required: false
    version_added: 2.1.0
  wait:
    description:
      - Whether or not to wait for the target group.
    type: bool
    default: false
  wait_timeout:
    description:
      - The time to wait for the target group.
    default: 200
    type: int

notes:
  - Once a target group has been created, only its health check can then be modified using subsequent calls

extends_documentation_fragment:
  - amazon.aws.common.modules
  - amazon.aws.region.modules
  - amazon.aws.tags
  - amazon.aws.boto3
"""

EXAMPLES = r"""
# Note: These examples do not set authentication details, see the AWS Guide for details.

- name: Create a target group with a default health check
  community.aws.elb_target_group:
    name: mytargetgroup
    protocol: http
    port: 80
    vpc_id: vpc-01234567
    state: present

- name: Create a target group with protocol_version 'GRPC'
  community.aws.elb_target_group:
    name: mytargetgroup
    protocol: http
    port: 80
    vpc_id: vpc-01234567
    protocol_version: GRPC
    state: present

- name: Modify the target group with a custom health check
  community.aws.elb_target_group:
    name: mytargetgroup
    protocol: http
    port: 80
    vpc_id: vpc-01234567
    health_check_protocol: http
    health_check_path: /health_check
    health_check_port: 80
    successful_response_codes: 200
    health_check_interval: 15
    health_check_timeout: 3
    healthy_threshold_count: 4
    unhealthy_threshold_count: 3
    state: present

- name: Delete a target group
  community.aws.elb_target_group:
    name: mytargetgroup
    state: absent

- name: Create a target group with instance targets
  community.aws.elb_target_group:
    name: mytargetgroup
    protocol: http
    port: 81
    vpc_id: vpc-01234567
    health_check_protocol: http
    health_check_path: /
    successful_response_codes: "200,250-260"
    targets:
      - Id: i-01234567
        Port: 80
      - Id: i-98765432
        Port: 80
    state: present
    wait_timeout: 200
    wait: true

- name: Create a target group with IP address targets
  community.aws.elb_target_group:
    name: mytargetgroup
    protocol: http
    port: 81
    vpc_id: vpc-01234567
    health_check_protocol: http
    health_check_path: /
    successful_response_codes: "200,250-260"
    target_type: ip
    targets:
      - Id: 10.0.0.10
        Port: 80
        AvailabilityZone: all
      - Id: 10.0.0.20
        Port: 80
    state: present
    wait_timeout: 200
    wait: true

# Using lambda as targets require that the target group
# itself is allow to invoke the lambda function.
# therefore you need first to create an empty target group
# to receive its arn, second, allow the target group
# to invoke the lambda function and third, add the target
# to the target group
- name: first, create empty target group
  community.aws.elb_target_group:
    name: my-lambda-targetgroup
    target_type: lambda
    state: present
    modify_targets: false
  register: out

- name: second, allow invoke of the lambda
  community.aws.lambda_policy:
    state: "{{ state | default('present') }}"
    function_name: my-lambda-function
    statement_id: someID
    action: lambda:InvokeFunction
    principal: elasticloadbalancing.amazonaws.com
    source_arn: "{{ out.target_group_arn }}"

- name: third, add target
  community.aws.elb_target_group:
    name: my-lambda-targetgroup
    target_type: lambda
    state: present
    targets:
      - Id: arn:aws:lambda:eu-central-1:123456789012:function:my-lambda-function
"""

RETURN = r"""
deregistration_delay_timeout_seconds:
    description: The amount time for Elastic Load Balancing to wait before changing the state of a deregistering target from draining to unused.
    returned: when state present
    type: int
    sample: 300
deregistration_connection_termination:
    description: Indicates whether the load balancer terminates connections at the end of the deregistration timeout.
    returned: when state present
    type: bool
    sample: True
health_check_interval_seconds:
    description: The approximate amount of time, in seconds, between health checks of an individual target.
    returned: when state present
    type: int
    sample: 30
health_check_path:
    description: The destination for the health check request.
    returned: when state present
    type: str
    sample: /index.html
health_check_port:
    description: The port to use to connect with the target.
    returned: when state present
    type: str
    sample: traffic-port
health_check_protocol:
    description: The protocol to use to connect with the target.
    returned: when state present
    type: str
    sample: HTTP
health_check_timeout_seconds:
    description: The amount of time, in seconds, during which no response means a failed health check.
    returned: when state present
    type: int
    sample: 5
healthy_threshold_count:
    description: The number of consecutive health checks successes required before considering an unhealthy target healthy.
    returned: when state present
    type: int
    sample: 5
load_balancer_arns:
    description: The Amazon Resource Names (ARN) of the load balancers that route traffic to this target group.
    returned: when state present
    type: list
    sample: []
matcher:
    description: The HTTP codes to use when checking for a successful response from a target.
    returned: when state present
    type: dict
    sample: {
        "http_code": "200"
    }
port:
    description: The port on which the targets are listening.
    returned: when state present
    type: int
    sample: 80
protocol:
    description: The protocol to use for routing traffic to the targets.
    returned: when state present
    type: str
    sample: HTTP
stickiness_enabled:
    description: Indicates whether sticky sessions are enabled.
    returned: when state present
    type: bool
    sample: true
stickiness_lb_cookie_duration_seconds:
    description: The time period, in seconds, during which requests from a client should be routed to the same target.
    returned: when state present
    type: int
    sample: 86400
stickiness_type:
    description: The type of sticky sessions.
    returned: when state present
    type: str
    sample: lb_cookie
load_balancing_algorithm_type:
    description: The type load balancing algorithm used.
    returned: when state present
    type: str
    version_added: 3.2.0
    sample: least_outstanding_requests
tags:
    description: The tags attached to the target group.
    returned: when state present
    type: dict
    sample: "{
        'Tag': 'Example'
    }"
target_group_arn:
    description: The Amazon Resource Name (ARN) of the target group.
    returned: when state present
    type: str
    sample: "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/mytargetgroup/aabbccddee0044332211"
target_group_name:
    description: The name of the target group.
    returned: when state present
    type: str
    sample: mytargetgroup
unhealthy_threshold_count:
    description: The number of consecutive health check failures required before considering the target unhealthy.
    returned: when state present
    type: int
    sample: 2
vpc_id:
    description: The ID of the VPC for the targets.
    returned: when state present
    type: str
    sample: vpc-0123456
"""

import time

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

from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict

from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags

from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule


def get_tg_attributes(connection, module, tg_arn):
    try:
        _attributes = connection.describe_target_group_attributes(TargetGroupArn=tg_arn, aws_retry=True)
        tg_attributes = boto3_tag_list_to_ansible_dict(_attributes["Attributes"])
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Couldn't get target group attributes")

    # Replace '.' with '_' in attribute key names to make it more Ansible friendly
    return dict((k.replace(".", "_"), v) for k, v in tg_attributes.items())


def get_target_group_tags(connection, module, target_group_arn):
    try:
        _tags = connection.describe_tags(ResourceArns=[target_group_arn], aws_retry=True)
        return _tags["TagDescriptions"][0]["Tags"]
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Couldn't get target group tags")


def get_target_group(connection, module, retry_missing=False):
    extra_codes = ["TargetGroupNotFound"] if retry_missing else []
    try:
        target_group_paginator = connection.get_paginator("describe_target_groups").paginate(
            Names=[module.params.get("name")]
        )
        jittered_retry = AWSRetry.jittered_backoff(retries=10, catch_extra_error_codes=extra_codes)
        result = jittered_retry(target_group_paginator.build_full_result)()
    except is_boto3_error_code("TargetGroupNotFound"):
        return None
    except (
        botocore.exceptions.ClientError,
        botocore.exceptions.BotoCoreError,
    ) as e:  # pylint: disable=duplicate-except
        module.fail_json_aws(e, msg="Couldn't get target group")

    return result["TargetGroups"][0]


def wait_for_status(connection, module, target_group_arn, targets, status):
    polling_increment_secs = 5
    max_retries = module.params.get("wait_timeout") // polling_increment_secs
    status_achieved = False

    for x in range(0, max_retries):
        try:
            response = connection.describe_target_health(
                TargetGroupArn=target_group_arn, Targets=targets, aws_retry=True
            )
            if response["TargetHealthDescriptions"][0]["TargetHealth"]["State"] == status:
                status_achieved = True
                break
            time.sleep(polling_increment_secs)
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            module.fail_json_aws(e, msg="Couldn't describe target health")

    result = response
    return status_achieved, result


def create_or_update_attributes(connection, module, target_group, new_target_group):
    changed = False
    target_type = module.params.get("target_type")
    deregistration_delay_timeout = module.params.get("deregistration_delay_timeout")
    deregistration_connection_termination = module.params.get("deregistration_connection_termination")
    stickiness_enabled = module.params.get("stickiness_enabled")
    stickiness_lb_cookie_duration = module.params.get("stickiness_lb_cookie_duration")
    stickiness_type = module.params.get("stickiness_type")
    stickiness_app_cookie_duration = module.params.get("stickiness_app_cookie_duration")
    stickiness_app_cookie_name = module.params.get("stickiness_app_cookie_name")
    preserve_client_ip_enabled = module.params.get("preserve_client_ip_enabled")
    proxy_protocol_v2_enabled = module.params.get("proxy_protocol_v2_enabled")
    load_balancing_algorithm_type = module.params.get("load_balancing_algorithm_type")

    # Now set target group attributes
    update_attributes = []

    # Get current attributes
    current_tg_attributes = get_tg_attributes(connection, module, target_group["TargetGroupArn"])

    if deregistration_delay_timeout is not None:
        if str(deregistration_delay_timeout) != current_tg_attributes["deregistration_delay_timeout_seconds"]:
            update_attributes.append(
                {"Key": "deregistration_delay.timeout_seconds", "Value": str(deregistration_delay_timeout)}
            )
    if deregistration_connection_termination is not None:
        if (
            deregistration_connection_termination
            and current_tg_attributes.get("deregistration_delay_connection_termination_enabled") != "true"
        ):
            update_attributes.append({"Key": "deregistration_delay.connection_termination.enabled", "Value": "true"})
    if stickiness_enabled is not None:
        if stickiness_enabled and current_tg_attributes["stickiness_enabled"] != "true":
            update_attributes.append({"Key": "stickiness.enabled", "Value": "true"})
    if stickiness_lb_cookie_duration is not None:
        if str(stickiness_lb_cookie_duration) != current_tg_attributes["stickiness_lb_cookie_duration_seconds"]:
            update_attributes.append(
                {"Key": "stickiness.lb_cookie.duration_seconds", "Value": str(stickiness_lb_cookie_duration)}
            )
    if stickiness_type is not None:
        if stickiness_type != current_tg_attributes.get("stickiness_type"):
            update_attributes.append({"Key": "stickiness.type", "Value": stickiness_type})
    if stickiness_app_cookie_name is not None:
        if stickiness_app_cookie_name != current_tg_attributes.get("stickiness_app_cookie_name"):
            update_attributes.append(
                {"Key": "stickiness.app_cookie.cookie_name", "Value": str(stickiness_app_cookie_name)}
            )
    if stickiness_app_cookie_duration is not None:
        if str(stickiness_app_cookie_duration) != current_tg_attributes["stickiness_app_cookie_duration_seconds"]:
            update_attributes.append(
                {"Key": "stickiness.app_cookie.duration_seconds", "Value": str(stickiness_app_cookie_duration)}
            )
    if preserve_client_ip_enabled is not None:
        if target_type not in ("udp", "tcp_udp"):
            if str(preserve_client_ip_enabled).lower() != current_tg_attributes.get("preserve_client_ip_enabled"):
                update_attributes.append(
                    {"Key": "preserve_client_ip.enabled", "Value": str(preserve_client_ip_enabled).lower()}
                )
    if proxy_protocol_v2_enabled is not None:
        if str(proxy_protocol_v2_enabled).lower() != current_tg_attributes.get("proxy_protocol_v2_enabled"):
            update_attributes.append(
                {"Key": "proxy_protocol_v2.enabled", "Value": str(proxy_protocol_v2_enabled).lower()}
            )
    if load_balancing_algorithm_type is not None:
        if str(load_balancing_algorithm_type) != current_tg_attributes["load_balancing_algorithm_type"]:
            update_attributes.append(
                {"Key": "load_balancing.algorithm.type", "Value": str(load_balancing_algorithm_type)}
            )

    if update_attributes:
        try:
            connection.modify_target_group_attributes(
                TargetGroupArn=target_group["TargetGroupArn"], Attributes=update_attributes, aws_retry=True
            )
            changed = True
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            # Something went wrong setting attributes. If this target group was created during this task, delete it to leave a consistent state
            if new_target_group:
                connection.delete_target_group(TargetGroupArn=target_group["TargetGroupArn"], aws_retry=True)
            module.fail_json_aws(e, msg="Couldn't delete target group")

    return changed


def create_or_update_target_group(connection, module):
    changed = False
    new_target_group = False
    params = dict()
    target_type = module.params.get("target_type")
    params["Name"] = module.params.get("name")
    params["TargetType"] = target_type
    if target_type != "lambda":
        params["Protocol"] = module.params.get("protocol").upper()
        if module.params.get("protocol_version") is not None:
            params["ProtocolVersion"] = module.params.get("protocol_version")
        params["Port"] = module.params.get("port")
        params["VpcId"] = module.params.get("vpc_id")
    tags = module.params.get("tags")
    purge_tags = module.params.get("purge_tags")

    health_option_keys = [
        "health_check_path",
        "health_check_protocol",
        "health_check_interval",
        "health_check_timeout",
        "healthy_threshold_count",
        "unhealthy_threshold_count",
        "successful_response_codes",
    ]
    health_options = any(module.params[health_option_key] is not None for health_option_key in health_option_keys)

    # Set health check if anything set
    if health_options:
        if module.params.get("health_check_protocol") is not None:
            params["HealthCheckProtocol"] = module.params.get("health_check_protocol").upper()

        if module.params.get("health_check_port") is not None:
            params["HealthCheckPort"] = module.params.get("health_check_port")

        if module.params.get("health_check_interval") is not None:
            params["HealthCheckIntervalSeconds"] = module.params.get("health_check_interval")

        if module.params.get("health_check_timeout") is not None:
            params["HealthCheckTimeoutSeconds"] = module.params.get("health_check_timeout")

        if module.params.get("healthy_threshold_count") is not None:
            params["HealthyThresholdCount"] = module.params.get("healthy_threshold_count")

        if module.params.get("unhealthy_threshold_count") is not None:
            params["UnhealthyThresholdCount"] = module.params.get("unhealthy_threshold_count")

        # Only need to check response code and path for http(s) health checks
        protocol = module.params.get("health_check_protocol")
        if protocol is not None and protocol.upper() in ["HTTP", "HTTPS"]:
            if module.params.get("health_check_path") is not None:
                params["HealthCheckPath"] = module.params.get("health_check_path")

            if module.params.get("successful_response_codes") is not None:
                params["Matcher"] = {}
                code_key = "HttpCode"
                protocol_version = module.params.get("protocol_version")
                if protocol_version is not None and protocol_version.upper() == "GRPC":
                    code_key = "GrpcCode"
                params["Matcher"][code_key] = module.params.get("successful_response_codes")

    # Get target group
    target_group = get_target_group(connection, module)

    if target_group:
        diffs = [param for param in ("Port", "Protocol", "VpcId") if target_group.get(param) != params.get(param)]
        if diffs:
            module.fail_json(msg=f"Cannot modify {', '.join(diffs)} parameter(s) for a target group")
        # Target group exists so check health check parameters match what has been passed
        health_check_params = dict()

        # Modify health check if anything set
        if health_options:
            # Health check protocol
            if "HealthCheckProtocol" in params and target_group["HealthCheckProtocol"] != params["HealthCheckProtocol"]:
                health_check_params["HealthCheckProtocol"] = params["HealthCheckProtocol"]

            # Health check port
            if "HealthCheckPort" in params and target_group["HealthCheckPort"] != params["HealthCheckPort"]:
                health_check_params["HealthCheckPort"] = params["HealthCheckPort"]

            # Health check interval
            if (
                "HealthCheckIntervalSeconds" in params
                and target_group["HealthCheckIntervalSeconds"] != params["HealthCheckIntervalSeconds"]
            ):
                health_check_params["HealthCheckIntervalSeconds"] = params["HealthCheckIntervalSeconds"]

            # Health check timeout
            if (
                "HealthCheckTimeoutSeconds" in params
                and target_group["HealthCheckTimeoutSeconds"] != params["HealthCheckTimeoutSeconds"]
            ):
                health_check_params["HealthCheckTimeoutSeconds"] = params["HealthCheckTimeoutSeconds"]

            # Healthy threshold
            if (
                "HealthyThresholdCount" in params
                and target_group["HealthyThresholdCount"] != params["HealthyThresholdCount"]
            ):
                health_check_params["HealthyThresholdCount"] = params["HealthyThresholdCount"]

            # Unhealthy threshold
            if (
                "UnhealthyThresholdCount" in params
                and target_group["UnhealthyThresholdCount"] != params["UnhealthyThresholdCount"]
            ):
                health_check_params["UnhealthyThresholdCount"] = params["UnhealthyThresholdCount"]

            # Only need to check response code and path for http(s) health checks
            if target_group["HealthCheckProtocol"] in ["HTTP", "HTTPS"]:
                # Health check path
                if "HealthCheckPath" in params and target_group["HealthCheckPath"] != params["HealthCheckPath"]:
                    health_check_params["HealthCheckPath"] = params["HealthCheckPath"]

                # Matcher (successful response codes)
                # TODO: required and here?
                if "Matcher" in params:
                    code_key = "HttpCode"
                    if target_group.get("ProtocolVersion") == "GRPC":
                        code_key = "GrpcCode"
                    current_matcher_list = target_group["Matcher"][code_key].split(",")
                    requested_matcher_list = params["Matcher"][code_key].split(",")
                    if set(current_matcher_list) != set(requested_matcher_list):
                        health_check_params["Matcher"] = {}
                        health_check_params["Matcher"][code_key] = ",".join(requested_matcher_list)

            try:
                if health_check_params:
                    connection.modify_target_group(
                        TargetGroupArn=target_group["TargetGroupArn"], aws_retry=True, **health_check_params
                    )
                    changed = True
            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                module.fail_json_aws(e, msg="Couldn't update target group")

        # Do we need to modify targets?
        if module.params.get("modify_targets"):
            # get list of current target instances. I can't see anything like a describe targets in the doco so
            # describe_target_health seems to be the only way to get them
            try:
                current_targets = connection.describe_target_health(
                    TargetGroupArn=target_group["TargetGroupArn"], aws_retry=True
                )
            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                module.fail_json_aws(e, msg="Couldn't get target group health")

            if module.params.get("targets"):
                if target_type != "lambda":
                    params["Targets"] = module.params.get("targets")

                    # Correct type of target ports
                    for target in params["Targets"]:
                        target["Port"] = int(target.get("Port", module.params.get("port")))

                    current_instance_ids = []

                    for instance in current_targets["TargetHealthDescriptions"]:
                        current_instance_ids.append(instance["Target"]["Id"])

                    new_instance_ids = []
                    for instance in params["Targets"]:
                        new_instance_ids.append(instance["Id"])

                    add_instances = set(new_instance_ids) - set(current_instance_ids)

                    if add_instances:
                        instances_to_add = []
                        for target in params["Targets"]:
                            if target["Id"] in add_instances:
                                tmp_item = {"Id": target["Id"], "Port": target["Port"]}
                                if target.get("AvailabilityZone"):
                                    tmp_item["AvailabilityZone"] = target["AvailabilityZone"]
                                instances_to_add.append(tmp_item)

                        changed = True
                        try:
                            connection.register_targets(
                                TargetGroupArn=target_group["TargetGroupArn"], Targets=instances_to_add, aws_retry=True
                            )
                        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                            module.fail_json_aws(e, msg="Couldn't register targets")

                        if module.params.get("wait"):
                            status_achieved, registered_instances = wait_for_status(
                                connection, module, target_group["TargetGroupArn"], instances_to_add, "healthy"
                            )
                            if not status_achieved:
                                module.fail_json(
                                    msg="Error waiting for target registration to be healthy - please check the AWS console"
                                )

                    remove_instances = set(current_instance_ids) - set(new_instance_ids)

                    if remove_instances:
                        instances_to_remove = []
                        for target in current_targets["TargetHealthDescriptions"]:
                            if target["Target"]["Id"] in remove_instances:
                                instances_to_remove.append(
                                    {"Id": target["Target"]["Id"], "Port": target["Target"]["Port"]}
                                )

                        changed = True
                        try:
                            connection.deregister_targets(
                                TargetGroupArn=target_group["TargetGroupArn"],
                                Targets=instances_to_remove,
                                aws_retry=True,
                            )
                        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                            module.fail_json_aws(e, msg="Couldn't remove targets")

                        if module.params.get("wait"):
                            status_achieved, registered_instances = wait_for_status(
                                connection, module, target_group["TargetGroupArn"], instances_to_remove, "unused"
                            )
                            if not status_achieved:
                                module.fail_json(
                                    msg="Error waiting for target deregistration - please check the AWS console"
                                )

                # register lambda target
                else:
                    try:
                        changed = False
                        target = module.params.get("targets")[0]
                        if len(current_targets["TargetHealthDescriptions"]) == 0:
                            changed = True
                        else:
                            for item in current_targets["TargetHealthDescriptions"]:
                                if target["Id"] != item["Target"]["Id"]:
                                    changed = True
                                    break  # only one target is possible with lambda

                        if changed:
                            if target.get("Id"):
                                connection.register_targets(
                                    TargetGroupArn=target_group["TargetGroupArn"],
                                    Targets=[{"Id": target["Id"]}],
                                    aws_retry=True,
                                )

                    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                        module.fail_json_aws(e, msg="Couldn't register targets")
            else:
                if target_type != "lambda":
                    current_instances = current_targets["TargetHealthDescriptions"]

                    if current_instances:
                        instances_to_remove = []
                        for target in current_targets["TargetHealthDescriptions"]:
                            instances_to_remove.append({"Id": target["Target"]["Id"], "Port": target["Target"]["Port"]})

                        changed = True
                        try:
                            connection.deregister_targets(
                                TargetGroupArn=target_group["TargetGroupArn"],
                                Targets=instances_to_remove,
                                aws_retry=True,
                            )
                        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                            module.fail_json_aws(e, msg="Couldn't remove targets")

                        if module.params.get("wait"):
                            status_achieved, registered_instances = wait_for_status(
                                connection, module, target_group["TargetGroupArn"], instances_to_remove, "unused"
                            )
                            if not status_achieved:
                                module.fail_json(
                                    msg="Error waiting for target deregistration - please check the AWS console"
                                )

                # remove lambda targets
                else:
                    changed = False
                    if current_targets["TargetHealthDescriptions"]:
                        changed = True
                        # only one target is possible with lambda
                        target_to_remove = current_targets["TargetHealthDescriptions"][0]["Target"]["Id"]
                    if changed:
                        connection.deregister_targets(
                            TargetGroupArn=target_group["TargetGroupArn"],
                            Targets=[{"Id": target_to_remove}],
                            aws_retry=True,
                        )
    else:
        try:
            connection.create_target_group(aws_retry=True, **params)
            changed = True
            new_target_group = True
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            module.fail_json_aws(e, msg="Couldn't create target group")

        target_group = get_target_group(connection, module, retry_missing=True)

        if module.params.get("targets"):
            if target_type != "lambda":
                params["Targets"] = module.params.get("targets")
                try:
                    connection.register_targets(
                        TargetGroupArn=target_group["TargetGroupArn"], Targets=params["Targets"], aws_retry=True
                    )
                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                    module.fail_json_aws(e, msg="Couldn't register targets")

                if module.params.get("wait"):
                    status_achieved, registered_instances = wait_for_status(
                        connection, module, target_group["TargetGroupArn"], params["Targets"], "healthy"
                    )
                    if not status_achieved:
                        module.fail_json(
                            msg="Error waiting for target registration to be healthy - please check the AWS console"
                        )

            else:
                try:
                    target = module.params.get("targets")[0]
                    connection.register_targets(
                        TargetGroupArn=target_group["TargetGroupArn"], Targets=[{"Id": target["Id"]}], aws_retry=True
                    )
                    changed = True
                except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                    module.fail_json_aws(e, msg="Couldn't register targets")

    attributes_update = create_or_update_attributes(connection, module, target_group, new_target_group)

    if attributes_update:
        changed = True

    # Tags - only need to play with tags if tags parameter has been set to something
    if tags is not None:
        # Get tags
        current_tags = get_target_group_tags(connection, module, target_group["TargetGroupArn"])

        # Delete necessary tags
        tags_need_modify, tags_to_delete = compare_aws_tags(
            boto3_tag_list_to_ansible_dict(current_tags), tags, purge_tags
        )
        if tags_to_delete:
            try:
                connection.remove_tags(
                    ResourceArns=[target_group["TargetGroupArn"]], TagKeys=tags_to_delete, aws_retry=True
                )
            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                module.fail_json_aws(e, msg="Couldn't delete tags from target group")
            changed = True

        # Add/update tags
        if tags_need_modify:
            try:
                connection.add_tags(
                    ResourceArns=[target_group["TargetGroupArn"]],
                    Tags=ansible_dict_to_boto3_tag_list(tags_need_modify),
                    aws_retry=True,
                )
            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
                module.fail_json_aws(e, msg="Couldn't add tags to target group")
            changed = True

    # Get the target group again
    target_group = get_target_group(connection, module)

    # Get the target group attributes again
    target_group.update(get_tg_attributes(connection, module, target_group["TargetGroupArn"]))

    # Convert target_group to snake_case
    snaked_tg = camel_dict_to_snake_dict(target_group)

    snaked_tg["tags"] = boto3_tag_list_to_ansible_dict(
        get_target_group_tags(connection, module, target_group["TargetGroupArn"])
    )

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


def delete_target_group(connection, module):
    changed = False
    tg = get_target_group(connection, module)

    if tg:
        try:
            connection.delete_target_group(TargetGroupArn=tg["TargetGroupArn"], aws_retry=True)
            changed = True
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            module.fail_json_aws(e, msg="Couldn't delete target group")

    module.exit_json(changed=changed)


def main():
    protocols_list = ["http", "https", "tcp", "tls", "udp", "tcp_udp", "HTTP", "HTTPS", "TCP", "TLS", "UDP", "TCP_UDP"]
    argument_spec = dict(
        deregistration_delay_timeout=dict(type="int"),
        deregistration_connection_termination=dict(type="bool", default=False),
        health_check_protocol=dict(choices=protocols_list),
        health_check_port=dict(),
        health_check_path=dict(),
        health_check_interval=dict(type="int"),
        health_check_timeout=dict(type="int"),
        healthy_threshold_count=dict(type="int"),
        modify_targets=dict(default=True, type="bool"),
        name=dict(required=True),
        port=dict(type="int"),
        protocol=dict(choices=protocols_list),
        protocol_version=dict(type="str", choices=["GRPC", "HTTP1", "HTTP2"]),
        purge_tags=dict(default=True, type="bool"),
        stickiness_enabled=dict(type="bool"),
        stickiness_type=dict(),
        stickiness_lb_cookie_duration=dict(type="int"),
        stickiness_app_cookie_duration=dict(type="int"),
        stickiness_app_cookie_name=dict(),
        load_balancing_algorithm_type=dict(type="str", choices=["round_robin", "least_outstanding_requests"]),
        state=dict(required=True, choices=["present", "absent"]),
        successful_response_codes=dict(),
        tags=dict(type="dict", aliases=["resource_tags"]),
        target_type=dict(choices=["instance", "ip", "lambda", "alb"]),
        targets=dict(type="list", elements="dict"),
        unhealthy_threshold_count=dict(type="int"),
        vpc_id=dict(),
        preserve_client_ip_enabled=dict(type="bool"),
        proxy_protocol_v2_enabled=dict(type="bool"),
        wait_timeout=dict(type="int", default=200),
        wait=dict(type="bool", default=False),
    )
    required_by = dict(
        health_check_path=["health_check_protocol"],
        successful_response_codes=["health_check_protocol"],
    )
    required_if = [
        ["target_type", "instance", ["protocol", "port", "vpc_id"]],
        ["target_type", "ip", ["protocol", "port", "vpc_id"]],
        ["target_type", "alb", ["protocol", "port", "vpc_id"]],
    ]

    module = AnsibleAWSModule(argument_spec=argument_spec, required_by=required_by, required_if=required_if)

    if module.params.get("target_type") is None:
        module.params["target_type"] = "instance"

    connection = module.client("elbv2", retry_decorator=AWSRetry.jittered_backoff(retries=10))

    if module.params.get("state") == "present":
        if module.params.get("protocol") in ["http", "https", "HTTP", "HTTPS"] and module.params.get(
            "deregistration_connection_termination", None
        ):
            module.fail_json(
                msg="A target group with HTTP/S protocol does not support setting deregistration_connection_termination"
            )

        create_or_update_target_group(connection, module)
    else:
        delete_target_group(connection, module)


if __name__ == "__main__":
    main()
