data "aws_caller_identity" "current" {}

data "aws_vpc" "current_vpc" {
  id = var.vpc_id
}

data "aws_iam_policy" "permissions_boundary_policy" {
  count = local.permissions_boundary_enabled ? 1 : 0
  name  = var.permissions_boundary_policy_name
}

locals {
  account_id       = data.aws_caller_identity.current.account_id
  service_name     = var.secrets_manager_service_name != "" ? var.secrets_manager_service_name : var.service_name
  ecs_cluster_name = var.ecs_cluster_name != "" ? var.ecs_cluster_name : aws_ecs_cluster.fargate_ecs_cluster[0].name
  ecs_cluster_arn  = var.ecs_cluster_name != "" ? data.aws_ecs_cluster.fargate_ecs_cluster[0].arn : aws_ecs_cluster.fargate_ecs_cluster[0].arn
  platform_version = var.task_type == "external_worker" ? null : var.service_platform_version

  primsa_sg_exception_tags = {
    "eiso-exception" = "aws.08.30"
  }

  cw_event_pattern = jsonencode({
    detail-type = ["ECS Task State Change"]
    source      = ["aws.ecs"]
    detail = {
      desiredStatus     = ["STOPPED"]
      lastStatus        = ["STOPPED"]
      clusterArn        = [local.ecs_cluster_arn]
      taskDefinitionArn = [{ "prefix" = aws_ecs_task_definition.fargate_task.arn_without_revision }]
    }
  })
}

# Assume role policies
data "aws_iam_policy_document" "assume_role_policy_task_execution" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type = "Service"

      identifiers = [
        "ecs.amazonaws.com",
        "ec2.amazonaws.com",
        "ecs-tasks.amazonaws.com",
        "application-autoscaling.amazonaws.com",
      ]
    }
  }
}

data "aws_iam_policy_document" "assume_role_policy_task" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type = "Service"

      identifiers = [
        "ecs.amazonaws.com",
        "ec2.amazonaws.com",
        "ecs-tasks.amazonaws.com",
        "application-autoscaling.amazonaws.com",
      ]
    }

    dynamic "principals" {
      for_each = local.additional_task_role_principals_enabled
      content {
        type        = "AWS"
        identifiers = var.additional_task_role_principals
      }
    }
  }
}

data "aws_secretsmanager_secret" "datadog_api_key" {
  name = "${var.environment}/datadog/DD_API_KEY"
}

data "aws_iam_role" "fargate_autoscaling_role" {
  name = "AWSServiceRoleForApplicationAutoScaling_ECSService"
}

# Get private subnet prefix list for default allow rules
data "aws_ec2_managed_prefix_lists" "private_subnets" {
  for_each = toset(var.task_type == "web_service" ? ["PREFIX_LISTS"] : [])
  tags = {
    environment  = var.environment
    subnet_group = "all"
    tier         = "private"
    vpc_id       = data.aws_vpc.current_vpc.id
  }
}

data "aws_ec2_managed_prefix_list" "private_subnets" {
  for_each = data.aws_ec2_managed_prefix_lists.private_subnets
  id       = one(data.aws_ec2_managed_prefix_lists.private_subnets[each.key].ids)
}

data "aws_ec2_managed_prefix_list" "http_listener_allow_prefix_lists" {
  for_each = var.task_type == "web_service" ? toset(local.http_listener_allow_prefix_lists) : []
  name     = each.value
}

data "aws_ec2_managed_prefix_list" "https_listener_allow_prefix_lists" {
  for_each = var.task_type == "web_service" ? toset(local.https_listener_allow_prefix_lists) : []
  name     = each.value
}

# Task run role. Used for running the task
resource "aws_iam_role" "fargate_run_task_role" {
  count                = var.task_type == "worker" && length(var.worker_run_task_role_principals) > 0 ? 1 : 0
  name                 = "${var.environment}-${var.service_name}-task-run-role"
  assume_role_policy   = data.aws_iam_policy_document.assume_role_policy_task_run[count.index].json
  permissions_boundary = local.permissions_boundary_arn

  tags = local.combined_resource_tags

  lifecycle {
    precondition {
      condition     = var.task_type == "worker"
      error_message = "fargate_run_task_role is only supported for worker tasks"
    }

    precondition {
      condition     = length("${var.environment}-${var.service_name}-task-run-role") <= 64
      error_message = "IAM role name must be 64 characters or less"
    }
  }
}

data "aws_iam_policy_document" "assume_role_policy_task_run" {
  count = var.task_type == "worker" && length(var.worker_run_task_role_principals) > 0 ? 1 : 0
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "AWS"
      identifiers = var.worker_run_task_role_principals
    }
  }
}

data "aws_iam_policy_document" "run_task_policy_document" {
  count = var.task_type == "worker" && length(var.worker_run_task_role_principals) > 0 ? 1 : 0
  statement {
    effect = "Allow"
    actions = [
      "ec2:DescribeSecurityGroups",
      "ec2:DescribeSubnets",
      "ec2:DescribeVpcs",
      "ecs:DescribeTasks"
    ]
    resources = ["*"]
  }

  statement {
    effect = "Allow"
    actions = [
      "ecs:DescribeServices"
    ]
    resources = [concat(aws_ecs_service.fargate_service_without_load_balancer.*.arn, [""])[0]]
  }

  statement {
    effect = "Allow"
    actions = [
      "ecs:RunTask",
    ]
    resources = [
      "${aws_ecs_task_definition.fargate_task.arn_without_revision}:*"
    ]
  }

  statement {
    effect = "Allow"
    actions = [
      "ecs:TagResource",
    ]
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "ecs:CreateAction"
      values = [
        "RunTask",
      ]
    }
  }

  statement {
    effect  = "Allow"
    actions = ["iam:PassRole"]
    condition {
      test     = "StringLike"
      variable = "iam:PassedToService"
      values   = ["ecs-tasks.amazonaws.com"]
    }
    resources = [
      aws_iam_role.fargate_task_role.arn,
      aws_iam_role.fargate_task_execution_role.arn
    ]
  }
}

resource "aws_iam_policy" "fargate_run_task_policy" {
  count  = var.task_type == "worker" && length(var.worker_run_task_role_principals) > 0 ? 1 : 0
  name   = "${var.environment}-${var.service_name}-run-task-policy"
  policy = data.aws_iam_policy_document.run_task_policy_document[count.index].json

  tags = local.combined_resource_tags
}

resource "aws_iam_role_policy_attachment" "fargate_run_task_policy_attachment" {
  count      = var.task_type == "worker" && length(var.worker_run_task_role_principals) > 0 ? 1 : 0
  role       = aws_iam_role.fargate_run_task_role[count.index].id
  policy_arn = aws_iam_policy.fargate_run_task_policy[count.index].arn
}


# Task execution role. Used for interacting with ECR, shipping logs, etc...
resource "aws_iam_role" "fargate_task_execution_role" {
  name                 = "${var.environment}-${var.service_name}-task-execution-role"
  assume_role_policy   = data.aws_iam_policy_document.assume_role_policy_task_execution.json
  permissions_boundary = local.permissions_boundary_arn

  tags = local.combined_resource_tags

  lifecycle {
    precondition {
      condition     = length("${var.environment}-${var.service_name}-task-execution-role") <= 64
      error_message = "IAM role name must be 64 characters or less"
    }
  }
}

resource "aws_iam_role_policy_attachment" "fargate_task_execution_role_existing_iam_policy_attachment" {
  count      = length(var.execution_role_iam_managed_policy_attachments)
  role       = aws_iam_role.fargate_task_execution_role.name
  policy_arn = var.execution_role_iam_managed_policy_attachments[count.index]
}

data "aws_iam_policy_document" "secrets_manager_policy" {
  statement {
    actions = [
      "secretsmanager:GetResourcePolicy",
      "secretsmanager:GetSecretValue",
      "secretsmanager:DescribeSecret",
      "secretsmanager:ListSecretVersionIds",
    ]

    resources = [
      "arn:aws:secretsmanager:*:*:secret:${var.environment}/${local.service_name}/",
      "arn:aws:secretsmanager:*:*:secret:${var.environment}/${local.service_name}/*",
    ]
  }

  statement {
    actions = [
      "secretsmanager:GetRandomPassword",
    ]

    resources = [
      "*",
    ]
  }
}

module "aws_accounts" {
  count  = (var.task_type == "web_service" && var.custom_waf_arn == "") ? 1 : 0
  source = "git@github.com:theorchard/terraform-aws-accounts-map.git//modules/read_accounts_map?ref=3.1.1"
}

# Get the default AWS WAF web ACL to use if no custom WAF specified
data "aws_wafv2_web_acl" "default_waf" {
  count = (var.task_type == "web_service" && var.custom_waf_arn == "") ? 1 : 0
  name  = local.default_waf_name
  scope = "REGIONAL"
}

# Task execution role policy attachments
resource "aws_iam_role_policy_attachment" "fargate_iam_task_execution_policy_attachement" {
  role       = aws_iam_role.fargate_task_execution_role.id
  policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/Fargate-task-execution-policy"
}

resource "aws_iam_role_policy_attachment" "fargate_iam_task_execution_secrets_policy_attachement" {
  role       = aws_iam_role.fargate_task_execution_role.id
  policy_arn = aws_iam_policy.fargate_secrets_manager_policy.arn
}

resource "aws_iam_role_policy_attachment" "fargate_datadog_iam_task_execution_policy_attachement" {
  role       = aws_iam_role.fargate_task_execution_role.id
  policy_arn = local.datadog_secrets_manager_policy_arn
}

# Task role. This is the actual role provided to containers at runtime
resource "aws_iam_role" "fargate_task_role" {
  name                 = "${var.environment}-${var.service_name}-task-role"
  assume_role_policy   = data.aws_iam_policy_document.assume_role_policy_task.json
  max_session_duration = var.max_session_duration
  permissions_boundary = local.permissions_boundary_arn

  tags = local.combined_resource_tags

  lifecycle {
    precondition {
      condition     = length("${var.environment}-${var.service_name}-task-role") <= 64
      error_message = "IAM role name must be 64 characters or less"
    }
  }
}

# Secrets manager policy for task role
resource "aws_iam_policy" "fargate_secrets_manager_policy" {
  name   = "SecretsManager-${var.environment}-${var.service_name}-policy"
  policy = data.aws_iam_policy_document.secrets_manager_policy.json
}

# Attach secrets manager policy to task role
resource "aws_iam_role_policy_attachment" "fargate_secrets_manager_policy_attachement" {
  role       = aws_iam_role.fargate_task_role.id
  policy_arn = aws_iam_policy.fargate_secrets_manager_policy.arn
}

# Allow IAM policy from file to configure case-specific access requirements, if enabled.
resource "aws_iam_role_policy" "fargate_iam_task_policy" {
  count  = var.iam_policy_file_enabled ? 1 : 0
  name   = "${var.environment}-${var.service_name}-task-policy"
  role   = aws_iam_role.fargate_task_role.name
  policy = file("policies/${var.service_name}.json")
}

# Attach additional existing managed IAM policies, if enabled
resource "aws_iam_role_policy_attachment" "fargate_task_role_existing_iam_policy_attachment" {
  count      = length(var.iam_managed_policy_attachments)
  role       = aws_iam_role.fargate_task_role.name
  policy_arn = var.iam_managed_policy_attachments[count.index]
}

# Attach datadog secrets manager policy to fetch API keys
resource "aws_iam_role_policy_attachment" "datadog_secrets_manager_iam_policy_attachment" {
  count      = var.datadog_enabled ? 1 : 0
  role       = aws_iam_role.fargate_task_role.name
  policy_arn = local.datadog_secrets_manager_policy_arn
}

# Attach ows machine-to-machine secrets manager policy
resource "aws_iam_role_policy_attachment" "ows_machine_to_machine_secrets_manager_iam_policy_attachment" {
  count      = var.ows_machine_to_machine_enabled ? 1 : 0
  role       = aws_iam_role.fargate_task_role.name
  policy_arn = local.ows_machine_to_machine_manager_policy_arn
}

# Attach split secrets manager policies
resource "aws_iam_role_policy_attachment" "python_orchard_features_splitio_secrets_manager_iam_policy_attachment" {
  count      = var.splitio_enabled ? 1 : 0
  role       = aws_iam_role.fargate_task_role.name
  policy_arn = local.python_orchard_features_splitio_secrets_manager_policy_arn
}

resource "aws_iam_role_policy_attachment" "python_orchard_features_splitio_secrets_manager_iam_policy_attachment_exec_role" {
  count      = var.splitio_enabled ? 1 : 0
  role       = aws_iam_role.fargate_task_execution_role.name
  policy_arn = local.python_orchard_features_splitio_secrets_manager_policy_arn
}

resource "aws_iam_role_policy_attachment" "splitio_secrets_manager_iam_policy_attachment" {
  count      = var.splitio_enabled ? 1 : 0
  role       = aws_iam_role.fargate_task_role.name
  policy_arn = local.split_secrets_manager_policy_arn
}

resource "aws_iam_role_policy_attachment" "splitio_secrets_manager_iam_policy_attachment_exec_role" {
  count      = var.splitio_enabled ? 1 : 0
  role       = aws_iam_role.fargate_task_execution_role.name
  policy_arn = local.split_secrets_manager_policy_arn
}

# Attach swf-workflow-monitor policy
resource "aws_iam_role_policy_attachment" "swf_workflow_monitor_iam_policy_attachment" {
  count      = var.swf_worker_enabled ? 1 : 0
  role       = aws_iam_role.fargate_task_role.name
  policy_arn = local.swf_workflow_monitor_policy_arn
}

# Security group for task instances
resource "aws_security_group" "task_security_group" {
  count       = var.task_type == "external_worker" ? 0 : 1 # External workers don't need a security group
  name        = "${var.environment}-${var.service_name}-task-security-group"
  description = "Task security group for ${var.environment}-${var.service_name}"
  vpc_id      = data.aws_vpc.current_vpc.id

  tags = merge({ "type" = "task" }, local.combined_resource_tags, local.primsa_sg_exception_tags)
}

resource "aws_security_group_rule" "allow_inbound_from_load_balancer" {
  count                    = var.task_type == "web_service" ? 1 : 0
  type                     = "ingress"
  from_port                = var.container_port
  to_port                  = var.container_port
  protocol                 = "TCP"
  source_security_group_id = aws_security_group.load_balancer_security_group[count.index].id
  security_group_id        = aws_security_group.task_security_group[0].id
}

resource "aws_security_group_rule" "allow_task_egress" {
  count             = var.task_type == "external_worker" ? 0 : 1
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.task_security_group[0].id
}

# Security group for load balancer
resource "aws_security_group" "load_balancer_security_group" {
  count       = var.task_type == "web_service" ? 1 : 0
  name        = "${var.environment}-${var.service_name}-load-balancer-security-group"
  description = "Load balancer security group for ${var.environment}-${var.service_name}"
  vpc_id      = var.vpc_id

  tags = merge({ "type" = "lb" }, local.combined_resource_tags, local.primsa_sg_exception_tags)
}

# Create load balancer security group rules
resource "aws_security_group_rule" "allow_http_inbound" {
  count             = var.task_type == "web_service" && var.http_listener_enabled ? 1 : 0
  type              = "ingress"
  from_port         = var.http_listener_port
  to_port           = var.http_listener_port
  protocol          = "TCP"
  cidr_blocks       = local.http_listener_allow_cidr_blocks
  prefix_list_ids   = [for prefix_list in data.aws_ec2_managed_prefix_list.http_listener_allow_prefix_lists : prefix_list.id]
  security_group_id = aws_security_group.load_balancer_security_group[0].id
}

resource "aws_security_group_rule" "allow_https_inbound" {
  count             = var.task_type == "web_service" && var.https_listener_enabled ? 1 : 0
  type              = "ingress"
  from_port         = var.https_listener_port
  to_port           = var.https_listener_port
  protocol          = "TCP"
  cidr_blocks       = local.https_listener_allow_cidr_blocks
  prefix_list_ids   = [for prefix_list in data.aws_ec2_managed_prefix_list.https_listener_allow_prefix_lists : prefix_list.id]
  security_group_id = aws_security_group.load_balancer_security_group[0].id
}

resource "aws_security_group_rule" "allow_https_inbound_from_sg" {
  for_each                 = var.task_type == "web_service" && var.https_listener_enabled ? toset(var.https_listener_allow_security_group_ids) : toset([])
  type                     = "ingress"
  from_port                = var.https_listener_port
  to_port                  = var.https_listener_port
  protocol                 = "TCP"
  source_security_group_id = each.value
  security_group_id        = aws_security_group.load_balancer_security_group[0].id
}

# Allow egress only to task security group
resource "aws_security_group_rule" "allow_outbound_access_to_task" {
  count                    = var.task_type == "web_service" ? 1 : 0
  type                     = "egress"
  from_port                = var.container_port
  to_port                  = var.container_port
  protocol                 = "TCP"
  source_security_group_id = aws_security_group.task_security_group[0].id
  security_group_id        = aws_security_group.load_balancer_security_group[0].id
}

resource "aws_ecs_cluster" "fargate_ecs_cluster" {
  count = var.ecs_cluster_name != "" ? 0 : 1
  name  = "${var.environment}-${var.service_name}"

  setting {
    name  = "containerInsights"
    value = local.container_insights_setting
  }

  tags = local.combined_resource_tags
}

data "aws_ecs_cluster" "fargate_ecs_cluster" {
  count        = var.ecs_cluster_name != "" ? 1 : 0
  cluster_name = var.ecs_cluster_name
}

resource "aws_ecs_task_definition" "fargate_task" {
  family                   = "${var.environment}-${var.service_name}"
  network_mode             = var.ecs_network_mode
  requires_compatibilities = var.ecs_requires_compatibilities
  cpu                      = var.task_cpu
  memory                   = var.task_memory
  execution_role_arn       = aws_iam_role.fargate_task_execution_role.arn
  task_role_arn            = aws_iam_role.fargate_task_role.arn
  container_definitions    = local.container_definition_file

  dynamic "ephemeral_storage" {
    for_each = var.task_ephemeral_storage_size > 20 ? [" "] : []
    content {
      size_in_gib = var.task_ephemeral_storage_size
    }
  }

  dynamic "volume" {
    for_each = var.docker_volumes
    content {
      name = volume.value["name"]

      configure_at_launch = volume.value.type == "EBS" ? true : null

      dynamic "efs_volume_configuration" {
        for_each = volume.value.type == "EFS" ? [volume] : []

        content {
          file_system_id = volume.value["file_system_id"]
          # When using access points, this must be set to "/"
          root_directory     = "/"
          transit_encryption = "ENABLED"
          authorization_config {
            access_point_id = volume.value["access_point_id"]
            iam             = "ENABLED"
          }
        }
      }
    }
  }

  tags = local.combined_resource_tags
}

# This service configuration applies to web services.
resource "aws_ecs_service" "fargate_service_with_load_balancer" {
  # checkov:skip=CKV_AWS_332:We use the "LATEST" platform version.

  count                              = var.task_type == "web_service" ? 1 : 0
  name                               = "${var.environment}-${var.service_name}"
  cluster                            = local.ecs_cluster_name
  task_definition                    = aws_ecs_task_definition.fargate_task.arn
  desired_count                      = var.desired_task_count
  deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent
  deployment_maximum_percent         = var.deployment_maximum_percent
  health_check_grace_period_seconds  = var.health_check_grace_period_seconds
  platform_version                   = local.platform_version
  propagate_tags                     = var.propagate_tags ? "SERVICE" : null
  availability_zone_rebalancing      = var.availability_zone_rebalancing
  launch_type                        = var.fargate_spot_capacity_provider_weight == 0 && var.fargate_capacity_provider_weight == 0 ? "FARGATE" : null
  force_new_deployment               = var.force_new_deployment

  dynamic "deployment_circuit_breaker" {
    for_each = var.deployment_circuit_breaker_enabled ? [1] : []
    content {
      enable   = true
      rollback = var.deployment_circuit_breaker_rollback_enabled
    }
  }

  dynamic "capacity_provider_strategy" {
    for_each = var.fargate_spot_capacity_provider_weight > 0 ? ["FARGATE_SPOT"] : []
    content {
      capacity_provider = capacity_provider_strategy.value
      base              = var.fargate_spot_capacity_provider_base
      weight            = var.fargate_spot_capacity_provider_weight
    }
  }

  dynamic "capacity_provider_strategy" {
    for_each = var.fargate_capacity_provider_weight > 0 ? ["FARGATE"] : []
    content {
      capacity_provider = capacity_provider_strategy.value
      base              = var.fargate_capacity_provider_base
      weight            = var.fargate_capacity_provider_weight
    }
  }

  dynamic "load_balancer" {
    for_each = var.additional_lb_target_group_arns
    content {
      target_group_arn = load_balancer.value
      container_name   = var.service_name
      container_port   = var.container_port
    }
  }

  dynamic "load_balancer" {
    for_each = var.additional_lb_target_groups
    content {
      container_name   = load_balancer.value.container_name
      container_port   = load_balancer.value.container_port
      target_group_arn = load_balancer.value.target_group_arn
    }
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.fargate_target_group[0].arn
    container_name   = var.service_name
    container_port   = var.container_port
  }

  network_configuration {
    security_groups = concat(var.custom_security_group_ids, [aws_security_group.task_security_group[0].id])
    subnets         = var.fargate_service_subnets
  }

  dynamic "volume_configuration" {
    for_each = { for idx, volume in var.docker_volumes : idx => volume if volume.type == "EBS" }

    content {
      name = volume_configuration.value.name
      managed_ebs_volume {
        role_arn         = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ecsInfrastructureRole"
        file_system_type = volume_configuration.value.file_system_type
        iops             = volume_configuration.value.iops
        size_in_gb       = volume_configuration.value.size_in_gb
        snapshot_id      = volume_configuration.value.snapshot_id
        throughput       = volume_configuration.value.throughput
        volume_type      = volume_configuration.value.volume_type
      }
    }
  }

  # Allow external changes (e.g. scaling, updates) without Terraform caring
  lifecycle {
    ignore_changes = [
      desired_count,
      task_definition,
    ]
  }

  depends_on = [aws_ecs_task_definition.fargate_task]

  tags = local.combined_resource_tags
}

# Target group for load-balanced services
resource "aws_lb_target_group" "fargate_target_group" {
  #checkov:skip=CKV_AWS_378:Ensure AWS Load Balancer doesn't use HTTP protocol
  count                = var.task_type == "web_service" ? 1 : 0
  name                 = "${var.environment}-${var.service_name}"
  port                 = var.container_port
  protocol             = var.container_protocol
  protocol_version     = var.container_protocol_version != "" ? var.container_protocol_version : null
  target_type          = "ip"
  deregistration_delay = var.target_deregistration_delay
  vpc_id               = var.vpc_id

  health_check {
    healthy_threshold   = var.health_check_healthy_threshold
    interval            = var.health_check_interval
    matcher             = var.health_check_matcher
    path                = var.health_check_path
    port                = var.container_port
    protocol            = var.container_protocol
    timeout             = var.health_check_timeout
    unhealthy_threshold = var.health_check_unhealthy_threshold
  }

  stickiness {
    type            = "lb_cookie"
    cookie_duration = var.target_group_cookie_duration
    enabled         = var.target_group_stickiness_enabled
  }

  tags = local.combined_resource_tags

  depends_on = [aws_lb.application_load_balancer]
}

data "aws_acm_certificate" "lb_ssl_certificate" {
  count       = var.task_type == "web_service" && var.https_listener_enabled && var.https_listener_certificate_id == "" ? 1 : 0
  domain      = local.certificate_domain
  statuses    = ["ISSUED"]
  most_recent = true
}

resource "aws_lb" "application_load_balancer" {
  # checkov:skip=CKV2_AWS_20:We do not use HTTP to HTTPS redirect, as this module supports HTTP and HTTPS endpoints independently.

  count                      = var.task_type == "web_service" ? 1 : 0
  name                       = "${var.environment}-${var.service_name}"
  internal                   = var.load_balancer_is_internal
  load_balancer_type         = "application"
  idle_timeout               = var.load_balancer_idle_timeout
  security_groups            = [aws_security_group.load_balancer_security_group[0].id]
  subnets                    = var.load_balancer_subnets
  enable_deletion_protection = var.load_balancer_enable_deletion_protection

  access_logs {
    bucket  = local.load_balancer_access_logs_s3_bucket_name
    prefix  = local.alb_access_logs_bucket_prefix
    enabled = true
  }

  tags = local.combined_resource_tags

  lifecycle {
    precondition {
      condition     = length("${var.environment}-${var.service_name}") <= 32
      error_message = "Load balancer name must be 32 characters or less"
    }
  }
}

resource "aws_lb_listener" "load_balancer_http_listener" {
  # checkov:skip=CKV_AWS_2:Although using a non-SSL load balancer is not recommended, this module supports this feature for edge case scenarios.
  # checkov:skip=CKV_AWS_103:SSL Policy can't be set for an HTTP listener
  count             = var.task_type == "web_service" ? var.http_listener_enabled ? 1 : 0 : 0
  load_balancer_arn = aws_lb.application_load_balancer[0].arn
  port              = var.http_listener_port
  protocol          = "HTTP"

  default_action {
    target_group_arn = aws_lb_target_group.fargate_target_group[0].arn
    type             = "forward"
  }

  depends_on = [aws_lb_target_group.fargate_target_group]

  tags = local.combined_resource_tags
}

resource "aws_lb_listener" "load_balancer_https_listener" {
  count             = var.task_type == "web_service" ? var.https_listener_enabled ? 1 : 0 : 0
  load_balancer_arn = aws_lb.application_load_balancer[0].arn
  port              = var.https_listener_port
  protocol          = "HTTPS"
  ssl_policy        = var.https_listener_ssl_policy
  certificate_arn   = local.certificate_arn

  default_action {
    target_group_arn = aws_lb_target_group.fargate_target_group[0].arn
    type             = "forward"
  }

  depends_on = [aws_lb_target_group.fargate_target_group]

  tags = local.combined_resource_tags
}

# Create a service for daemon and worker tasks
resource "aws_ecs_service" "fargate_service_without_load_balancer" {
  # checkov:skip=CKV_AWS_332:We use the "LATEST" platform version.

  count                              = var.task_type == "worker" || var.task_type == "external_worker" ? 1 : 0
  name                               = "${var.environment}-${var.service_name}"
  cluster                            = local.ecs_cluster_name
  task_definition                    = aws_ecs_task_definition.fargate_task.arn
  desired_count                      = var.desired_task_count
  deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent
  deployment_maximum_percent         = var.deployment_maximum_percent
  platform_version                   = local.platform_version
  propagate_tags                     = var.propagate_tags ? "SERVICE" : null
  availability_zone_rebalancing      = var.availability_zone_rebalancing
  launch_type                        = var.fargate_spot_capacity_provider_weight == 0 && var.fargate_capacity_provider_weight == 0 ? "FARGATE" : null
  force_new_deployment               = var.force_new_deployment

  dynamic "deployment_circuit_breaker" {
    for_each = var.deployment_circuit_breaker_enabled ? [1] : []
    content {
      enable   = true
      rollback = var.deployment_circuit_breaker_rollback_enabled
    }
  }

  dynamic "capacity_provider_strategy" {
    for_each = var.fargate_spot_capacity_provider_weight > 0 ? ["FARGATE_SPOT"] : []
    content {
      capacity_provider = capacity_provider_strategy.value
      base              = var.fargate_spot_capacity_provider_base
      weight            = var.fargate_spot_capacity_provider_weight
    }
  }

  dynamic "capacity_provider_strategy" {
    for_each = var.fargate_capacity_provider_weight > 0 ? ["FARGATE"] : []
    content {
      capacity_provider = capacity_provider_strategy.value
      base              = var.fargate_capacity_provider_base
      weight            = var.fargate_capacity_provider_weight
    }
  }

  dynamic "network_configuration" {
    for_each = var.task_type == "external_worker" ? [] : [1]
    content {
      security_groups = concat(var.custom_security_group_ids, [aws_security_group.task_security_group[0].id])
      subnets         = var.fargate_service_subnets
    }
  }

  dynamic "ordered_placement_strategy" {
    for_each = var.task_type == "external_worker" ? [1] : []
    content {
      type  = var.ordered_placement_strategy_type
      field = var.ordered_placement_strategy_type == "random" ? null : var.ordered_placement_strategy_field
    }
  }

  # Allow external changes (e.g. scaling, updates) without Terraform caring
  lifecycle {
    ignore_changes = [
      desired_count,
      task_definition,
    ]
  }

  dynamic "load_balancer" {
    for_each = var.additional_lb_target_group_arns
    content {
      target_group_arn = load_balancer.value
      container_name   = var.service_name
      container_port   = var.container_port
    }
  }

  dynamic "load_balancer" {
    for_each = var.additional_lb_target_groups
    content {
      container_name   = load_balancer.value.container_name
      container_port   = load_balancer.value.container_port
      target_group_arn = load_balancer.value.target_group_arn
    }
  }

  dynamic "volume_configuration" {
    for_each = { for idx, volume in var.docker_volumes : idx => volume if volume.type == "EBS" }

    content {
      name = volume_configuration.value.name
      managed_ebs_volume {
        role_arn         = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ecsInfrastructureRole"
        file_system_type = volume_configuration.value.file_system_type
        iops             = volume_configuration.value.iops
        size_in_gb       = volume_configuration.value.size_in_gb
        snapshot_id      = volume_configuration.value.snapshot_id
        throughput       = volume_configuration.value.throughput
        volume_type      = volume_configuration.value.volume_type
      }
    }
  }

  depends_on = [aws_ecs_task_definition.fargate_task]

  tags = local.combined_resource_tags
}

# Configure metric-based autoscaling
resource "aws_appautoscaling_target" "scaling_target" {
  count              = local.autoscaling_target_enabled ? 1 : 0
  service_namespace  = "ecs"
  resource_id        = "service/${local.ecs_cluster_name}/${var.environment}-${var.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"
  role_arn           = data.aws_iam_role.fargate_autoscaling_role.arn
  min_capacity       = var.minimum_capacity
  max_capacity       = var.maximum_capacity

  depends_on = [
    aws_ecs_service.fargate_service_with_load_balancer,
    aws_ecs_service.fargate_service_without_load_balancer,
  ]
}

resource "aws_appautoscaling_policy" "cpu_scaling_policy" {
  count              = var.autoscaling_cpu_policy_enabled ? 1 : 0
  name               = "${var.environment}-${var.service_name}-cpu-scaling-policy"
  policy_type        = "TargetTrackingScaling"
  service_namespace  = "ecs"
  resource_id        = "service/${local.ecs_cluster_name}/${var.environment}-${var.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"

  target_tracking_scaling_policy_configuration {
    target_value       = var.scaling_cpu_target_value
    scale_in_cooldown  = var.scale_in_cooldown_period
    scale_out_cooldown = var.scale_out_cooldown_period

    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
  }

  depends_on = [aws_appautoscaling_target.scaling_target]
}

resource "aws_appautoscaling_policy" "memory_scaling_policy" {
  count              = var.autoscaling_memory_policy_enabled ? 1 : 0
  name               = "${var.environment}-${var.service_name}-memory-scaling-policy"
  policy_type        = "TargetTrackingScaling"
  service_namespace  = "ecs"
  resource_id        = "service/${local.ecs_cluster_name}/${var.environment}-${var.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"

  target_tracking_scaling_policy_configuration {
    target_value       = var.scaling_memory_target_value
    scale_in_cooldown  = var.scale_in_cooldown_period
    scale_out_cooldown = var.scale_out_cooldown_period

    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageMemoryUtilization"
    }
  }

  depends_on = [aws_appautoscaling_target.scaling_target]
}

resource "aws_appautoscaling_policy" "load_balancer_scaling_policy" {
  count              = local.autoscaling_load_balancer_policy_enabled ? 1 : 0
  name               = "${var.environment}-${var.service_name}-load-balancer-scaling-policy"
  policy_type        = "TargetTrackingScaling"
  service_namespace  = "ecs"
  resource_id        = "service/${aws_ecs_cluster.fargate_ecs_cluster[0].name}/${var.environment}-${var.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"

  target_tracking_scaling_policy_configuration {
    target_value       = var.scaling_load_balancer_target_value
    scale_in_cooldown  = var.scale_in_cooldown_period
    scale_out_cooldown = var.scale_out_cooldown_period

    predefined_metric_specification {
      predefined_metric_type = "ALBRequestCountPerTarget"
      resource_label = join("/", [
        aws_lb.application_load_balancer[0].arn_suffix,
        aws_lb_target_group.fargate_target_group[0].arn_suffix,
      ])
    }
  }

  depends_on = [aws_appautoscaling_target.scaling_target]
}

# Scale out while connections per task are greater than configured threshold
resource "aws_cloudwatch_metric_alarm" "active_connection_count_per_task_scale_out_alarm" {
  count             = var.autoscaling_active_connections_policy_enabled ? 1 : 0
  alarm_name        = "${var.environment}-${var.service_name}-active-connection-count-scale-out-alarm"
  alarm_description = "${var.environment}-${var.service_name}-active-connection-count-scale-out-alarm"

  comparison_operator = "GreaterThanThreshold"
  threshold           = var.scaling_active_connection_count_scale_out_threshold
  evaluation_periods  = var.scaling_active_connection_count_scale_out_number_of_evaluation_periods

  metric_query {
    id          = "active_connections_per_task"
    expression  = "active_connections / running_tasks"
    return_data = true
  }

  metric_query {
    id = "active_connections"

    metric {
      namespace   = "AWS/ApplicationELB"
      metric_name = "ActiveConnectionCount"
      period      = var.scaling_active_connection_count_evaluation_period_seconds
      stat        = "Sum"
      unit        = "Count"

      dimensions = {
        LoadBalancer = aws_lb.application_load_balancer[0].arn_suffix
      }
    }
  }

  metric_query {
    id = "running_tasks"

    metric {
      namespace   = "ECS/ContainerInsights"
      metric_name = "RunningTaskCount"
      period      = var.scaling_active_connection_count_evaluation_period_seconds
      stat        = "Maximum"
      unit        = "Count"

      dimensions = {
        ServiceName = aws_ecs_service.fargate_service_with_load_balancer[0].name
        ClusterName = aws_ecs_cluster.fargate_ecs_cluster[0].name
      }
    }
  }

  alarm_actions             = [aws_appautoscaling_policy.active_connection_count_per_task_scale_out_policy[0].arn]
  insufficient_data_actions = []
}

# Scale in while connections per task are less than configured threshold
resource "aws_cloudwatch_metric_alarm" "active_connection_count_per_task_scale_in_alarm" {
  count             = var.autoscaling_active_connections_policy_enabled ? 1 : 0
  alarm_name        = "${var.environment}-${var.service_name}-active-connection-count-scale-in-alarm"
  alarm_description = "${var.environment}-${var.service_name}-active-connection-count-scale-in-alarm"

  comparison_operator = "LessThanThreshold"
  threshold           = var.scaling_active_connection_count_scale_in_threshold
  evaluation_periods  = var.scaling_active_connection_count_scale_in_number_of_evaluation_periods

  metric_query {
    id          = "active_connections_per_task"
    expression  = "active_connections / running_tasks"
    return_data = true
  }

  metric_query {
    id = "active_connections"

    metric {
      namespace   = "AWS/ApplicationELB"
      metric_name = "ActiveConnectionCount"
      period      = var.scaling_active_connection_count_evaluation_period_seconds
      stat        = "Sum"
      unit        = "Count"

      dimensions = {
        LoadBalancer = aws_lb.application_load_balancer[0].arn_suffix
      }
    }
  }

  metric_query {
    id = "running_tasks"

    metric {
      namespace   = "ECS/ContainerInsights"
      metric_name = "RunningTaskCount"
      period      = var.scaling_active_connection_count_evaluation_period_seconds
      stat        = "Maximum"
      unit        = "Count"

      dimensions = {
        ServiceName = aws_ecs_service.fargate_service_with_load_balancer[0].name
        ClusterName = aws_ecs_cluster.fargate_ecs_cluster[0].name
      }
    }
  }

  alarm_actions             = [aws_appautoscaling_policy.active_connection_count_per_task_scale_in_policy[0].arn]
  insufficient_data_actions = []
}

resource "aws_appautoscaling_policy" "active_connection_count_per_task_scale_out_policy" {
  count              = var.autoscaling_active_connections_policy_enabled ? 1 : 0
  name               = "${var.environment}-${var.service_name}-active-connections-scale-out-policy"
  policy_type        = "StepScaling"
  service_namespace  = "ecs"
  resource_id        = aws_appautoscaling_target.scaling_target[0].resource_id
  scalable_dimension = aws_appautoscaling_target.scaling_target[0].scalable_dimension

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = var.scale_out_cooldown_period
    metric_aggregation_type = "Maximum"

    step_adjustment {
      metric_interval_lower_bound = 0
      scaling_adjustment          = var.active_connections_autoscaling_scale_out_adjustment
    }
  }
}

resource "aws_appautoscaling_policy" "active_connection_count_per_task_scale_in_policy" {
  count              = var.autoscaling_active_connections_policy_enabled ? 1 : 0
  name               = "${var.environment}-${var.service_name}-active-connections-scale-in-policy"
  policy_type        = "StepScaling"
  service_namespace  = "ecs"
  resource_id        = aws_appautoscaling_target.scaling_target[0].resource_id
  scalable_dimension = aws_appautoscaling_target.scaling_target[0].scalable_dimension

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = var.scale_in_cooldown_period
    metric_aggregation_type = "Maximum"

    step_adjustment {
      metric_interval_upper_bound = 0
      scaling_adjustment          = var.active_connections_autoscaling_scale_in_adjustment
    }
  }
}

# Create a record for nonproduction environments (e.g. qa-ows-search). Only create if environment is not "prod"
resource "aws_route53_record" "service_route53_record" {
  provider = aws.dns
  count    = var.route53_record_creation_enabled ? var.task_type == "web_service" ? var.environment != "prod" ? 1 : 0 : 0 : 0

  name    = "${var.environment}-${var.service_name}"
  zone_id = local.route53_zone_id
  type    = "CNAME"
  ttl     = var.route53_record_ttl

  records = [aws_lb.application_load_balancer[0].dns_name]
}

# Create a record for production environments (e.g. ows-search). Only create if environment is "prod"
resource "aws_route53_record" "prod_service_route53_record" {
  provider = aws.dns
  count    = var.route53_record_creation_enabled ? var.task_type == "web_service" ? var.environment == "prod" ? 1 : 0 : 0 : 0

  name    = var.service_name
  zone_id = local.route53_zone_id
  type    = "CNAME"
  ttl     = var.route53_record_ttl

  records = [aws_lb.application_load_balancer[0].dns_name]
}

# Create a cname record of prod-ows-service to ows-service. Only create if environment is "prod"
resource "aws_route53_record" "prod_cname_service_route53_record" {
  provider = aws.dns
  count    = var.route53_record_creation_enabled ? var.task_type == "web_service" ? var.environment == "prod" ? 1 : 0 : 0 : 0

  name    = "${var.environment}-${var.service_name}"
  zone_id = local.route53_zone_id
  type    = "CNAME"
  ttl     = var.route53_record_ttl

  records = [aws_route53_record.prod_service_route53_record[0].fqdn]
}

# Attach the loadbalancer to the WAF if appropriate
resource "aws_wafv2_web_acl_association" "fargate_service_waf_association" {
  count        = var.task_type == "web_service" ? 1 : 0
  resource_arn = aws_lb.application_load_balancer[0].arn
  web_acl_arn  = var.custom_waf_arn != "" ? var.custom_waf_arn : data.aws_wafv2_web_acl.default_waf[0].arn
}

data "aws_iam_policy_document" "task_protection_policy_document" {
  statement {
    actions = [
      "ecs:GetTaskProtection",
      "ecs:UpdateTaskProtection",
    ]

    resources = [
      "arn:aws:ecs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:task/${local.ecs_cluster_name}/*"
    ]
  }
}

resource "aws_iam_role_policy" "task_protection_policy" {
  count  = var.task_protection_policy_enabled ? 1 : 0
  name   = "${var.environment}-${var.service_name}-task-protection-policy"
  role   = aws_iam_role.fargate_task_role.name
  policy = data.aws_iam_policy_document.task_protection_policy_document.json
}

resource "aws_ecs_cluster_capacity_providers" "fargate_ecs_cluster_capacity_providers" {
  count = var.ecs_cluster_name != "" ? 0 : 1

  cluster_name = local.ecs_cluster_name

  capacity_providers = var.fargate_spot_capacity_provider_weight > 0 || var.fargate_capacity_provider_weight > 0 ? ["FARGATE", "FARGATE_SPOT"] : ["FARGATE"]

  default_capacity_provider_strategy {
    base              = 1
    weight            = 100
    capacity_provider = "FARGATE"
  }
}

resource "aws_security_group_rule" "allow_additional_http_port_to_load_balancer" {
  for_each          = { for each in var.additional_http_ports : each.http_listener_port => each }
  type              = "ingress"
  from_port         = each.value.http_listener_port
  to_port           = each.value.http_listener_port
  protocol          = upper(each.value.protocol)
  cidr_blocks       = local.http_listener_allow_cidr_blocks
  prefix_list_ids   = [for prefix_list in data.aws_ec2_managed_prefix_list.http_listener_allow_prefix_lists : prefix_list.id]
  security_group_id = aws_security_group.load_balancer_security_group[0].id

  lifecycle {
    precondition {
      condition     = var.task_type == "web_service" && var.http_listener_enabled
      error_message = "Additional HTTP ports can only be provided if task type is web_service and HTTP listener is enabled"
    }
  }
}

resource "aws_security_group_rule" "allow_additional_http_port_from_load_balancer_to_container" {
  for_each                 = { for each in var.additional_http_ports : each.http_listener_port => each }
  type                     = "ingress"
  from_port                = each.value.http_listener_port
  to_port                  = each.value.container_port
  protocol                 = upper(each.value.protocol)
  source_security_group_id = aws_security_group.load_balancer_security_group[0].id
  security_group_id        = aws_security_group.task_security_group[0].id

  lifecycle {
    precondition {
      condition     = var.task_type == "web_service" && var.http_listener_enabled
      error_message = "Additional HTTP ports can only be provided if task type is web_service and HTTP listener is enabled"
    }
  }
}


resource "aws_security_group_rule" "allow_additional_https_port_to_load_balancer" {
  for_each          = { for each in var.additional_https_ports : each.https_listener_port => each }
  type              = "ingress"
  from_port         = each.value.https_listener_port
  to_port           = each.value.https_listener_port
  protocol          = upper(each.value.protocol)
  cidr_blocks       = local.https_listener_allow_cidr_blocks
  prefix_list_ids   = [for prefix_list in data.aws_ec2_managed_prefix_list.https_listener_allow_prefix_lists : prefix_list.id]
  security_group_id = aws_security_group.load_balancer_security_group[0].id

  lifecycle {
    precondition {
      condition     = var.task_type == "web_service" && var.https_listener_enabled
      error_message = "Additional HTTPS ports can only be provided if task type is web_service and HTTPS listener is enabled"
    }
  }
}

resource "aws_security_group_rule" "allow_additional_https_port_from_load_balancer_to_container" {
  for_each                 = { for each in var.additional_https_ports : each.https_listener_port => each }
  type                     = "ingress"
  from_port                = each.value.https_listener_port
  to_port                  = each.value.container_port
  protocol                 = upper(each.value.protocol)
  source_security_group_id = aws_security_group.load_balancer_security_group[0].id
  security_group_id        = aws_security_group.task_security_group[0].id

  lifecycle {
    precondition {
      condition     = var.task_type == "web_service" && var.https_listener_enabled
      error_message = "Additional HTTPS ports can only be provided if task type is web_service and HTTPS listener is enabled"
    }
  }
}
