provider "aws" {
  region = var.aws_region
}

# Terraform backends cannot contain interpolations
terraform {
  backend "s3" {
    bucket  = "orcd-terraform-state"
    key     = "prod/orcdbucket/terraform.tfstate"
    region  = "us-east-1"
    encrypt = "true"
  }
}

data "aws_caller_identity" "current" {}

data "aws_kms_key" "source_keys" {
  for_each = var.backup_kms_keys

  key_id = each.value
}

locals {
  source_bucket_kms_key_arns = [
    for v in data.aws_kms_key.source_keys : v.arn
  ]
}

data "aws_iam_policy_document" "bucket_policy" {
  statement {
    principals {
      type        = "Service"
      identifiers = ["s3.amazonaws.com"]
    }

    actions = [
      "s3:PutObject",
    ]

    resources = [
      "${module.s3_bucket.s3_bucket_arn_output}/*",
    ]

    condition {
      test     = "StringEquals"
      variable = "s3:x-amz-acl"

      values = [
        "bucket-owner-full-control"
      ]
    }

    condition {
      test     = "StringEquals"
      variable = "aws:SourceAccount"

      values = [
        data.aws_caller_identity.current.account_id
      ]
    }

    condition {
      test     = "ArnLike"
      variable = "aws:SourceArn"

      values = [
        "arn:aws:s3:::cucumbers"
      ]
    }
  }
}

data "aws_iam_policy_document" "neo4j_dev_refresh" {
  statement {
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::103233932089:role/dev-neo4j-5-cluster-debian-asg-role"]
    }

    actions = [
      "s3:GetObject",
    ]

    resources = [
      "${module.s3_bucket.s3_bucket_arn_output}/backup/neo4j/*",
    ]
  }

  statement {
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::103233932089:role/dev-neo4j-5-cluster-debian-asg-role"]
    }

    actions = [
      "s3:ListBucket",
    ]

    resources = [
      "${module.s3_bucket.s3_bucket_arn_output}",
    ]

    condition {
      test     = "StringLike"
      variable = "s3:prefix"

      values = [
        "backup/neo4j/*"
      ]
    }
  }
}

module "s3_bucket" {
  source = "git@github.com:theorchard/terraform-s3.git//modules/s3_bucket?ref=3.15.0"

  env                 = var.environment
  bucket_name         = var.service_name
  s3_read_only_policy = false
  block_public_access = true
  application_family  = var.application_family

  bucket_policy_overrides = [
    data.aws_iam_policy_document.bucket_policy.json,
    data.aws_iam_policy_document.neo4j_dev_refresh.json,
  ]

  # Cleared (non-empty module default) so the module creates no lifecycle config.
  lifecycle_rules_abort_incomplete_multipart_upload_days = []

  apply_server_side_encryption_by_default = {
    sse_algorithm = "AES256"
  }

  apply_replication_configuration = [{
    prefix                            = "backup/"
    destination_bucket                = "prod-orcdbucket-replica"
    storage_class                     = "STANDARD_IA"
    enabled                           = true
    delete_marker_replication_enabled = false
    replicate_kms_encrypted_objects   = true
    replica_kms_key_arn               = var.replica_kms_key_id
    source_kms_key_arns               = local.source_bucket_kms_key_arns
  }]

  storage_lens_configuration = {
    enabled                                    = true
    advanced_cost_optimization_metrics_enabled = true
    advanced_data_protection_metrics_enabled   = true
  }
}

moved {
  from = module.s3_bucket.aws_s3_bucket_lifecycle_configuration.lifecycle_configuration[0]
  to   = aws_s3_bucket_lifecycle_configuration.bucket
}

resource "aws_s3_bucket_lifecycle_configuration" "bucket" {
  bucket = module.s3_bucket.s3_bucket_name_output

  rule {
    id     = "transition-backup-to-glacier"
    status = "Enabled"

    filter {
      prefix = "backup/"
    }

    transition {
      days          = 60
      storage_class = "GLACIER"
    }
  }

  rule {
    id     = "transition-feed-drop-to-glacier"
    status = "Enabled"

    filter {
      prefix = "feed-drop"
    }

    transition {
      days          = 90
      storage_class = "GLACIER"
    }
  }

  rule {
    id     = "transition-mr-active-snowflake-export-to-glacier"
    status = "Enabled"

    filter {
      prefix = "mr-active-snowflake-export"
    }

    transition {
      days          = 30
      storage_class = "GLACIER"
    }
  }

  rule {
    id     = "noncurrent-transition-backup-to-glacier"
    status = "Enabled"

    filter {
      prefix = "backup/"
    }

    noncurrent_version_transition {
      noncurrent_days = 60
      storage_class   = "GLACIER"
    }
  }

  rule {
    id     = "noncurrent-transition-feed-drop-to-glacier"
    status = "Enabled"

    filter {
      prefix = "feed-drop"
    }

    noncurrent_version_transition {
      noncurrent_days = 30
      storage_class   = "GLACIER"
    }
  }

  rule {
    id     = "expire-helpcenter"
    status = "Enabled"

    filter {
      prefix = "helpcenter/"
    }

    expiration {
      days = 1
    }

    noncurrent_version_expiration {
      noncurrent_days = 1
    }
  }

  rule {
    id     = "expire-backup-neo4j-prod-4"
    status = "Enabled"

    filter {
      prefix = "backup/neo4j/prod-neo4j-cluster.theorchard.io/prod-4"
    }

    expiration {
      days = 30
    }
  }

  rule {
    id     = "expire-mr-active-snowflake-export"
    status = "Enabled"

    filter {
      prefix = "mr-active-snowflake-export/"
    }

    expiration {
      days = 1
    }

    noncurrent_version_expiration {
      noncurrent_days = 1
    }
  }

  rule {
    id     = "expire-feed-drop-ticketek"
    status = "Enabled"

    filter {
      prefix = "feed-drop/Ticketek/"
    }

    expiration {
      days = 14
    }
  }

  rule {
    id     = "expire-feed-drop-seated"
    status = "Enabled"

    filter {
      prefix = "feed-drop/Seated/"
    }

    expiration {
      days = 14
    }
  }

  rule {
    id     = "cleanup-stale-statement-attachment-uploads"
    status = "Enabled"

    # Size filter spares the 0-byte .keep file from expiration.
    filter {
      and {
        prefix                   = "prod-statement-attachments/"
        object_size_greater_than = 0
        object_size_less_than    = 5497558138880 # 5 TB, S3 max object size
      }
    }

    expiration {
      days = 30
    }

    noncurrent_version_expiration {
      noncurrent_days = 30
    }
  }

  rule {
    id     = "abort-incomplete-multipart-uploads"
    status = "Enabled"

    filter {}

    abort_incomplete_multipart_upload {
      days_after_initiation = 7
    }
  }
}

resource "aws_s3_object" "statement_attachments_keep" {
  bucket  = module.s3_bucket.s3_bucket_name_output
  key     = "prod-statement-attachments/.keep"
  content = ""
}

data "aws_lambda_function" "event_notification_functions" {
  for_each = toset([for configuration in var.event_notification_configurations : configuration.function_name])

  function_name = each.value
}

resource "aws_s3_bucket_notification" "event_notification_configuration" {
  bucket = module.s3_bucket.s3_bucket_name_output

  dynamic "lambda_function" {
    for_each = var.event_notification_configurations
    content {
      lambda_function_arn = data.aws_lambda_function.event_notification_functions[lambda_function.value.function_name].arn
      events              = lambda_function.value.events
      filter_prefix       = lambda_function.value.filter_prefix
    }
  }
  
  depends_on = [aws_lambda_permission.allow_s3_invoke]
}

resource "aws_lambda_permission" "allow_s3_invoke" {
  for_each = data.aws_lambda_function.event_notification_functions

  statement_id   = "AllowExecutionFromS3Bucket"
  action         = "lambda:InvokeFunction"
  function_name  = each.value.function_name
  principal      = "s3.amazonaws.com"
  source_arn     = module.s3_bucket.s3_bucket_arn_output
  source_account = data.aws_caller_identity.current.account_id
}

# Create assume_role policies for each cross-account role entry
data "aws_iam_policy_document" "cross_account_role_assume_role_policy" {
  for_each = toset([
    for key, role_dict in var.feed_drop_roles_read_write_scoped_policy_subdirectories :
    role_dict["role_arn"]
  ])

  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type = "AWS"
      identifiers = [
        each.key,
      ]
    }
  }
}

# Feed-drop SSE-KMS setup: creates dedicated symmetric KMS keys, aliases, and upload permissions
# for configured feed-drop subdirectories, and restricts key usage to S3 writes in matching prefixes.
resource "aws_kms_key" "feed_drop_sse_kms_key" {
  for_each = var.feed_drop_sse_kms_role_subdirectories

  description             = "Symmetric KMS key for ${var.environment}-${var.service_name} feed-drop/${each.value} SSE-KMS uploads"
  enable_key_rotation     = true
  deletion_window_in_days = 30

  tags = {
    environment        = var.environment
    service_name       = var.service_name
    application_family = var.application_family
    terraformed        = true
  }
}

resource "aws_kms_alias" "feed_drop_sse_kms_alias" {
  for_each = var.feed_drop_sse_kms_role_subdirectories

  name          = "alias/${var.environment}-${var.service_name}-feed-drop-${replace(each.value, "/", "-")}"
  target_key_id = aws_kms_key.feed_drop_sse_kms_key[each.value].key_id
}

data "aws_iam_policy_document" "feed_drop_sse_kms_usage_policy" {
  for_each = var.feed_drop_sse_kms_role_subdirectories

  statement {
    actions = [
      "kms:GenerateDataKey",
      "kms:Encrypt",
    ]

    resources = [
      aws_kms_key.feed_drop_sse_kms_key[each.value].arn,
    ]
  }
}

resource "aws_iam_policy" "feed_drop_sse_kms_usage_policy" {
  for_each = var.feed_drop_sse_kms_role_subdirectories

  name   = "KMS-${var.environment}-${var.service_name}-feed-drop-${replace(each.value, "/", "-")}-upload"
  policy = data.aws_iam_policy_document.feed_drop_sse_kms_usage_policy[each.value].json
}

data "aws_iam_policy_document" "feed_drop_sse_kms_decrypt_policy" {
  for_each = var.feed_drop_sse_kms_role_subdirectories

  statement {
    actions = [
      "kms:Decrypt",
      "kms:DescribeKey",
    ]

    resources = [
      aws_kms_key.feed_drop_sse_kms_key[each.value].arn,
    ]
  }
}

resource "aws_iam_policy" "feed_drop_sse_kms_decrypt_policy" {
  for_each = var.feed_drop_sse_kms_role_subdirectories

  name   = "KMS-${var.environment}-${var.service_name}-feed-drop-${replace(each.value, "/", "-")}-decrypt"
  policy = data.aws_iam_policy_document.feed_drop_sse_kms_decrypt_policy[each.value].json
}

resource "aws_iam_role_policy_attachment" "feed_drop_sse_kms_usage_policy_attachment" {
  for_each = var.feed_drop_sse_kms_role_subdirectories

  role       = aws_iam_role.feed_drop_read_write_roles[each.value].name
  policy_arn = aws_iam_policy.feed_drop_sse_kms_usage_policy[each.value].arn
}

# Create a policy for a configurable number of feed drop workloads
data "aws_iam_policy_document" "s3_feed_drop_read_write_policy" {
  for_each = toset(
    concat(
      var.feed_drop_users_read_write_scoped_policy_subdirectories,
      [
        for role_dict in var.feed_drop_roles_read_write_scoped_policy_subdirectories :
        role_dict["s3_key"]
      ],
    )
  )
  statement {
    actions = [
      "s3:PutObject",
    ]

    resources = [
      "${module.s3_bucket.s3_bucket_arn_output}/feed-drop/${each.key}/*",
    ]

    dynamic "condition" {
      for_each = contains(var.feed_drop_sse_kms_role_subdirectories, each.key) ? [1] : []
      content {
        test     = "StringEquals"
        variable = "s3:x-amz-server-side-encryption"

        values = [
          "aws:kms",
        ]
      }
    }

    dynamic "condition" {
      for_each = contains(var.feed_drop_sse_kms_role_subdirectories, each.key) ? [1] : []
      content {
        test     = "StringEquals"
        variable = "s3:x-amz-server-side-encryption-aws-kms-key-id"

        values = [
          aws_kms_key.feed_drop_sse_kms_key[each.key].arn,
        ]
      }
    }
  }

  statement {
    actions = [
      "s3:AbortMultipartUpload",
      "s3:GetAccelerateConfiguration",
      "s3:GetObject*",
      "s3:ListMultipartUploadParts",
    ]

    resources = [
      "${module.s3_bucket.s3_bucket_arn_output}/feed-drop/${each.key}/*",
    ]
  }

  statement {
    actions = [
      "s3:GetBucketLocation",
    ]

    resources = [
      module.s3_bucket.s3_bucket_arn_output
    ]
  }

  statement {
    actions = [
      "s3:ListBucket",
    ]

    resources = [
      module.s3_bucket.s3_bucket_arn_output
    ]

    condition {
      test     = "StringLike"
      variable = "s3:prefix"

      values = [
        "feed-drop/${each.key}/",
        "feed-drop/${each.key}/*",
      ]
    }
  }
}

resource "aws_iam_policy" "feed_drop_read_write_policy" {
  for_each = toset(
    concat(
      var.feed_drop_users_read_write_scoped_policy_subdirectories,
      [
        for key, role_dict in var.feed_drop_roles_read_write_scoped_policy_subdirectories :
        role_dict["s3_key"]
      ],
    )
  )

  name   = "S3-${module.s3_bucket.s3_bucket_name_output}-feed-drop-${replace(each.key, "/", "-")}-RW"
  policy = data.aws_iam_policy_document.s3_feed_drop_read_write_policy[each.key].json
}

# These are users solely used for feed drop scenarios. They generally only have the S3 feed drop RW policy attached.
resource "aws_iam_user" "feed_drop_read_write_users" {
  for_each = toset(var.feed_drop_users_read_write_scoped_policy_subdirectories)

  name = "${module.s3_bucket.s3_bucket_name_output}-feed-drop-${each.key}"

  tags = {
    role = "service-user",
  }

  lifecycle {
    ignore_changes = [tags]
  }
}

resource "aws_iam_user_policy_attachment" "feed_drop_read_write_user_attachments" {
  for_each = toset(var.feed_drop_users_read_write_scoped_policy_subdirectories)

  user       = aws_iam_user.feed_drop_read_write_users[each.key].name
  policy_arn = aws_iam_policy.feed_drop_read_write_policy[each.key].arn
}

# These are roles solely used for feed drop scenarios. They generally only have the S3 feed drop RW policy attached.
resource "aws_iam_role" "feed_drop_read_write_roles" {
  for_each = {
    for role_dict in var.feed_drop_roles_read_write_scoped_policy_subdirectories :
    role_dict["s3_key"] => role_dict
  }

  name               = "${module.s3_bucket.s3_bucket_name_output}-feed-drop-${replace(each.value.s3_key, "/", "-")}-role"
  assume_role_policy = data.aws_iam_policy_document.cross_account_role_assume_role_policy[each.value.role_arn].json
}

resource "aws_iam_role_policy_attachment" "feed_drop_read_write_role_attachments" {
  for_each = toset([
    for key, role_dict in var.feed_drop_roles_read_write_scoped_policy_subdirectories :
    role_dict["s3_key"]
  ])

  role       = aws_iam_role.feed_drop_read_write_roles[each.key].name
  policy_arn = aws_iam_policy.feed_drop_read_write_policy[each.key].arn
}

# Object prefixes requiring their own policies
data "aws_iam_policy_document" "s3_object_prefix_read_write_delete_policy_document" {
  for_each = toset(var.s3_object_prefixes_with_unique_read_write_delete_policies)
  statement {
    actions = [
      "s3:DeleteObject",
      "s3:GetObject*",
      "s3:PutObject*",
    ]

    resources = [
      "${module.s3_bucket.s3_bucket_arn_output}/${each.key}/*",
    ]
  }

  statement {
    actions = [
      "s3:ListBucket",
    ]

    resources = [
      module.s3_bucket.s3_bucket_arn_output
    ]
  }

  statement {
    actions = [
      "s3:ListAllMyBuckets",
      "s3:GetBucketLocation",
    ]

    resources = [
      "*"
    ]
  }
}

resource "aws_iam_policy" "s3_object_prefix_read_write_delete_policy" {
  for_each = data.aws_iam_policy_document.s3_object_prefix_read_write_delete_policy_document
  name     = "S3-${module.s3_bucket.s3_bucket_name_output}-${each.key}-RWD"
  policy   = data.aws_iam_policy_document.s3_object_prefix_read_write_delete_policy_document[each.key].json
}

# Create assume_role policies for each cross-account role entry
data "aws_iam_policy_document" "cross_account_read_role_assume_role_policy" {
  for_each = toset([
    for key, role_dict in var.feed_drop_roles_read_scoped_policy_subdirectories :
    role_dict["role_arn"]
  ])

  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type = "AWS"
      identifiers = [
        each.key,
      ]
    }
  }
}

# Create a policy for a configurable number of feed drop workloads
data "aws_iam_policy_document" "s3_feed_drop_read_policy_document" {
  for_each = toset(
    [
      for role_dict in var.feed_drop_roles_read_scoped_policy_subdirectories :
      role_dict["s3_key"]
    ],
  )
  statement {
    actions = [
      "s3:GetObject*",
      "s3:ListMultipartUploadParts",
    ]

    resources = [
      "${module.s3_bucket.s3_bucket_arn_output}/feed-drop/${each.key}/*",
    ]
  }

  statement {
    actions = [
      "s3:GetBucketLocation",
    ]

    resources = [
      module.s3_bucket.s3_bucket_arn_output
    ]
  }

  statement {
    actions = [
      "s3:ListBucket",
    ]

    resources = [
      module.s3_bucket.s3_bucket_arn_output
    ]

    condition {
      test     = "StringLike"
      variable = "s3:prefix"

      values = [
        "feed-drop/${each.key}/",
        "feed-drop/${each.key}/*",
      ]
    }
  }
}

resource "aws_iam_policy" "s3_feed_drop_read_policy" {
  for_each = toset(
    [
      for key, role_dict in var.feed_drop_roles_read_scoped_policy_subdirectories :
      role_dict["s3_key"]
    ],
  )

  name   = "S3-${module.s3_bucket.s3_bucket_name_output}-feed-drop-${each.key}-Read"
  policy = data.aws_iam_policy_document.s3_feed_drop_read_policy_document[each.key].json
}

# These are roles solely used for feed drop scenarios. They generally only have the S3 feed drop read policy attached.
resource "aws_iam_role" "feed_drop_read_roles" {
  for_each = {
    for role_dict in var.feed_drop_roles_read_scoped_policy_subdirectories :
    role_dict["s3_key"] => role_dict
  }

  name               = "S3-${module.s3_bucket.s3_bucket_name_output}-feed-drop-${each.value.s3_key}-role"
  assume_role_policy = data.aws_iam_policy_document.cross_account_read_role_assume_role_policy[each.value.role_arn].json
}

resource "aws_iam_role_policy_attachment" "feed_drop_read_role_attachments" {
  for_each = toset([
    for key, role_dict in var.feed_drop_roles_read_scoped_policy_subdirectories :
    role_dict["s3_key"]
  ])

  role       = aws_iam_role.feed_drop_read_roles[each.key].name
  policy_arn = aws_iam_policy.s3_feed_drop_read_policy[each.key].arn
}

data "aws_iam_policy" "feed_drop_ro_existing_policy" {
  name = var.feed_drop_ro_existing_policy
}

resource "aws_iam_group" "feed_drop_read_only_group" {
  name = "${module.s3_bucket.s3_bucket_name_output}-feed-drop-ro-users"
}

resource "aws_iam_group_membership" "feed_drop_read_only_group_membership" {
  name = "${module.s3_bucket.s3_bucket_name_output}-feed-drop-ro-users-membership"

  users = [
    "droberge"
  ]

  group = aws_iam_group.feed_drop_read_only_group.name
}

resource "aws_iam_group_policy_attachment" "feed_drop_read_only_group_policy_attachment" {
  group      = aws_iam_group.feed_drop_read_only_group.name
  policy_arn = data.aws_iam_policy.feed_drop_ro_existing_policy.arn
}
