# ---------------------------------------------------------------------------
# The hosted playground: image repository, task, service, load balancer,
# hostname, and the scale-to-zero machinery.
#
# Every resource here is new. The existing roles and policies are not edited;
# where the playground needs a permission the CI role or an existing role does
# not have, it is granted through a new policy attached alongside.
# ---------------------------------------------------------------------------

locals {
  playground_container = "playground"
  playground_image     = "${aws_ecr_repository.playground.repository_url}:${var.playground_image_tag}"
  playground_port      = 8101
  documents_prefix     = "documents/"
  activity_namespace   = "Quber/Playground"
}

# ---- image ----------------------------------------------------------------

resource "aws_ecr_repository" "playground" {
  name                 = "quber-playground"
  image_tag_mutability = "MUTABLE"
  tags                 = local.playground_tags

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecr_lifecycle_policy" "playground" {
  repository = aws_ecr_repository.playground.name

  policy = jsonencode({
    rules = [{
      rulePriority = 1
      description  = "Keep the last 3 playground images"
      selection = {
        tagStatus   = "any"
        countType   = "imageCountMoreThan"
        countNumber = 3
      }
      action = { type = "expire" }
    }]
  })
}

# ---- secrets ----------------------------------------------------------------

# The shared login's two parameters are no longer read by the app or the wake
# function. They stay, with their values, until WorkOS sign-in is verified on
# the hosted playground: the task definition still passes them, so rolling
# back is redeploying the previous image tag. A follow-up change deletes them.
resource "aws_ssm_parameter" "playground_login_user" {
  name  = "/quber/PLAYGROUND_LOGIN_USER"
  type  = "SecureString"
  value = "REPLACE_ME"
  tags  = local.playground_tags

  lifecycle {
    ignore_changes = [value]
  }
}

resource "aws_ssm_parameter" "playground_login_password" {
  name  = "/quber/PLAYGROUND_LOGIN_PASSWORD"
  type  = "SecureString"
  value = "REPLACE_ME"
  tags  = local.playground_tags

  lifecycle {
    ignore_changes = [value]
  }
}

# The WorkOS environment the playground signs people in through, and the one
# organization whose members may enter.
resource "aws_ssm_parameter" "workos_api_key" {
  name  = "/quber/WORKOS_API_KEY"
  type  = "SecureString"
  value = "REPLACE_ME"
  tags  = local.playground_tags

  lifecycle {
    ignore_changes = [value]
  }
}

resource "aws_ssm_parameter" "workos_client_id" {
  name  = "/quber/WORKOS_CLIENT_ID"
  type  = "SecureString"
  value = "REPLACE_ME"
  tags  = local.playground_tags

  lifecycle {
    ignore_changes = [value]
  }
}

resource "aws_ssm_parameter" "workos_organization_id" {
  name  = "/quber/WORKOS_ORGANIZATION_ID"
  type  = "SecureString"
  value = "REPLACE_ME"
  tags  = local.playground_tags

  lifecycle {
    ignore_changes = [value]
  }
}

resource "aws_ssm_parameter" "playground_session_secret" {
  name  = "/quber/PLAYGROUND_SESSION_SECRET"
  type  = "SecureString"
  value = "REPLACE_ME"
  tags  = local.playground_tags

  lifecycle {
    ignore_changes = [value]
  }
}

# ---- roles ------------------------------------------------------------------

resource "aws_iam_role" "playground_execution" {
  name               = "quber-playground-execution"
  assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
  tags               = local.playground_tags
}

resource "aws_iam_role_policy_attachment" "playground_execution_managed" {
  role       = aws_iam_role.playground_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "playground_execution_secrets" {
  statement {
    sid     = "ReadParameters"
    actions = ["ssm:GetParameters"]
    resources = [
      aws_ssm_parameter.anthropic_api_key.arn,
      aws_ssm_parameter.ade_api_key.arn,
      aws_ssm_parameter.runpod_api_key.arn,
      aws_ssm_parameter.logfire_token.arn,
      aws_ssm_parameter.langsmith_api_key.arn,
      aws_ssm_parameter.postgres_password.arn,
      aws_ssm_parameter.typesafe_api_key.arn,
      aws_ssm_parameter.playground_login_user.arn,
      aws_ssm_parameter.playground_login_password.arn,
      aws_ssm_parameter.workos_api_key.arn,
      aws_ssm_parameter.workos_client_id.arn,
      aws_ssm_parameter.workos_organization_id.arn,
      aws_ssm_parameter.playground_session_secret.arn,
    ]
  }
  statement {
    sid       = "DecryptParameters"
    actions   = ["kms:Decrypt"]
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "kms:ViaService"
      values   = ["ssm.${var.region}.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy" "playground_execution_secrets" {
  name   = "ssm-read"
  role   = aws_iam_role.playground_execution.id
  policy = data.aws_iam_policy_document.playground_execution_secrets.json
}

resource "aws_iam_role" "playground_task" {
  name               = "quber-playground-task"
  assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
  tags               = local.playground_tags
}

data "aws_iam_policy_document" "playground_task" {
  statement {
    sid       = "DocumentsPrefix"
    actions   = ["s3:GetObject", "s3:PutObject"]
    resources = ["${aws_s3_bucket.output.arn}/${local.documents_prefix}*"]
  }
  statement {
    sid       = "ListOutput"
    actions   = ["s3:ListBucket"]
    resources = [aws_s3_bucket.output.arn]
  }
  statement {
    # An upload may name an s3:// source anywhere in the input bucket.
    sid       = "ReadInput"
    actions   = ["s3:GetObject"]
    resources = ["${data.aws_s3_bucket.input.arn}/*"]
  }
  statement {
    sid       = "ListInput"
    actions   = ["s3:ListBucket"]
    resources = [data.aws_s3_bucket.input.arn]
  }
  statement {
    # Signed-in activity, the metric the idle alarm watches.
    sid       = "ReportActivity"
    actions   = ["cloudwatch:PutMetricData"]
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "cloudwatch:namespace"
      values   = [local.activity_namespace]
    }
  }
  statement {
    # The output bucket is KMS-encrypted; writing and reading it decrypts.
    sid       = "OutputBucketKms"
    actions   = ["kms:Decrypt", "kms:GenerateDataKey"]
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "kms:ViaService"
      values   = ["s3.${var.region}.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy" "playground_task" {
  name   = "s3-access"
  role   = aws_iam_role.playground_task.id
  policy = data.aws_iam_policy_document.playground_task.json
}

# The CI role gains what deploying the playground needs, as its own policy
# beside the existing one: push to the new repository, register the new task
# family's revisions, hand the new roles to it, and update the service.
data "aws_iam_policy_document" "github_actions_playground" {
  statement {
    sid = "EcrPushPlayground"
    actions = [
      "ecr:BatchCheckLayerAvailability",
      "ecr:BatchGetImage",
      "ecr:CompleteLayerUpload",
      "ecr:GetDownloadUrlForLayer",
      "ecr:InitiateLayerUpload",
      "ecr:PutImage",
      "ecr:UploadLayerPart",
    ]
    resources = [aws_ecr_repository.playground.arn]
  }
  statement {
    sid       = "PassPlaygroundRoles"
    actions   = ["iam:PassRole"]
    resources = [aws_iam_role.playground_task.arn, aws_iam_role.playground_execution.arn]
  }
  statement {
    sid       = "UpdateService"
    actions   = ["ecs:UpdateService", "ecs:DescribeServices"]
    resources = [aws_ecs_service.playground.id]
  }
}

resource "aws_iam_role_policy" "github_actions_playground" {
  name   = "ci-playground"
  role   = aws_iam_role.github_actions.id
  policy = data.aws_iam_policy_document.github_actions_playground.json
}

# ---- logs -------------------------------------------------------------------

resource "aws_cloudwatch_log_group" "playground" {
  name              = "/quber/playground"
  retention_in_days = var.playground_log_retention_days
  tags              = local.playground_tags
}

resource "aws_cloudwatch_log_group" "wake" {
  name              = "/aws/lambda/quber-playground-wake"
  retention_in_days = var.playground_log_retention_days
  tags              = local.playground_tags
}

# ---- network: load balancer ------------------------------------------------

resource "aws_security_group" "playground_alb" {
  name        = "quber-playground-alb"
  description = "Playground load balancer: HTTPS from anywhere"
  vpc_id      = data.aws_vpc.default.id
  tags        = local.playground_tags

  ingress {
    description = "HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTP, redirected to HTTPS"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group_rule" "playground_task_from_alb" {
  type                     = "ingress"
  description              = "App port from the load balancer"
  from_port                = local.playground_port
  to_port                  = local.playground_port
  protocol                 = "tcp"
  security_group_id        = aws_security_group.playground_task.id
  source_security_group_id = aws_security_group.playground_alb.id
}

resource "aws_lb" "playground" {
  name               = "quber-playground"
  load_balancer_type = "application"
  security_groups    = [aws_security_group.playground_alb.id]
  subnets            = data.aws_subnets.default.ids
  idle_timeout       = 300
  tags               = local.playground_tags
}

resource "aws_lb_target_group" "playground_app" {
  name        = "quber-playground-app"
  port        = local.playground_port
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = data.aws_vpc.default.id
  tags        = local.playground_tags

  # Thirty seconds lets a request in flight finish when the service scales in.
  deregistration_delay = 30

  health_check {
    path                = "/healthz"
    matcher             = "200"
    interval            = 15
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 3
  }
}

resource "aws_lb_target_group" "playground_wake" {
  name        = "quber-playground-wake"
  target_type = "lambda"
  tags        = local.playground_tags
}

resource "aws_lambda_permission" "wake_from_alb" {
  statement_id  = "AllowALBInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.wake.function_name
  principal     = "elasticloadbalancing.amazonaws.com"
  source_arn    = aws_lb_target_group.playground_wake.arn
}

resource "aws_lb_target_group_attachment" "playground_wake" {
  target_group_arn = aws_lb_target_group.playground_wake.arn
  target_id        = aws_lambda_function.wake.arn
  depends_on       = [aws_lambda_permission.wake_from_alb]
}

# ---- hostname and certificate ----------------------------------------------

data "aws_route53_zone" "playground" {
  name = var.playground_zone
}

resource "aws_acm_certificate" "playground" {
  domain_name       = var.playground_hostname
  validation_method = "DNS"
  tags              = local.playground_tags

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route53_record" "playground_validation" {
  for_each = {
    for dvo in aws_acm_certificate.playground.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  zone_id = data.aws_route53_zone.playground.zone_id
  name    = each.value.name
  type    = each.value.type
  ttl     = 60
  records = [each.value.record]
}

resource "aws_acm_certificate_validation" "playground" {
  certificate_arn         = aws_acm_certificate.playground.arn
  validation_record_fqdns = [for r in aws_route53_record.playground_validation : r.fqdn]
}

resource "aws_route53_record" "playground" {
  zone_id = data.aws_route53_zone.playground.zone_id
  name    = var.playground_hostname
  type    = "A"

  alias {
    name                   = aws_lb.playground.dns_name
    zone_id                = aws_lb.playground.zone_id
    evaluate_target_health = false
  }
}

# ---- listeners --------------------------------------------------------------

resource "aws_lb_listener" "playground_http" {
  load_balancer_arn = aws_lb.playground.arn
  port              = 80
  protocol          = "HTTP"
  tags              = local.playground_tags

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# The listener has one rule for every path, and the wake function owns where
# it points: the app's target group while a task is healthy, the function
# itself otherwise. The load balancer cannot make that choice on its own -- a
# forward to an empty target group is a 503 -- so the rule's action is
# expected to drift from what is written here.
#
# The default action names the app's target group and never serves a request,
# because the rule matches every path first. It is there so the target group
# is always in use: the load balancer health-checks a target group only while
# some action references it, and the wake function decides by that health.
resource "aws_lb_listener" "playground_https" {
  load_balancer_arn = aws_lb.playground.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate_validation.playground.certificate_arn
  tags              = local.playground_tags

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.playground_app.arn
  }
}

resource "aws_lb_listener_rule" "playground" {
  listener_arn = aws_lb_listener.playground_https.arn
  priority     = 10
  tags         = local.playground_tags

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.playground_wake.arn
  }

  condition {
    path_pattern {
      values = ["/*"]
    }
  }

  lifecycle {
    ignore_changes = [action]
  }
}

# ---- task and service -------------------------------------------------------

resource "aws_ecs_task_definition" "playground" {
  family                   = "quber-playground"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = var.playground_task_cpu
  memory                   = var.playground_task_memory
  execution_role_arn       = aws_iam_role.playground_execution.arn
  task_role_arn            = aws_iam_role.playground_task.arn
  tags                     = local.playground_tags

  ephemeral_storage {
    size_in_gib = 40
  }

  container_definitions = jsonencode([{
    name      = local.playground_container
    image     = local.playground_image
    essential = true
    portMappings = [{
      containerPort = local.playground_port
      protocol      = "tcp"
    }]
    environment = [
      { name = "QUBER_LLM_BACKEND", value = "api" },
      { name = "AWS_REGION", value = var.region },
      { name = "QUBER_S3_REGION", value = var.region },
      { name = "PYTHONUNBUFFERED", value = "1" },
      { name = "EMBEDDING_DEVICE", value = "cpu" },
      { name = "QUBER_PLAYGROUND_ARTIFACTS_URI", value = "s3://${aws_s3_bucket.output.bucket}/${local.documents_prefix}" },
      { name = "POSTGRES_HOST", value = aws_db_instance.playground.address },
      { name = "POSTGRES_PORT", value = tostring(aws_db_instance.playground.port) },
      { name = "POSTGRES_USER", value = var.db_username },
      { name = "POSTGRES_DB", value = var.db_name },
      { name = "TRACE_TO_LANGSMITH", value = "true" },
      { name = "CC_LANGSMITH_PROJECT", value = "quberai" },
      { name = "RUNPOD_ENDPOINT_ID", value = var.runpod_endpoint_id },
      { name = "QUBER_PLAYGROUND_ACTIVITY_NAMESPACE", value = local.activity_namespace },
    ]
    secrets = [
      { name = "ANTHROPIC_API_KEY", valueFrom = aws_ssm_parameter.anthropic_api_key.arn },
      { name = "ADE_API_KEY", valueFrom = aws_ssm_parameter.ade_api_key.arn },
      { name = "TYPESAFE_API_KEY", valueFrom = aws_ssm_parameter.typesafe_api_key.arn },
      { name = "RUNPOD_API_KEY", valueFrom = aws_ssm_parameter.runpod_api_key.arn },
      { name = "LOGFIRE_TOKEN", valueFrom = aws_ssm_parameter.logfire_token.arn },
      { name = "CC_LANGSMITH_API_KEY", valueFrom = aws_ssm_parameter.langsmith_api_key.arn },
      { name = "POSTGRES_PASSWORD", valueFrom = aws_ssm_parameter.postgres_password.arn },
      { name = "PLAYGROUND_LOGIN_USER", valueFrom = aws_ssm_parameter.playground_login_user.arn },
      { name = "PLAYGROUND_LOGIN_PASSWORD", valueFrom = aws_ssm_parameter.playground_login_password.arn },
      { name = "WORKOS_API_KEY", valueFrom = aws_ssm_parameter.workos_api_key.arn },
      { name = "WORKOS_CLIENT_ID", valueFrom = aws_ssm_parameter.workos_client_id.arn },
      { name = "WORKOS_ORGANIZATION_ID", valueFrom = aws_ssm_parameter.workos_organization_id.arn },
      { name = "PLAYGROUND_SESSION_SECRET", valueFrom = aws_ssm_parameter.playground_session_secret.arn },
    ]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"         = aws_cloudwatch_log_group.playground.name
        "awslogs-region"        = var.region
        "awslogs-stream-prefix" = "playground"
      }
    }
  }])

  lifecycle {
    ignore_changes = [container_definitions]
  }
}

resource "aws_ecs_service" "playground" {
  name            = "quber-playground"
  cluster         = aws_ecs_cluster.quber.id
  task_definition = aws_ecs_task_definition.playground.arn
  launch_type     = "FARGATE"
  desired_count   = 0
  tags            = local.playground_tags

  # The task boots, loads the embedding model and connects to the database
  # before its health route answers 200; ECS must not replace it meanwhile.
  health_check_grace_period_seconds = 600

  network_configuration {
    subnets          = data.aws_subnets.default.ids
    security_groups  = [aws_security_group.playground_task.id]
    assign_public_ip = true
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.playground_app.arn
    container_name   = local.playground_container
    container_port   = local.playground_port
  }

  # The wake function and the idle policy move desired_count; the deploy
  # workflow moves task_definition.
  lifecycle {
    ignore_changes = [desired_count, task_definition]
  }

  depends_on = [aws_lb_listener.playground_https]
}

# ---- scale to zero ----------------------------------------------------------

data "aws_iam_policy_document" "wake" {
  statement {
    sid       = "Logs"
    actions   = ["logs:CreateLogStream", "logs:PutLogEvents"]
    resources = ["${aws_cloudwatch_log_group.wake.arn}:*"]
  }
  statement {
    sid       = "WakeService"
    actions   = ["ecs:DescribeServices", "ecs:UpdateService"]
    resources = [aws_ecs_service.playground.id]
  }
  statement {
    sid       = "ReadIdleAlarm"
    actions   = ["cloudwatch:DescribeAlarms", "cloudwatch:GetMetricStatistics"]
    resources = ["*"]
  }
  statement {
    # DescribeTargetHealth and DescribeRules take no resource-level scope.
    sid       = "ReadTargetHealth"
    actions   = ["elasticloadbalancing:DescribeTargetHealth", "elasticloadbalancing:DescribeRules"]
    resources = ["*"]
  }
  statement {
    sid       = "ThrowTheSwitch"
    actions   = ["elasticloadbalancing:ModifyRule"]
    resources = [aws_lb_listener_rule.playground.arn]
  }
  statement {
    # The function is the sign-in while no task runs: same WorkOS
    # environment, organization and session secret as the app.
    sid     = "ReadSignInParameters"
    actions = ["ssm:GetParameters"]
    resources = [
      aws_ssm_parameter.workos_api_key.arn,
      aws_ssm_parameter.workos_client_id.arn,
      aws_ssm_parameter.workos_organization_id.arn,
      aws_ssm_parameter.playground_session_secret.arn,
    ]
  }
  statement {
    sid       = "DecryptParameters"
    actions   = ["kms:Decrypt"]
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "kms:ViaService"
      values   = ["ssm.${var.region}.amazonaws.com"]
    }
  }
  statement {
    # A sign-in at the function counts as one signed-in request.
    sid       = "RecordSignIn"
    actions   = ["cloudwatch:PutMetricData"]
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "cloudwatch:namespace"
      values   = [local.activity_namespace]
    }
  }
  statement {
    # The scheduled check leaves a task alone while it is younger than the
    # wake grace. ListTasks and DescribeTasks take no resource-level scope
    # beyond the cluster.
    sid       = "ReadTasks"
    actions   = ["ecs:ListTasks", "ecs:DescribeTasks"]
    resources = ["*"]
    condition {
      test     = "ArnEquals"
      variable = "ecs:cluster"
      values   = [aws_ecs_cluster.quber.arn]
    }
  }
}

resource "aws_iam_role" "wake" {
  name               = "quber-playground-wake"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
  tags               = local.playground_tags
}

resource "aws_iam_role_policy" "wake" {
  name   = "wake"
  role   = aws_iam_role.wake.id
  policy = data.aws_iam_policy_document.wake.json
}

# The function's package is its own file plus the app's session module, so
# the sign-in, the organization check and the cookie have one source.
data "archive_file" "wake" {
  type        = "zip"
  output_path = "${path.module}/build/wake.zip"

  source {
    content  = file("${path.module}/../lambda/wake.py")
    filename = "wake.py"
  }
  source {
    content  = file("${path.module}/../../src/quber/playground/session.py")
    filename = "session.py"
  }
  source {
    content  = file("${path.module}/../lambda/startup.html")
    filename = "startup.html"
  }
  source {
    content  = filebase64("${path.module}/../../src/quber/playground/static/link-card.png")
    filename = "link-card.png.b64"
  }
  source {
    content  = filebase64("${path.module}/../../src/quber/playground/static/qubera-mark-160.png")
    filename = "qubera-mark-160.png.b64"
  }
}

resource "aws_lambda_function" "wake" {
  function_name = "quber-playground-wake"
  role          = aws_iam_role.wake.arn
  runtime       = "python3.13"
  handler       = "wake.handler"
  timeout       = 20
  tags          = local.playground_tags

  filename         = data.archive_file.wake.output_path
  source_code_hash = data.archive_file.wake.output_base64sha256

  environment {
    variables = {
      ECS_CLUSTER                      = aws_ecs_cluster.quber.name
      ECS_SERVICE                      = aws_ecs_service.playground.name
      RULE_ARN                         = aws_lb_listener_rule.playground.arn
      IDLE_ALARM                       = "quber-playground-idle"
      ACTIVITY_NAMESPACE               = local.activity_namespace
      APP_TARGET_GROUP_ARN             = aws_lb_target_group.playground_app.arn
      WAKE_TARGET_GROUP_ARN            = aws_lb_target_group.playground_wake.arn
      WORKOS_API_KEY_PARAMETER         = aws_ssm_parameter.workos_api_key.name
      WORKOS_CLIENT_ID_PARAMETER       = aws_ssm_parameter.workos_client_id.name
      WORKOS_ORGANIZATION_ID_PARAMETER = aws_ssm_parameter.workos_organization_id.name
      SESSION_SECRET_PARAMETER         = aws_ssm_parameter.playground_session_secret.name
    }
  }

  depends_on = [aws_cloudwatch_log_group.wake]
}

# No signed-in request for the idle period. The metric is the app's own count
# of requests that carried a valid session, reported by the login gate; the
# load balancer's request count is never zero on a public hostname, because
# internet scanners reach the login page every few minutes. A task that is
# down reports nothing, so missing data counts as breaching.
resource "aws_cloudwatch_metric_alarm" "playground_idle" {
  alarm_name          = "quber-playground-idle"
  alarm_description   = "The playground has had no signed-in request for ${var.playground_idle_minutes} minutes; scale it to zero."
  namespace           = local.activity_namespace
  metric_name         = "AuthenticatedRequests"
  statistic           = "Sum"
  period              = 300
  evaluation_periods  = var.playground_idle_minutes / 5
  threshold           = 0
  comparison_operator = "LessThanOrEqualToThreshold"
  treat_missing_data  = "breaching"
  tags                = local.playground_tags

  dimensions = {
    Service = aws_ecs_service.playground.name
  }
}

# The two events that throw the switch back: the idle alarm entering ALARM,
# and any task of the service stopping.
resource "aws_cloudwatch_event_rule" "playground_idle" {
  name        = "quber-playground-idle"
  description = "The playground idle alarm changed state"
  tags        = local.playground_tags

  event_pattern = jsonencode({
    source        = ["aws.cloudwatch"]
    "detail-type" = ["CloudWatch Alarm State Change"]
    resources     = [aws_cloudwatch_metric_alarm.playground_idle.arn]
  })
}

resource "aws_cloudwatch_event_target" "playground_idle" {
  rule = aws_cloudwatch_event_rule.playground_idle.name
  arn  = aws_lambda_function.wake.arn
}

resource "aws_lambda_permission" "wake_from_idle_event" {
  statement_id  = "AllowIdleEvent"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.wake.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.playground_idle.arn
}

resource "aws_cloudwatch_event_rule" "playground_task_stopped" {
  name        = "quber-playground-task-stopped"
  description = "A task of the playground service stopped"
  tags        = local.playground_tags

  event_pattern = jsonencode({
    source        = ["aws.ecs"]
    "detail-type" = ["ECS Task State Change"]
    detail = {
      clusterArn = [aws_ecs_cluster.quber.arn]
      group      = ["service:${aws_ecs_service.playground.name}"]
      lastStatus = ["STOPPED"]
    }
  })
}

resource "aws_cloudwatch_event_target" "playground_task_stopped" {
  rule = aws_cloudwatch_event_rule.playground_task_stopped.name
  arn  = aws_lambda_function.wake.arn
}

resource "aws_lambda_permission" "wake_from_task_event" {
  statement_id  = "AllowTaskEvent"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.wake.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.playground_task_stopped.arn
}

# The alarm announces transitions only. A task woken while the alarm already
# sits in ALARM, with nobody signing in afterwards, never causes one, so the
# function also checks the alarm on a schedule.
resource "aws_cloudwatch_event_rule" "playground_idle_check" {
  name                = "quber-playground-idle-check"
  description         = "Scale the playground in if the idle alarm is in ALARM while a task runs"
  schedule_expression = "rate(5 minutes)"
  tags                = local.playground_tags
}

resource "aws_cloudwatch_event_target" "playground_idle_check" {
  rule = aws_cloudwatch_event_rule.playground_idle_check.name
  arn  = aws_lambda_function.wake.arn
}

resource "aws_lambda_permission" "wake_from_idle_check" {
  statement_id  = "AllowIdleCheck"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.wake.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.playground_idle_check.arn
}
