################
# ECS EC2 — GPU Search Service Infrastructure
################
# The search service runs on ECS EC2 with g4dn.xlarge (NVIDIA T4 GPU) instead
# of Fargate because Fargate does not support GPU workloads. A dedicated ECS
# cluster with a managed capacity provider handles instance lifecycle.
#
# All resources are gated by is_ephemeral — only deployed in the main environment.
# Ephemeral PR instances skip the search service entirely; the server falls back
# to keyword search automatically (SEARCH_URL is empty).
#
# Requires: NAT Gateway or VPC Endpoints (ECR, S3, Secrets Manager, CloudWatch
# Logs, ECS) in the private subnets for outbound connectivity.

################
# AMI — ECS-optimized with NVIDIA GPU drivers
################

data "aws_ssm_parameter" "ecs_gpu_ami" {
  count = local.is_ephemeral ? 0 : 1
  name  = "/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended/image_id"
}

################
# Secrets — ARN construction for task definition valueFrom
################
# Secrets are created by the ows_coda_search_secrets module in search.tf.
# We construct ARNs directly instead of using data sources to avoid a
# chicken-and-egg failure on first apply (data source reads before module creates).

locals {
  search_secret_arn_prefix   = "arn:aws:secretsmanager:${var.region}:${data.aws_caller_identity.current.account_id}:secret:${var.environment}/${local.search_service_name}"
  shared_services_account_id = "086679231553"
  datadog_agent_image        = "${local.shared_services_account_id}.dkr.ecr.${var.region}.amazonaws.com/orchard-datadog-agent:latest"
}

################
# Datadog — API key secret (shared across all services in the environment)
################

data "aws_secretsmanager_secret" "datadog_api_key" {
  count = local.is_ephemeral ? 0 : 1
  name  = "${var.environment}/datadog/DD_API_KEY"
}

################
# DNS — Route 53 record for internal ALB
################

data "aws_route53_zone" "theorchard_io" {
  count    = local.is_ephemeral ? 0 : 1
  provider = aws.networking
  name     = "theorchard.io."
}

resource "aws_route53_record" "search" {
  count    = local.is_ephemeral ? 0 : 1
  provider = aws.networking
  zone_id  = data.aws_route53_zone.theorchard_io[0].zone_id
  name     = "${var.environment}-${local.search_service_name}.theorchard.io"
  type     = "A"

  alias {
    name                   = aws_lb.search[0].dns_name
    zone_id                = aws_lb.search[0].zone_id
    evaluate_target_health = true
  }
}

################
# ECS Cluster + Capacity Provider
################

resource "aws_ecs_cluster" "search" {
  count = local.is_ephemeral ? 0 : 1
  name  = "${var.environment}-${local.search_service_name}"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

resource "aws_ecs_cluster_capacity_providers" "search" {
  count              = local.is_ephemeral ? 0 : 1
  cluster_name       = aws_ecs_cluster.search[0].name
  capacity_providers = [aws_ecs_capacity_provider.search_gpu[0].name]

  default_capacity_provider_strategy {
    base              = 1
    weight            = 100
    capacity_provider = aws_ecs_capacity_provider.search_gpu[0].name
  }
}

resource "aws_ecs_capacity_provider" "search_gpu" {
  count = local.is_ephemeral ? 0 : 1
  name  = "${var.environment}-${local.search_service_name}-gpu"

  auto_scaling_group_provider {
    auto_scaling_group_arn         = aws_autoscaling_group.search_gpu[0].arn
    managed_termination_protection = "ENABLED"

    managed_scaling {
      status                    = "ENABLED"
      target_capacity           = 100
      minimum_scaling_step_size = 1
      maximum_scaling_step_size = 1
    }
  }
}

################
# Auto Scaling Group + Launch Template
################

# Future considerations for production:
# - Spot instances: mixed_instances_policy with capacity-optimized allocation
#   could save 60-70% on g4dn.xlarge. Viable when desired_count >= 2 so spot
#   interruptions don't cause full outage (keyword fallback covers the gap).
# - GPU monitoring: Container Insights captures basic GPU metrics. For detailed
#   utilization (VRAM, compute %), add a Datadog agent sidecar or expose a
#   /metrics endpoint with nvidia-smi stats.
resource "aws_autoscaling_group" "search_gpu" {
  count                 = local.is_ephemeral ? 0 : 1
  name                  = "${var.environment}-${local.search_service_name}-gpu"
  vpc_zone_identifier   = module.vpc_info.default_private_subnet_ids
  min_size              = 1
  max_size              = 2
  desired_capacity      = 1
  protect_from_scale_in = true

  launch_template {
    id      = aws_launch_template.search_gpu[0].id
    version = "$Latest"
  }

  instance_refresh {
    strategy = "Rolling"
    preferences {
      min_healthy_percentage = 100
    }
  }

  tag {
    key                 = "AmazonECSManaged"
    value               = "true"
    propagate_at_launch = true
  }

  lifecycle {
    ignore_changes = [desired_capacity]
  }
}

resource "aws_launch_template" "search_gpu" {
  count       = local.is_ephemeral ? 0 : 1
  name_prefix = "${var.environment}-${local.search_service_name}-gpu-"
  # AMI is fetched dynamically from the SSM recommended parameter. For production,
  # consider pinning to a tested AMI ID to avoid surprise upgrades. The
  # instance_refresh block on the ASG ensures safe rollout when the AMI changes.
  image_id      = data.aws_ssm_parameter.ecs_gpu_ami[0].value
  instance_type = "g4dn.xlarge"

  iam_instance_profile {
    arn = aws_iam_instance_profile.search_ecs[0].arn
  }

  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"
    http_put_response_hop_limit = 1 # Tasks use task IAM roles, not instance profile — no container metadata access needed
  }

  network_interfaces {
    associate_public_ip_address = false
    security_groups             = [aws_security_group.search_ec2[0].id]
  }

  user_data = base64encode(<<-EOF
    #!/bin/bash
    set -euxo pipefail
    exec > >(tee /var/log/user-data.log | logger -t user-data) 2>&1

    echo ECS_CLUSTER=${aws_ecs_cluster.search[0].name} >> /etc/ecs/ecs.config
    echo ECS_ENABLE_GPU_SUPPORT=true >> /etc/ecs/ecs.config

    # Verify ECS agent registers with the cluster before declaring success
    timeout 300 bash -c 'until curl -sf http://localhost:51678/v1/metadata | grep -q ContainerInstanceArn; do echo "Waiting for ECS agent..."; sleep 5; done' \
      || { echo "FATAL: ECS agent failed to register within 300s"; exit 1; }
    echo "ECS agent registered successfully"
  EOF
  )

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name               = "${var.environment}-${local.search_service_name}-gpu"
      environment        = var.environment
      application_family = var.application_family
      service_name       = local.search_service_name
      team_name          = var.team_name
    }
  }
}

################
# Security Groups
################

# Search ALB SG — inline egress kept to avoid import issues during migration.
# Ingress is standalone (server + VPC Link from api-gateway.tf).
resource "aws_security_group" "search_alb" {
  count       = local.is_ephemeral ? 0 : 1
  name        = "${var.environment}-${local.search_service_name}-alb"
  description = "Internal ALB for ${local.search_service_name}"
  vpc_id      = module.vpc_info.vpc_id

  egress {
    description = "Outbound: health checks to target group"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Server Fargate tasks call search directly within the VPC.
resource "aws_security_group_rule" "search_alb_from_server" {
  count                    = local.is_ephemeral ? 0 : 1
  type                     = "ingress"
  from_port                = 443
  to_port                  = 443
  protocol                 = "tcp"
  source_security_group_id = module.ows_coda_fargate_environment.fargate_security_group_id
  security_group_id        = aws_security_group.search_alb[0].id
  description              = "HTTPS from server Fargate tasks"
}

# API Gateway VPC Link ingress is defined in api-gateway.tf (vpc_link_to_search_alb).

# EC2 instance SG — ECS agent, Docker daemon, outbound connectivity.
# Task ENIs use a separate SG (search_task) below.
resource "aws_security_group" "search_ec2" {
  count       = local.is_ephemeral ? 0 : 1
  name        = "${var.environment}-${local.search_service_name}-ec2"
  description = "Search GPU EC2 instances - ECS agent and Docker daemon"
  vpc_id      = module.vpc_info.vpc_id

  egress {
    description = "Outbound: ECR, Secrets Manager, CloudWatch Logs, S3, Snowflake, GraphQL"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Task SG — applied to the task ENI (awsvpc mode). Separate from the EC2
# instance SG so that changes to instance-level networking (e.g. SSH for
# debugging) don't accidentally affect task connectivity.
resource "aws_security_group" "search_task" {
  count       = local.is_ephemeral ? 0 : 1
  name        = "${var.environment}-${local.search_service_name}-task"
  description = "Search ECS tasks - ConnectRPC on port 8081"
  vpc_id      = module.vpc_info.vpc_id

  ingress {
    description     = "Traffic from internal ALB"
    from_port       = 8081
    to_port         = 8081
    protocol        = "tcp"
    security_groups = [aws_security_group.search_alb[0].id]
  }

  egress {
    description = "Outbound: ECR, Secrets Manager, CloudWatch Logs, S3, Snowflake, GraphQL"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

################
# Load Balancer — Internal ALB for search service
#
# Previously public (external consumers hit it directly). Now internal —
# external access goes through the API Gateway, internal access (server →
# search) stays direct within the VPC.
################

resource "aws_lb" "search" {
  count              = local.is_ephemeral ? 0 : 1
  name               = "${var.environment}-${local.search_service_name}"
  internal           = true
  load_balancer_type = "application"
  subnets            = module.vpc_info.default_private_subnet_ids
  security_groups    = [aws_security_group.search_alb[0].id]

  access_logs {
    bucket  = local.elb_logs_bucket
    prefix  = local.search_service_name
    enabled = true
  }
}

resource "aws_lb_target_group" "search" {
  count                = local.is_ephemeral ? 0 : 1
  name                 = "${var.environment}-${local.search_service_name}"
  port                 = 8081
  protocol             = "HTTP"
  vpc_id               = module.vpc_info.vpc_id
  target_type          = "ip"
  deregistration_delay = 60

  health_check {
    path                = "/health"
    port                = "traffic-port"
    healthy_threshold   = 2
    unhealthy_threshold = 5
    timeout             = 5
    interval            = 10
    matcher             = "200"
  }
}

# TLS 1.3 policy — required even for internal ALBs (VPC Link traffic is HTTPS).
# The main Fargate ALB's TLS policy is managed by the terraform-fargate module;
# if the org standardises on a different policy, update both here and in the module.
resource "aws_lb_listener" "search_https" {
  count             = local.is_ephemeral ? 0 : 1
  load_balancer_arn = aws_lb.search[0].arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = data.aws_acm_certificate.theorchard_io.arn

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

################
# CloudWatch Log Group
################

resource "aws_cloudwatch_log_group" "search" {
  count             = local.is_ephemeral ? 0 : 1
  name              = "/ecs/${var.environment}-${local.search_service_name}"
  retention_in_days = 365
}

################
# CloudWatch Alarms — active alerting for search service health
################

resource "aws_sns_topic" "search_alerts" {
  count             = local.is_ephemeral ? 0 : 1
  name              = "${var.environment}-${local.search_service_name}-alerts"
  kms_master_key_id = "alias/aws/sns"
}

resource "aws_cloudwatch_metric_alarm" "search_no_running_tasks" {
  count               = local.is_ephemeral ? 0 : 1
  alarm_name          = "${var.environment}-${local.search_service_name}-no-running-tasks"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 2
  metric_name         = "RunningTaskCount"
  namespace           = "ECS/ContainerInsights"
  period              = 60
  statistic           = "Minimum"
  threshold           = 1
  alarm_description   = "Search ECS service has 0 running tasks — service is down"
  alarm_actions       = [aws_sns_topic.search_alerts[0].arn]
  ok_actions          = [aws_sns_topic.search_alerts[0].arn]

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

resource "aws_cloudwatch_metric_alarm" "search_no_healthy_targets" {
  count               = local.is_ephemeral ? 0 : 1
  alarm_name          = "${var.environment}-${local.search_service_name}-no-healthy-targets"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 2
  metric_name         = "HealthyHostCount"
  namespace           = "AWS/ApplicationELB"
  period              = 60
  statistic           = "Minimum"
  threshold           = 1
  alarm_description   = "Search ALB has 0 healthy targets — health checks failing"
  alarm_actions       = [aws_sns_topic.search_alerts[0].arn]
  ok_actions          = [aws_sns_topic.search_alerts[0].arn]

  dimensions = {
    TargetGroup  = aws_lb_target_group.search[0].arn_suffix
    LoadBalancer = aws_lb.search[0].arn_suffix
  }
}

resource "aws_cloudwatch_metric_alarm" "search_asg_no_instances" {
  count               = local.is_ephemeral ? 0 : 1
  alarm_name          = "${var.environment}-${local.search_service_name}-asg-no-instances"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 2
  metric_name         = "GroupInServiceInstances"
  namespace           = "AWS/AutoScaling"
  period              = 60
  statistic           = "Minimum"
  threshold           = 1
  alarm_description   = "Search ASG has 0 in-service instances — g4dn.xlarge capacity may be exhausted"
  alarm_actions       = [aws_sns_topic.search_alerts[0].arn]
  ok_actions          = [aws_sns_topic.search_alerts[0].arn]

  dimensions = {
    AutoScalingGroupName = aws_autoscaling_group.search_gpu[0].name
  }
}

# SNS routing to #coda-monitoring is configured in alerting.tf (AWS Chatbot).

################
# IAM — EC2 Instance Profile (ECS container agent)
################

data "aws_iam_policy_document" "ecs_instance_assume" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "search_ecs_instance" {
  count              = local.is_ephemeral ? 0 : 1
  name               = "${var.environment}-${local.search_service_name}-ecs-instance"
  assume_role_policy = data.aws_iam_policy_document.ecs_instance_assume.json
}

resource "aws_iam_role_policy_attachment" "search_ecs_instance_ecs" {
  count      = local.is_ephemeral ? 0 : 1
  role       = aws_iam_role.search_ecs_instance[0].name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}

resource "aws_iam_role_policy_attachment" "search_ecs_instance_ssm" {
  count      = local.is_ephemeral ? 0 : 1
  role       = aws_iam_role.search_ecs_instance[0].name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "search_ecs" {
  count = local.is_ephemeral ? 0 : 1
  name  = "${var.environment}-${local.search_service_name}-ecs-instance"
  role  = aws_iam_role.search_ecs_instance[0].name
}

################
# IAM — ECS Task Execution Role (pull images + inject secrets)
################

data "aws_iam_policy_document" "ecs_task_assume" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "search_task_execution" {
  count              = local.is_ephemeral ? 0 : 1
  name               = "${var.environment}-${local.search_service_name}-task-execution"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_assume.json
}

resource "aws_iam_role_policy_attachment" "search_task_execution_ecs" {
  count      = local.is_ephemeral ? 0 : 1
  role       = aws_iam_role.search_task_execution[0].name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "search_task_execution_secrets" {
  count = local.is_ephemeral ? 0 : 1

  statement {
    effect = "Allow"
    actions = [
      "secretsmanager:GetSecretValue",
    ]
    resources = [
      "arn:aws:secretsmanager:${var.region}:${data.aws_caller_identity.current.account_id}:secret:${var.environment}/${local.search_service_name}/*",
      "arn:aws:secretsmanager:${var.region}:${data.aws_caller_identity.current.account_id}:secret:${var.environment}/datadog/*"
    ]
  }
}

resource "aws_iam_role_policy" "search_task_execution_secrets" {
  count  = local.is_ephemeral ? 0 : 1
  name   = "${var.environment}-${local.search_service_name}-execution-secrets"
  role   = aws_iam_role.search_task_execution[0].name
  policy = data.aws_iam_policy_document.search_task_execution_secrets[0].json
}

################
# IAM — ECS Task Role + S3 snapshot access
################

resource "aws_iam_role" "search_task" {
  count              = local.is_ephemeral ? 0 : 1
  name               = "${var.environment}-${local.search_service_name}-task"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_assume.json
}

data "aws_iam_policy_document" "search_snapshots" {
  count = local.is_ephemeral ? 0 : 1

  statement {
    effect    = "Allow"
    actions   = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
    resources = ["${module.s3_ows_coda_search_snapshots[0].s3_bucket_arn_output}/*"]
  }

  statement {
    effect    = "Allow"
    actions   = ["s3:GetBucketLocation", "s3:ListBucket"]
    resources = [module.s3_ows_coda_search_snapshots[0].s3_bucket_arn_output]
  }

  # SSE-KMS: task role needs key access to encrypt/decrypt snapshot objects.
  statement {
    effect    = "Allow"
    actions   = ["kms:GenerateDataKey", "kms:Decrypt"]
    resources = [data.aws_kms_alias.s3.target_key_arn]
  }
}

resource "aws_iam_policy" "search_snapshots" {
  count = local.is_ephemeral ? 0 : 1

  name        = "${var.environment}-${local.search_service_name}-snapshots-policy"
  description = "Allow ${local.search_service_name} ECS task to read/write S3 index snapshots"
  policy      = data.aws_iam_policy_document.search_snapshots[0].json
}

resource "aws_iam_role_policy_attachment" "search_snapshots" {
  count = local.is_ephemeral ? 0 : 1

  role       = aws_iam_role.search_task[0].name
  policy_arn = aws_iam_policy.search_snapshots[0].arn
}

################
# ECS Task Definition
################

# checkov:skip=CKV_AWS_249: App container needs writable root for ONNX runtime model optimisation at startup (HF cache dir)
resource "aws_ecs_task_definition" "search" {
  count                    = local.is_ephemeral ? 0 : 1
  family                   = "${var.environment}-${local.search_service_name}"
  requires_compatibilities = ["EC2"]
  network_mode             = "awsvpc"
  cpu                      = "4096"
  memory                   = "14336"
  execution_role_arn       = aws_iam_role.search_task_execution[0].arn
  task_role_arn            = aws_iam_role.search_task[0].arn

  container_definitions = jsonencode([
    # ── Application container ──
    {
      name      = local.search_service_name
      image     = "${data.aws_caller_identity.current.account_id}.dkr.ecr.${var.region}.amazonaws.com/${local.search_service_name}:latest"
      essential = true
      # readonlyRootFilesystem is intentionally omitted. The HuggingFace
      # transformers library writes to the model cache dir at runtime (ONNX
      # graph optimisation). Both the embedding and reranker models are
      # pre-cached in the Docker image, but the ONNX runtime still needs
      # write access for session init. Write access is limited to dirs
      # chown'd to the node user in the Dockerfile — the rest of the image
      # is owned by root and unwritable.

      dependsOn = [{
        containerName = "datadog-agent"
        condition     = "START"
      }]

      portMappings = [{
        containerPort = 8081
        protocol      = "tcp"
      }]

      resourceRequirements = [{
        type  = "GPU"
        value = "1"
      }]

      environment = [
        { name = "AWS_REGION", value = var.region },
        { name = "DD_LOGS_INJECTION", value = "true" },
        { name = "DD_SERVICE", value = local.search_service_name },
        { name = "DD_ENV", value = var.environment },
        { name = "DD_TRACE_AGENT_HOSTNAME", value = "localhost" },
        { name = "ENVIRONMENT", value = var.environment },
        { name = "GRAPHQL_GATEWAY_URL", value = local.graphql_gateway_url },
        { name = "HF_CACHE_DIR", value = "/var/app/.hf-cache" },
        { name = "NODE_ENV", value = var.environment },
        { name = "PORT", value = "8081" },
        { name = "S3_SNAPSHOT_BUCKET", value = module.s3_ows_coda_search_snapshots[0].s3_bucket_name_output },
        { name = "SNOWFLAKE_ACCOUNT", value = var.search_snowflake_account },
        { name = "SNOWFLAKE_DATABASE", value = var.search_snowflake_database },
        { name = "SNOWFLAKE_ROLE", value = var.search_snowflake_role },
        { name = "SNOWFLAKE_USER", value = var.search_snowflake_username },
        { name = "SNOWFLAKE_ENV", value = var.search_snowflake_env },
        { name = "SNOWFLAKE_WAREHOUSE", value = var.search_snowflake_warehouse },
        { name = "GRAPHQL_POLL_INTERVAL_MS", value = tostring(var.search_graphql_poll_interval_ms) },
        { name = "SNOWFLAKE_POLL_INTERVAL_MS", value = tostring(var.search_snowflake_poll_interval_ms) },

        # ── Reranking ──
        # Activates the cross-encoder reranker that is pre-cached in the Docker
        # image. Re-scores the top hybrid-search candidates using full
        # query-document attention, improving precision for ambiguous queries.
        { name = "SEARCH_RERANKER_MODEL", value = "Xenova/bge-reranker-large" },

        # ── Search tuning ──
        # Pinned here so app-default changes don't silently alter search behavior.
        # RRF_K: Reciprocal Rank Fusion smoothing constant. Lower values give more
        # weight to top-ranked results when merging BM25 + vector scores.
        { name = "RRF_K", value = "20" },
        # Number of vector-stage results used as proximity anchors in RRF fusion.
        { name = "SEARCH_PROXIMITY_ANCHORS", value = "15" },
        # FK-graph traversal depth for relationship augmentation.
        { name = "SEARCH_GRAPH_DEPTH", value = "2" },
        # Max related items returned from graph augmentation.
        { name = "SEARCH_GRAPH_MAX_RELATED", value = "100" },
        # Pre-allocated capacity for the HNSW approximate nearest-neighbor index.
        { name = "HNSW_CAPACITY", value = "100000" },
        # Max columns surfaced per table in search results.
        { name = "MAX_COLUMNS_PER_TABLE", value = "32" },
        # Max Snowflake connections — headroom for concurrent refresh + queries.
        { name = "SNOWFLAKE_SYSTEM_POOL_MAX", value = "5" },
        # Sentry DSN
        { name = "SENTRY_DSN", value = try(module.sentry_ows_coda_search[0].sentry_key_dsn_public_output, "") },
      ]

      secrets = [
        { name = "SNOWFLAKE_PRIVATE_KEY", valueFrom = "${local.search_secret_arn_prefix}/SNOWFLAKE_PRIVATE_KEY" },
        { name = "SNOWFLAKE_PRIVATE_KEY_PASS", valueFrom = "${local.search_secret_arn_prefix}/SNOWFLAKE_PRIVATE_KEY_PASS" },
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = aws_cloudwatch_log_group.search[0].name
          "awslogs-region"        = var.region
          "awslogs-stream-prefix" = "search"
        }
      }
    },

    # ── Datadog agent sidecar ──
    # Receives traces on :8126 and OTLP on :4317/:4318 from the app container.
    # Config mirrors the terraform-fargate module's datadog-agent container.
    {
      name                   = "datadog-agent"
      image                  = local.datadog_agent_image
      cpu                    = 64
      memory                 = 256
      essential              = false
      readonlyRootFilesystem = true

      portMappings = [
        { hostPort = 8126, containerPort = 8126, protocol = "tcp" },
        { hostPort = 4317, containerPort = 4317, protocol = "tcp" },
        { hostPort = 4318, containerPort = 4318, protocol = "tcp" },
      ]

      environment = [
        { name = "DD_SITE", value = "datadoghq.com" },
        { name = "DD_APM_ENABLED", value = "true" },
        { name = "DD_APM_IGNORE_RESOURCES", value = "^GET /health.*$" },
        { name = "DD_LOGS_ENABLED", value = "true" },
        { name = "DD_OTLP_CONFIG_LOGS_ENABLED", value = "true" },
        { name = "DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT", value = "0.0.0.0:4317" },
        { name = "DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT", value = "0.0.0.0:4318" },
        { name = "DD_ECS_TASK_COLLECTION_ENABLED", value = "true" },
      ]

      secrets = [
        { name = "DD_API_KEY", valueFrom = data.aws_secretsmanager_secret.datadog_api_key[0].arn },
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = aws_cloudwatch_log_group.search[0].name
          "awslogs-region"        = var.region
          "awslogs-stream-prefix" = "datadog-agent"
        }
      }
    },
  ])
}

################
# ECS Service
################

resource "aws_ecs_service" "search" {
  count           = local.is_ephemeral ? 0 : 1
  name            = "${var.environment}-${local.search_service_name}"
  cluster         = aws_ecs_cluster.search[0].id
  task_definition = aws_ecs_task_definition.search[0].arn
  desired_count   = 1 # QA: single task is acceptable (keyword fallback covers outages).
  # Production: raise to 2+ across AZs for HA. GPU instance launches take 2-5 min,
  # so a single-task failure means 5-10 min of degraded search until replacement is ready.

  # GPU model warm-up takes 30-120s; without this grace period ECS kills the
  # task for failing ALB health checks before the model finishes loading.
  health_check_grace_period_seconds = 300

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  capacity_provider_strategy {
    capacity_provider = aws_ecs_capacity_provider.search_gpu[0].name
    base              = 1
    weight            = 100
  }

  network_configuration {
    subnets          = module.vpc_info.default_private_subnet_ids
    security_groups  = [aws_security_group.search_task[0].id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.search[0].arn
    container_name   = local.search_service_name
    container_port   = 8081
  }

  # CI/CD deploys new task definition revisions outside Terraform.
  # Without this, terraform apply rolls back to the Terraform-managed revision.
  lifecycle {
    ignore_changes = [task_definition]
  }
}
