AWSTemplateFormatVersion: '2010-09-09'
Description: >
  AWS Cost Optimization Report Generator - V1.
  Report-only serverless toolkit that scans for common cost-reduction opportunities
  and writes HTML, JSON, and CSV reports to S3.

Parameters:
  TemplateVersion:
    Type: String
    Default: v2026-07-24b-report-index-history-clienterror-fix
    Description: Human-readable template version marker used to verify the deployed template and generated reports.

  ForceLambdaUpdateToken:
    Type: String
    Default: v2026-07-24-v30
    Description: Optional deployment token. Change this value to force CloudFormation to update the Lambda function environment when report generator code or behavior changes.

  ProjectName:
    Type: String
    Default: aws-cost-optimization-report
    Description: Logical project name used for resource naming.

  ReportBucketName:
    Type: String
    Default: ''
    Description: >
      Optional existing/new globally unique S3 bucket name for reports. Leave blank
      to create a name based on account and region.

  ConfigPrefix:
    Type: String
    Default: config
    Description: S3 prefix where per-account config files are stored.

  ReportPrefix:
    Type: String
    Default: reports
    Description: S3 prefix where generated reports are written.

  RunReportOnDeploy:
    Type: String
    Default: 'true'
    AllowedValues:
      - 'true'
      - 'false'
    Description: Whether to generate an initial report during CloudFormation deployment or a forced stack update.

  RunReportToken:
    Type: String
    Default: v2
    Description: >
      Change this value when updating the stack if you want CloudFormation to force
      a new report run. Example values: v3, 2026-05-08-Run2, manual-rerun-001.

  EnableSchedule:
    Type: String
    Default: 'false'
    AllowedValues:
      - 'true'
      - 'false'
    Description: Whether to run the report Lambda automatically on a schedule.

  ScheduleExpression:
    Type: String
    Default: rate(7 days)
    Description: EventBridge schedule expression used when EnableSchedule is true.

  DefaultIdleDays:
    Type: Number
    Default: 30
    MinValue: 1
    MaxValue: 3650
    Description: Default idle/stale threshold in days used when no account config exists.

Conditions:
  UseProvidedBucketName: !Not [!Equals [!Ref ReportBucketName, '']]
  EnableScheduledRun: !Equals [!Ref EnableSchedule, 'true']
  RunInitialReport: !Equals [!Ref RunReportOnDeploy, 'true']

Resources:
  ReportBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !If
        - UseProvidedBucketName
        - !Ref ReportBucketName
        - !Sub '${ProjectName}-${AWS::AccountId}-${AWS::Region}'
      Tags:
        - Key: SECURITY_CONTACT_EMAIL
          Value: anthony.wilder@siemens.com
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: Enabled

  ReportBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref ReportBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          # Required company baseline policy statement.
          # Keep this statement in place. Add any future bucket-policy statements
          # as additional entries in this Statement array rather than replacing it.
          - Sid: MustBeEncryptedInTransit
            Effect: Deny
            Principal: '*'
            Action: 's3:*'
            Resource:
              - !Sub '${ReportBucket.Arn}'
              - !Sub '${ReportBucket.Arn}/*'
            Condition:
              Bool:
                aws:SecureTransport: 'false'

  CostReportLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ProjectName}-lambda-role-${AWS::Region}'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: !Sub '${ProjectName}-lambda-policy'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: CloudWatchLogs
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: '*'
              - Sid: ReportBucketAccess
                Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:ListBucket
                Resource:
                  - !Sub '${ReportBucket.Arn}'
                  - !Sub '${ReportBucket.Arn}/*'
              - Sid: InvokeSelfForInitialReport
                Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ProjectName}'
              - Sid: ReadOnlyInventory
                Effect: Allow
                Action:
                  - sts:GetCallerIdentity
                  - iam:ListAccountAliases
                  - ec2:DescribeRegions
                  - ec2:DescribeAddresses
                  - ec2:DescribeVolumes
                  - ec2:DescribeInstances
                  - ec2:DescribeTags
                  - ec2:DescribeNatGateways
                  - ec2:DescribeSnapshots
                  - ec2:DescribeImages
                  - elasticloadbalancing:DescribeLoadBalancers
                  - elasticloadbalancing:DescribeTargetGroups
                  - elasticloadbalancing:DescribeTargetHealth
                  - logs:DescribeLogGroups
                  - cloudwatch:GetMetricStatistics
                  - cloudwatch:GetMetricData
                  - lambda:ListFunctions
                  - lambda:ListTags
                  - rds:DescribeDBInstances
                  - rds:DescribeDBClusters
                  - rds:DescribeDBSnapshots
                  - rds:DescribeDBClusterSnapshots
                  - rds:ListTagsForResource
                  - s3:ListAllMyBuckets
                  - s3:GetBucketLocation
                  - s3:GetBucketVersioning
                  - s3:GetLifecycleConfiguration
                  - s3:GetBucketLocation
                  - s3:GetBucketVersioning
                  - s3:GetLifecycleConfiguration
                  - globalaccelerator:ListAccelerators
                  - globalaccelerator:ListListeners
                  - globalaccelerator:ListEndpointGroups
                  - globalaccelerator:DescribeAccelerator
                  - appstream:DescribeFleets
                  - appstream:DescribeStacks
                  - appstream:DescribeImageBuilders
                  - appstream:DescribeSessions
                  - backup:ListBackupVaults
                  - backup:DescribeBackupVault
                  - backup:ListRecoveryPointsByBackupVault
                  - backup:DescribeRecoveryPoint
                  - backup:ListProtectedResources
                  - tag:GetResources
                  - pricing:GetProducts
                  - ec2:DescribeInstanceTypes
                  - elasticfilesystem:DescribeFileSystems
                  - elasticfilesystem:DescribeLifecycleConfiguration
                  - elasticfilesystem:DescribeMountTargets
                  - elasticfilesystem:DescribeTags
                  - ecr:DescribeRepositories
                  - ecr:DescribeImages
                  - ecr:GetLifecyclePolicy
                  - dynamodb:ListTables
                  - dynamodb:DescribeTable
                  - dynamodb:DescribeContinuousBackups
                  - es:ListDomainNames
                  - es:DescribeDomain
                  - es:DescribeDomainConfig
                  - es:ListTags
                  - rds:DescribeDBInstances
                  - rds:DescribeDBSnapshots
                  - rds:DescribeDBClusters
                  - tag:GetResources
                  - elasticfilesystem:DescribeFileSystems
                  - elasticfilesystem:DescribeLifecycleConfiguration
                  - elasticfilesystem:DescribeMountTargets
                  - elasticfilesystem:DescribeTags
                  - ecr:DescribeRepositories
                  - ecr:DescribeImages
                  - ecr:GetLifecyclePolicy
                  - ecr:ListTagsForResource
                  - dynamodb:ListTables
                  - dynamodb:DescribeTable
                  - dynamodb:ListTagsOfResource
                  - es:ListDomainNames
                  - es:DescribeDomain
                  - es:ListTags
                  - cloudwatch:GetMetricStatistics
                Resource: '*'

  CostReportLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Ref ProjectName
      Description: AWS Cost Optimization Report Generator - report-only V1.
      Runtime: python3.12
      Handler: index.handler
      Timeout: 900
      MemorySize: 512
      Role: !GetAtt CostReportLambdaRole.Arn
      Environment:
        Variables:
          REPORT_BUCKET: !Ref ReportBucket
          CONFIG_PREFIX: !Ref ConfigPrefix
          REPORT_PREFIX: !Ref ReportPrefix
          DEFAULT_IDLE_DAYS: !Ref DefaultIdleDays
          PROJECT_NAME: !Ref ProjectName
          TEMPLATE_VERSION: !Ref TemplateVersion
          FORCE_LAMBDA_UPDATE_TOKEN: !Ref ForceLambdaUpdateToken
      Code:
        ZipFile: |
          import boto3
          import botocore
          from botocore.exceptions import ClientError
          import csv
          import datetime
          import io
          import json
          import os
          import re
          import traceback
          import urllib.request
          from html import escape

          REPORT_BUCKET = os.environ["REPORT_BUCKET"]
          CONFIG_PREFIX = os.environ.get("CONFIG_PREFIX", "config").strip("/")
          REPORT_PREFIX = os.environ.get("REPORT_PREFIX", "reports").strip("/")
          DEFAULT_IDLE_DAYS = int(os.environ.get("DEFAULT_IDLE_DAYS", "30"))
          PROJECT_NAME = os.environ.get("PROJECT_NAME", "aws-cost-optimization-report")
          TEMPLATE_VERSION = os.environ.get("TEMPLATE_VERSION", "unknown")
          TOOLKIT_VERSION = "0.5.1"
          TEMPLATE_BUILD = "2026-07-24-v30"

          s3 = boto3.client("s3")
          sts = boto3.client("sts")

          def utc_now():
              return datetime.datetime.now(datetime.timezone.utc)

          def iso_now():
              return utc_now().isoformat()

          def json_safe(value):
              if isinstance(value, (datetime.datetime, datetime.date)):
                  return value.isoformat()
              return str(value)

          def get_account_id():
              return sts.get_caller_identity()["Account"]

          def get_account_alias(warnings=None):
              try:
                  iam = boto3.client("iam")
                  aliases = iam.list_account_aliases().get("AccountAliases", [])
                  return aliases[0] if aliases else None
              except Exception as e:
                  if warnings is not None:
                      warnings.append({
                          "region": "global",
                          "service": "IAM",
                          "operation": "ListAccountAliases",
                          "resource": "account",
                          "error": str(e)
                      })
                  return None

          def default_config(account_id):
              return {
                  "account_id": account_id,
                  "account_name": account_id,
                  "environment": "",
                  "regions": "all-enabled",
                  "excluded_regions": ["ap-northeast-3"],
                  "service_region_exclusions": {
                      "efs_review": ["ap-northeast-3"],
                      "ecr_review": ["ap-northeast-3"],
                      "dynamodb_review": ["ap-northeast-3"],
                      "opensearch_review": ["ap-northeast-3"],
                      "aws_backup_review": ["ap-northeast-3"],
                      "resource_discovery_summary": ["ap-northeast-3"]
                  },
                  "mode": "ReportOnly",
                  "enabled_checks": {
                      "unused_eips": True,
                      "unattached_ebs": True,
                      "stopped_ec2_with_ebs": True,
                      "elb_review": True,
                      "cloudwatch_log_retention": True,
                      "large_cloudwatch_logs": True,
                      "ebs_gp2_conversion": True,
                      "nat_gateway_idle": True,
                      "lambda_no_invocations": True,
                      "snapshots_and_amis": True,
                      "s3_lifecycle_review": True,
                      "global_accelerator_review": True,
                      "appstream_review": True,
                      "aws_backup_review": True,
                      "aws_backup_copied_recovery_point_details": False,
                      "resource_discovery_review": True,
                      "efs_review": True,
                      "ecr_review": True,
                      "dynamodb_review": True,
                      "opensearch_review": True,
                      "resource_discovery_summary": True,
                      "ec2_low_utilization": False,
                      "ec2_modernization_rightsizing_review": True,
                      "ebs_low_activity": False,
                      "rds_basic_review": True
                  },
                  "thresholds": {
                      "idle_days": DEFAULT_IDLE_DAYS,
                      "stopped_instance_days": DEFAULT_IDLE_DAYS,
                      "old_unattached_volume_days": DEFAULT_IDLE_DAYS,
                      "large_s3_bucket_tib": 1,
                      "s3_metrics_lookback_days": 7,
                      "nat_gateway_metrics_lookback_days": 7,
                      "nat_gateway_low_bytes_daily_avg": 1048576,
                      "lambda_metrics_lookback_days": 30,
                      "lambda_low_invocations_threshold": 0,
                      "old_snapshot_days": 180,
                      "old_ami_days": 180,
                      "large_log_group_bytes": 1073741824,
                      "log_group_warning_gib": 5,
                      "log_group_critical_gib": 50,
                      "gp2_min_size_gib": 1,
                      "ec2_metrics_lookback_days": 14,
                      "ec2_low_cpu_average_percent": 5,
                      "ec2_low_cpu_max_percent": 15,
                      "ec2_low_network_bytes_daily_avg": 10485760,
                      "ec2_rightsizing_metrics_lookback_days": 14,
                      "ec2_rightsizing_low_cpu_average_percent": 10,
                      "ec2_rightsizing_low_cpu_max_percent": 25,
                      "ec2_rightsizing_min_monthly_savings_usd": 10,
                      "large_ebs_volume_gib": 100,
                      "ebs_metrics_lookback_days": 14,
                      "ebs_low_ops_daily_avg": 100,
                      "ebs_low_bytes_daily_avg": 10485760,
                      "rds_metrics_lookback_days": 14,
                      "rds_low_cpu_average_percent": 10,
                      "rds_low_connections_average": 2,
                      "rds_large_allocated_storage_gib": 500,
                      "rds_old_snapshot_days": 180,
                      "rds_clumio_airgap_review_days": 120,
                      "rds_long_backup_retention_days": 14,
                      "aws_backup_old_recovery_point_days": 365,
                      "aws_backup_large_vault_recovery_point_count": 100,
                      "aws_backup_copied_recovery_point_detail_min_age_days": 90,
                      "resource_discovery_min_uncovered_count": 1,
                      "efs_large_filesystem_gib": 100,
                      "efs_standard_storage_gib_threshold": 100,
                      "ecr_old_image_days": 180,
                      "ecr_large_repository_image_count": 100,
                      "ecr_untagged_image_count": 10,
                      "ecr_large_repository_storage_gib": 10,
                      "dynamodb_old_table_days": 180,
                      "dynamodb_large_table_gib": 10,
                      "dynamodb_low_activity_lookback_days": 14,
                      "dynamodb_low_activity_request_threshold": 10,
                      "opensearch_large_domain_storage_gib": 100,
                      "opensearch_old_domain_days": 180,
                      "opensearch_large_instance_count": 3
                  },
                  "exclusions": {
                      "resource_ids": [],
                      "tag_keys": ["CostOptIgnore", "DoNotDelete"],
                      "tag_pairs": {
                          "CostOptIgnore": "true",
                          "DoNotDelete": "true"
                      }
                  },
                  "cost_assumptions": {
                      "elastic_ip_unattached_monthly_usd": 3.65,
                      "gp3_gib_monthly_usd": 0.08,
                      "gp2_gib_monthly_usd": 0.10,
                      "io1_gib_monthly_usd": 0.125,
                      "io2_gib_monthly_usd": 0.125,
                      "st1_gib_monthly_usd": 0.045,
                      "sc1_gib_monthly_usd": 0.015,
                      "standard_gib_monthly_usd": 0.05,
                      "cloudwatch_logs_storage_gib_monthly_usd": 0.03,
                      "s3_standard_storage_gib_monthly_usd": 0.023,
                      "global_accelerator_monthly_baseline_usd": 18.25,
                      "elbv2_load_balancer_monthly_baseline_usd": 16.43,
                      "nat_gateway_monthly_baseline_usd": 32.85,
                      "ebs_snapshot_gib_monthly_usd": 0.05,
                      "rds_snapshot_gib_monthly_usd": 0.095,
                      "aws_backup_warm_storage_gib_monthly_usd": 0.05,
                      "efs_standard_gib_monthly_usd": 0.30,
                      "efs_ia_gib_monthly_usd": 0.025,
                      "ecr_storage_gib_monthly_usd": 0.10,
                      "dynamodb_standard_storage_gib_monthly_usd": 0.25,
                      "opensearch_gp3_gib_monthly_usd": 0.122,
                      "opensearch_gp2_gib_monthly_usd": 0.135,
                      "opensearch_instance_baseline_monthly_usd": 75.0,
                      "gp2_gib_monthly_usd": 0.10,
                      "gp3_gib_monthly_usd": 0.08
                  },
                  "notes": [
                      "This default config was generated automatically by the report Lambda.",
                      "All findings are report-only. No resources are modified."
                  ]
              }

          def load_or_create_config(account_id):
              key = f"{CONFIG_PREFIX}/{account_id}.json"
              try:
                  obj = s3.get_object(Bucket=REPORT_BUCKET, Key=key)
                  return json.loads(obj["Body"].read().decode("utf-8")), False
              except botocore.exceptions.ClientError as e:
                  if e.response.get("Error", {}).get("Code") not in ["NoSuchKey", "NoSuchBucket", "404"]:
                      print(f"WARN: Could not read config {key}: {e}")
                  cfg = default_config(account_id)
                  s3.put_object(
                      Bucket=REPORT_BUCKET,
                      Key=key,
                      Body=json.dumps(cfg, indent=2, default=json_safe).encode("utf-8"),
                      ContentType="application/json"
                  )
                  return cfg, True

          def get_enabled_regions(config):
              cfg_regions = config.get("regions", "all-enabled")
              excluded_regions = set(config.get("excluded_regions", []) or [])
              if isinstance(cfg_regions, list) and cfg_regions:
                  return [r for r in cfg_regions if r not in excluded_regions]
              ec2 = boto3.client("ec2")
              regions = ec2.describe_regions(AllRegions=False)["Regions"]
              return sorted([r["RegionName"] for r in regions if r["RegionName"] not in excluded_regions])

          def tags_to_dict(tags):
              if not tags:
                  return {}
              return {t.get("Key", ""): t.get("Value", "") for t in tags if t.get("Key")}

          def get_name_tag(tags):
              return tags_to_dict(tags).get("Name", "")

          def is_check_enabled(config, check_name, default=True):
              enabled_checks = config.get("enabled_checks", {})
              if not isinstance(enabled_checks, dict):
                  return default
              return bool(enabled_checks.get(check_name, default))

          def is_service_region_excluded(config, check_name, region):
              exclusions = config.get("service_region_exclusions", {}) or {}
              if not isinstance(exclusions, dict):
                  return False
              values = exclusions.get(check_name, []) or []
              return region in values or "*" in values

          def is_excluded(resource_id, tags, config):
              exclusions = config.get("exclusions", {})
              if resource_id in exclusions.get("resource_ids", []):
                  return True
              tag_dict = tags_to_dict(tags)
              for key in exclusions.get("tag_keys", []):
                  if key in tag_dict:
                      return True
              for key, expected in exclusions.get("tag_pairs", {}).items():
                  if str(tag_dict.get(key, "")).lower() == str(expected).lower():
                      return True
              return False

          def add_finding(findings, **kwargs):
              base = {
                  "finding_id": f"F-{len(findings)+1:05d}",
                  "timestamp": iso_now(),
                  "mode": "ReportOnly"
              }
              base.update(kwargs)
              findings.append(base)

          def estimate_volume_monthly(volume_type, size_gib, config):
              assumptions = config.get("cost_assumptions", {})
              key = f"{volume_type}_gib_monthly_usd"
              rate = assumptions.get(key, assumptions.get("gp3_gib_monthly_usd", 0.08))
              try:
                  return round(float(size_gib) * float(rate), 2)
              except Exception:
                  return None

          def estimate_cloudwatch_logs_storage_monthly(stored_bytes, config):
              assumptions = config.get("cost_assumptions", {})
              rate = assumptions.get("cloudwatch_logs_storage_gib_monthly_usd", 0.03)
              try:
                  gib = float(stored_bytes or 0) / 1024 / 1024 / 1024
                  return round(gib * float(rate), 2)
              except Exception:
                  return None

          def scan_ec2_region(region, config, findings, warnings):
              ec2 = boto3.client("ec2", region_name=region)
              thresholds = config.get("thresholds", {})
              stopped_days = int(thresholds.get("stopped_instance_days", DEFAULT_IDLE_DAYS))
              unattached_days = int(thresholds.get("old_unattached_volume_days", DEFAULT_IDLE_DAYS))
              now = utc_now()

              # Unused Elastic IPs
              try:
                  addresses = ec2.describe_addresses().get("Addresses", [])
                  for addr in addresses:
                      allocation_id = addr.get("AllocationId", "")
                      public_ip = addr.get("PublicIp", "")
                      tags = addr.get("Tags", [])
                      if is_excluded(allocation_id or public_ip, tags, config):
                          continue
                      if not addr.get("AssociationId"):
                          monthly = config.get("cost_assumptions", {}).get("elastic_ip_unattached_monthly_usd", 3.65)
                          add_finding(
                              findings,
                              category="Unused Elastic IP",
                              confidence="High",
                              severity="Medium",
                              region=region,
                              service="EC2",
                              resource_id=allocation_id or public_ip,
                              resource_name=get_name_tag(tags),
                              estimated_monthly_savings_usd=monthly,
                              evidence=f"Elastic IP {public_ip} is allocated but has no AssociationId.",
                              recommendation="Release this Elastic IP if it is no longer needed.",
                              remediation_risk="Medium - confirm DNS, allow lists, vendor references, and documentation before release."
                          )
              except Exception as e:
                  warnings.append({"region": region, "service": "EC2", "operation": "DescribeAddresses", "error": str(e)})

              # Unattached EBS Volumes
              try:
                  paginator = ec2.get_paginator("describe_volumes")
                  for page in paginator.paginate(Filters=[{"Name": "status", "Values": ["available"]}]):
                      for vol in page.get("Volumes", []):
                          volume_id = vol.get("VolumeId")
                          tags = vol.get("Tags", [])
                          if is_excluded(volume_id, tags, config):
                              continue
                          create_time = vol.get("CreateTime")
                          age_days = None
                          if create_time:
                              age_days = (now - create_time).days
                          if age_days is None or age_days >= unattached_days:
                              size_gib = vol.get("Size", 0)
                              volume_type = vol.get("VolumeType", "unknown")
                              add_finding(
                                  findings,
                                  category="Unattached EBS Volume",
                                  confidence="High",
                                  severity="Medium",
                                  region=region,
                                  service="EBS",
                                  resource_id=volume_id,
                                  resource_name=get_name_tag(tags),
                                  estimated_monthly_savings_usd=estimate_volume_monthly(volume_type, size_gib, config),
                                  evidence=f"Volume is in available state, size={size_gib} GiB, type={volume_type}, age_days={age_days}.",
                                  recommendation="Snapshot for retention if needed, then delete the unattached volume after owner approval.",
                                  remediation_risk="Medium - verify backup, restore, application, and compliance requirements before deletion."
                              )
              except Exception as e:
                  warnings.append({"region": region, "service": "EBS", "operation": "DescribeVolumes", "error": str(e)})

              # Stopped EC2 Instances with Attached EBS
              try:
                  paginator = ec2.get_paginator("describe_instances")
                  for page in paginator.paginate(Filters=[{"Name": "instance-state-name", "Values": ["stopped"]}]):
                      for reservation in page.get("Reservations", []):
                          for inst in reservation.get("Instances", []):
                              instance_id = inst.get("InstanceId")
                              tags = inst.get("Tags", [])
                              if is_excluded(instance_id, tags, config):
                                  continue
                              block_devices = inst.get("BlockDeviceMappings", [])
                              volume_ids = [b.get("Ebs", {}).get("VolumeId") for b in block_devices if b.get("Ebs", {}).get("VolumeId")]
                              state_transition = inst.get("StateTransitionReason", "")
                              if volume_ids:
                                  add_finding(
                                      findings,
                                      category="Stopped EC2 Instance With Attached EBS",
                                      confidence="Medium",
                                      severity="Low",
                                      region=region,
                                      service="EC2/EBS",
                                      resource_id=instance_id,
                                      resource_name=get_name_tag(tags),
                                      estimated_monthly_savings_usd=None,
                                      evidence=f"Instance is stopped and has attached EBS volumes: {', '.join(volume_ids)}. State transition: {state_transition}",
                                      recommendation="Confirm whether the instance is still needed. If not, snapshot/AMI as required and terminate/delete associated volumes through a controlled process.",
                                      remediation_risk="Medium - stopped instances may be retained intentionally for rollback, audit, or seasonal use."
                                  )
              except Exception as e:
                  warnings.append({"region": region, "service": "EC2", "operation": "DescribeInstances", "error": str(e)})

          def estimate_snapshot_monthly(size_gib, config):
              assumptions = config.get("cost_assumptions", {})
              rate = assumptions.get("ebs_snapshot_gib_monthly_usd", 0.05)
              try:
                  return round(float(size_gib or 0) * float(rate), 2)
              except Exception:
                  return None

          def estimate_rds_snapshot_monthly(size_gib, config):
              assumptions = config.get("cost_assumptions", {})
              rate = assumptions.get("rds_snapshot_gib_monthly_usd", 0.095)
              try:
                  return round(float(size_gib or 0) * float(rate), 2)
              except Exception:
                  return None

          def get_cloudwatch_stat(cloudwatch, namespace, metric_name, dimensions, lookback_days, statistic="Sum", period=86400):
              end = utc_now()
              start = end - datetime.timedelta(days=lookback_days)
              response = cloudwatch.get_metric_statistics(
                  Namespace=namespace,
                  MetricName=metric_name,
                  Dimensions=dimensions,
                  StartTime=start,
                  EndTime=end,
                  Period=period,
                  Statistics=[statistic]
              )
              datapoints = response.get("Datapoints", [])
              if not datapoints:
                  return None
              values = [float(dp.get(statistic, 0) or 0) for dp in datapoints]
              if statistic == "Sum":
                  return sum(values)
              return sum(values) / len(values) if values else None

          def scan_nat_gateways_region(region, config, findings, warnings):
              ec2 = boto3.client("ec2", region_name=region)
              cloudwatch = boto3.client("cloudwatch", region_name=region)
              thresholds = config.get("thresholds", {})
              lookback_days = int(thresholds.get("nat_gateway_metrics_lookback_days", 7))
              low_bytes_daily_avg = float(thresholds.get("nat_gateway_low_bytes_daily_avg", 1048576))
              baseline = config.get("cost_assumptions", {}).get("nat_gateway_monthly_baseline_usd", 32.85)

              try:
                  paginator = ec2.get_paginator("describe_nat_gateways")
                  for page in paginator.paginate():
                      for ngw in page.get("NatGateways", []):
                          nat_id = ngw.get("NatGatewayId")
                          state = ngw.get("State", "unknown")
                          tags = ngw.get("Tags", [])
                          if is_excluded(nat_id, tags, config):
                              continue
                          if state not in ["available", "pending"]:
                              continue

                          dimensions = [{"Name": "NatGatewayId", "Value": nat_id}]
                          bytes_in = None
                          bytes_out = None
                          try:
                              bytes_in = get_cloudwatch_stat(cloudwatch, "AWS/NATGateway", "BytesInFromSource", dimensions, lookback_days, "Sum")
                              bytes_out = get_cloudwatch_stat(cloudwatch, "AWS/NATGateway", "BytesOutToDestination", dimensions, lookback_days, "Sum")
                          except Exception as e:
                              warnings.append({"region": region, "service": "NAT Gateway/CloudWatch", "operation": "GetMetricStatistics", "resource": nat_id, "error": str(e)})

                          total_bytes = None
                          daily_avg = None
                          if bytes_in is not None or bytes_out is not None:
                              total_bytes = float(bytes_in or 0) + float(bytes_out or 0)
                              daily_avg = total_bytes / max(lookback_days, 1)

                          if total_bytes is None or daily_avg <= low_bytes_daily_avg:
                              add_finding(
                                  findings,
                                  category="Idle NAT Gateway Review",
                                  confidence="Medium",
                                  severity="Medium",
                                  region=region,
                                  service="VPC/NAT Gateway",
                                  resource_id=nat_id,
                                  resource_name=get_name_tag(tags),
                                  estimated_monthly_savings_usd=baseline,
                                  evidence=f"state={state}; vpc={ngw.get('VpcId')}; subnet={ngw.get('SubnetId')}; lookback_days={lookback_days}; total_bytes={total_bytes}; daily_avg_bytes={daily_avg}",
                                  recommendation="Review whether this NAT Gateway is still required. If traffic is consistently near zero, consider deleting it or replacing the architecture with VPC endpoints where appropriate.",
                                  remediation_risk="High - deleting a NAT Gateway can break outbound private subnet connectivity. Confirm route tables, workloads, patching, and vendor access first."
                              )
              except Exception as e:
                  warnings.append({"region": region, "service": "VPC/NAT Gateway", "operation": "DescribeNatGateways", "error": str(e)})

          def scan_snapshots_and_amis_region(region, config, findings, warnings):
              ec2 = boto3.client("ec2", region_name=region)
              thresholds = config.get("thresholds", {})
              old_snapshot_days = int(thresholds.get("old_snapshot_days", 180))
              old_ami_days = int(thresholds.get("old_ami_days", 180))
              now = utc_now()
              ami_snapshot_ids = set()

              try:
                  images = ec2.describe_images(Owners=["self"]).get("Images", [])
                  for image in images:
                      image_id = image.get("ImageId")
                      tags = image.get("Tags", [])
                      if is_excluded(image_id, tags, config):
                          continue
                      snapshot_ids = []
                      for mapping in image.get("BlockDeviceMappings", []):
                          snap_id = mapping.get("Ebs", {}).get("SnapshotId")
                          if snap_id:
                              snapshot_ids.append(snap_id)
                              ami_snapshot_ids.add(snap_id)
                      created = image.get("CreationDate")
                      age_days = None
                      if created:
                          try:
                              created_dt = datetime.datetime.fromisoformat(created.replace("Z", "+00:00"))
                              age_days = (now - created_dt).days
                          except Exception:
                              pass
                      if age_days is not None and age_days >= old_ami_days:
                          add_finding(
                              findings,
                              category="Old AMI Review",
                              confidence="Medium",
                              severity="Low",
                              region=region,
                              service="EC2/AMI",
                              resource_id=image_id,
                              resource_name=image.get("Name", get_name_tag(tags)),
                              estimated_monthly_savings_usd=None,
                              evidence=f"ami_name={image.get('Name')}; age_days={age_days}; state={image.get('State')}; backing_snapshots={', '.join(snapshot_ids)}",
                              recommendation="Review whether this AMI is still needed. If retired, deregister the AMI and then review/delete associated snapshots through an approved process.",
                              remediation_risk="Medium - old AMIs may be needed for rollback, compliance, golden image history, or disaster recovery."
                          )
              except Exception as e:
                  warnings.append({"region": region, "service": "EC2/AMI", "operation": "DescribeImages", "error": str(e)})

              try:
                  paginator = ec2.get_paginator("describe_snapshots")
                  for page in paginator.paginate(OwnerIds=["self"]):
                      for snap in page.get("Snapshots", []):
                          snapshot_id = snap.get("SnapshotId")
                          tags = snap.get("Tags", [])
                          if is_excluded(snapshot_id, tags, config):
                              continue
                          start_time = snap.get("StartTime")
                          age_days = None
                          if start_time:
                              age_days = (now - start_time).days
                          if age_days is not None and age_days >= old_snapshot_days and snapshot_id not in ami_snapshot_ids:
                              size_gib = snap.get("VolumeSize", 0)
                              add_finding(
                                  findings,
                                  category="Old Unassociated EBS Snapshot Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="EBS Snapshot",
                                  resource_id=snapshot_id,
                                  resource_name=get_name_tag(tags),
                                  estimated_monthly_savings_usd=estimate_snapshot_monthly(size_gib, config),
                                  evidence=f"age_days={age_days}; volume_size_gib={size_gib}; state={snap.get('State')}; not associated with currently owned AMI backing snapshots.",
                                  recommendation="Review whether this snapshot is still needed for backup, compliance, or rollback. Estimate is a gross review/exposure indicator and may not equal standalone billable snapshot savings. Delete only after retention approval.",
                                  remediation_risk="Medium - snapshots may be retained for compliance, audit, recovery, or manual rollback."
                              )
              except Exception as e:
                  warnings.append({"region": region, "service": "EBS Snapshot", "operation": "DescribeSnapshots", "error": str(e)})

          def scan_lambda_region(region, config, findings, warnings):
              lambda_client = boto3.client("lambda", region_name=region)
              cloudwatch = boto3.client("cloudwatch", region_name=region)
              thresholds = config.get("thresholds", {})
              lookback_days = int(thresholds.get("lambda_metrics_lookback_days", 30))
              low_invocations = float(thresholds.get("lambda_low_invocations_threshold", 0))

              try:
                  paginator = lambda_client.get_paginator("list_functions")
                  for page in paginator.paginate():
                      for fn in page.get("Functions", []):
                          name = fn.get("FunctionName")
                          arn = fn.get("FunctionArn")
                          tags = {}
                          try:
                              tags = lambda_client.list_tags(Resource=arn).get("Tags", {})
                          except Exception as e:
                              warnings.append({"region": region, "service": "Lambda", "operation": "ListTags", "resource": name, "error": str(e)})
                          tag_list = [{"Key": k, "Value": v} for k, v in tags.items()]
                          if is_excluded(name, tag_list, config) or is_excluded(arn, tag_list, config):
                              continue

                          invocations = None
                          errors = None
                          dimensions = [{"Name": "FunctionName", "Value": name}]
                          try:
                              invocations = get_cloudwatch_stat(cloudwatch, "AWS/Lambda", "Invocations", dimensions, lookback_days, "Sum")
                              errors = get_cloudwatch_stat(cloudwatch, "AWS/Lambda", "Errors", dimensions, lookback_days, "Sum")
                          except Exception as e:
                              warnings.append({"region": region, "service": "Lambda/CloudWatch", "operation": "GetMetricStatistics", "resource": name, "error": str(e)})

                          if invocations is None or invocations <= low_invocations:
                              add_finding(
                                  findings,
                                  category="Lambda No Invocations Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="Lambda",
                                  resource_id=arn,
                                  resource_name=name,
                                  estimated_monthly_savings_usd=None,
                                  evidence=f"runtime={fn.get('Runtime')}; memory_mb={fn.get('MemorySize')}; last_modified={fn.get('LastModified')}; lookback_days={lookback_days}; invocations={invocations}; errors={errors}",
                                  recommendation="Review whether this function is still required. Check triggers, EventBridge rules, aliases, and deployment pipelines before removal.",
                                  remediation_risk="Medium - inactive Lambda functions may be break-glass tools, custom resources, or infrequent scheduled jobs."
                              )
              except Exception as e:
                  warnings.append({"region": region, "service": "Lambda", "operation": "ListFunctions", "error": str(e)})

          def get_cloudwatch_values(cloudwatch, namespace, metric_name, dimensions, lookback_days, statistics, period=86400):
              end = utc_now()
              start = end - datetime.timedelta(days=lookback_days)
              response = cloudwatch.get_metric_statistics(
                  Namespace=namespace,
                  MetricName=metric_name,
                  Dimensions=dimensions,
                  StartTime=start,
                  EndTime=end,
                  Period=period,
                  Statistics=statistics
              )
              return response.get("Datapoints", [])

          def metric_sum(datapoints, statistic):
              values = [float(dp.get(statistic, 0) or 0) for dp in datapoints]
              return sum(values) if values else None

          def metric_avg(datapoints, statistic):
              values = [float(dp.get(statistic, 0) or 0) for dp in datapoints]
              return (sum(values) / len(values)) if values else None

          def metric_max(datapoints, statistic):
              values = [float(dp.get(statistic, 0) or 0) for dp in datapoints]
              return max(values) if values else None

          def rds_tags_to_list(rds_client, arn, warnings, region):
              try:
                  tags = rds_client.list_tags_for_resource(ResourceName=arn).get("TagList", [])
                  return [{"Key": t.get("Key", ""), "Value": t.get("Value", "")} for t in tags]
              except Exception as e:
                  warnings.append({"region": region, "service": "RDS", "operation": "ListTagsForResource", "resource": arn, "error": str(e)})
                  return []

          def scan_rds_region(region, config, findings, warnings):
              rds = boto3.client("rds", region_name=region)
              cloudwatch = boto3.client("cloudwatch", region_name=region)
              thresholds = config.get("thresholds", {})
              lookback_days = int(thresholds.get("rds_metrics_lookback_days", 14))
              low_cpu_avg = float(thresholds.get("rds_low_cpu_average_percent", 10))
              low_conn_avg = float(thresholds.get("rds_low_connections_average", 2))
              large_storage_gib = int(thresholds.get("rds_large_allocated_storage_gib", 500))
              old_snapshot_days = int(thresholds.get("rds_old_snapshot_days", 180))
              clumio_airgap_review_days = int(thresholds.get("rds_clumio_airgap_review_days", 120))
              long_backup_days = int(thresholds.get("rds_long_backup_retention_days", 14))
              now = utc_now()

              try:
                  paginator = rds.get_paginator("describe_db_instances")
                  for page in paginator.paginate():
                      for db in page.get("DBInstances", []):
                          dbid = db.get("DBInstanceIdentifier")
                          arn = db.get("DBInstanceArn")
                          tags = rds_tags_to_list(rds, arn, warnings, region) if arn else []
                          if is_excluded(dbid, tags, config) or (arn and is_excluded(arn, tags, config)):
                              continue

                          status = db.get("DBInstanceStatus")
                          engine = db.get("Engine")
                          db_class = db.get("DBInstanceClass")
                          allocated = int(db.get("AllocatedStorage", 0) or 0)
                          multi_az = bool(db.get("MultiAZ"))
                          backup_retention = int(db.get("BackupRetentionPeriod", 0) or 0)
                          pi_enabled = bool(db.get("PerformanceInsightsEnabled", False))

                          if pi_enabled:
                              add_finding(
                                  findings,
                                  category="RDS Performance Insights / Database Insights Visibility Review",
                                  confidence="High",
                                  severity="Low",
                                  region=region,
                                  service="RDS",
                                  resource_id=dbid,
                                  resource_name=dbid,
                                  estimated_monthly_savings_usd=None,
                                  evidence=f"engine={engine}; class={db_class}; status={status}; PerformanceInsightsEnabled=True; retention_days={db.get('PerformanceInsightsRetentionPeriod')}.",
                                  recommendation="Performance Insights / Database Insights is enabled. This may be appropriate for production databases. Review whether the observability mode and retention period match the database environment, troubleshooting needs, and cost expectations. Do not disable blindly.",
                                  remediation_risk="Low to Medium - performance telemetry can be important during incidents, troubleshooting, and performance baselining. Confirm with the application/database owner first."
                              )

                          if status == "stopped":
                              add_finding(
                                  findings,
                                  category="Stopped RDS Instance Review",
                                  confidence="High",
                                  severity="Medium",
                                  region=region,
                                  service="RDS",
                                  resource_id=dbid,
                                  resource_name=dbid,
                                  estimated_monthly_savings_usd=None,
                                  evidence=f"engine={engine}; class={db_class}; allocated_storage_gib={allocated}; status=stopped.",
                                  recommendation="Review whether this stopped RDS instance is still required. Stopped RDS instances can restart automatically after the AWS stop limit; consider snapshot/export and deletion through an approved process if retired.",
                                  remediation_risk="High - RDS deletion is destructive without proper snapshots, final snapshot decisions, owner approval, and recovery validation."
                              )

                          if multi_az:
                              add_finding(
                                  findings,
                                  category="RDS Multi-AZ Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="RDS",
                                  resource_id=dbid,
                                  resource_name=dbid,
                                  estimated_monthly_savings_usd=None,
                                  evidence=f"engine={engine}; class={db_class}; status={status}; MultiAZ=True; environment={config.get('environment') or 'unknown'}.",
                                  recommendation="Review whether Multi-AZ is required for this workload and environment. It is often appropriate for production but may be unnecessary for some dev/test workloads.",
                                  remediation_risk="High - disabling Multi-AZ reduces availability and changes failure behavior. Confirm environment criticality and recovery requirements first."
                              )

                          if backup_retention > long_backup_days:
                              add_finding(
                                  findings,
                                  category="RDS Backup Retention Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="RDS",
                                  resource_id=dbid,
                                  resource_name=dbid,
                                  estimated_monthly_savings_usd=None,
                                  evidence=f"engine={engine}; class={db_class}; backup_retention_days={backup_retention}; threshold_days={long_backup_days}.",
                                  recommendation="Review backup retention against compliance, recovery, and environment requirements. Reduce retention only with owner and compliance approval.",
                                  remediation_risk="Medium - reducing backup retention can remove restore points needed for recovery, audit, or investigation."
                              )

                          if allocated >= large_storage_gib:
                              add_finding(
                                  findings,
                                  category="RDS Large Allocated Storage Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="RDS",
                                  resource_id=dbid,
                                  resource_name=dbid,
                                  estimated_monthly_savings_usd=None,
                                  evidence=f"engine={engine}; class={db_class}; allocated_storage_gib={allocated}; storage_type={db.get('StorageType')}; threshold_gib={large_storage_gib}.",
                                  recommendation="Review allocated storage, free storage, growth trends, and backup/snapshot retention. Storage reductions generally require migration or rebuild planning.",
                                  remediation_risk="Medium - RDS storage cannot always be reduced in place. Validate data growth, free space, and migration options."
                              )

                          if status == "available":
                              dimensions = [{"Name": "DBInstanceIdentifier", "Value": dbid}]
                              try:
                                  cpu_points = get_cloudwatch_values(cloudwatch, "AWS/RDS", "CPUUtilization", dimensions, lookback_days, ["Average", "Maximum"])
                                  conn_points = get_cloudwatch_values(cloudwatch, "AWS/RDS", "DatabaseConnections", dimensions, lookback_days, ["Average", "Maximum"])
                                  cpu_avg = metric_avg(cpu_points, "Average")
                                  cpu_max = metric_max(cpu_points, "Maximum")
                                  conn_avg = metric_avg(conn_points, "Average")
                                  conn_max = metric_max(conn_points, "Maximum")
                                  if cpu_avg is not None and conn_avg is not None and cpu_avg <= low_cpu_avg and conn_avg <= low_conn_avg:
                                      add_finding(
                                          findings,
                                          category="RDS Low Utilization Review",
                                          confidence="Medium",
                                          severity="Low",
                                          region=region,
                                          service="RDS",
                                          resource_id=dbid,
                                          resource_name=dbid,
                                          estimated_monthly_savings_usd=None,
                                          evidence=f"engine={engine}; class={db_class}; lookback_days={lookback_days}; cpu_avg={round(cpu_avg, 2)}%; cpu_max={round(cpu_max or 0, 2)}%; connections_avg={round(conn_avg, 2)}; connections_max={round(conn_max or 0, 2)}.",
                                          recommendation="Review this DB instance for rightsizing, scheduling, consolidation, or retirement. Validate workload patterns, batch windows, connection pooling, and application owner requirements before making changes.",
                                          remediation_risk="High - database changes can affect availability, performance, licensing, and recovery. Use database metrics and owner approval before resizing or stopping."
                                      )
                              except Exception as e:
                                  warnings.append({"region": region, "service": "RDS/CloudWatch", "operation": "GetMetricStatistics", "resource": dbid, "error": str(e)})
              except Exception as e:
                  warnings.append({"region": region, "service": "RDS", "operation": "DescribeDBInstances", "error": str(e)})

              try:
                  paginator = rds.get_paginator("describe_db_snapshots")
                  for page in paginator.paginate(SnapshotType="manual"):
                      for snap in page.get("DBSnapshots", []):
                          snap_id = snap.get("DBSnapshotIdentifier")
                          arn = snap.get("DBSnapshotArn")
                          tags = rds_tags_to_list(rds, arn, warnings, region) if arn else []
                          if is_excluded(snap_id, tags, config) or (arn and is_excluded(arn, tags, config)):
                              continue
                          created = snap.get("SnapshotCreateTime")
                          age_days = (now - created).days if created else None
                          if age_days is not None and age_days >= old_snapshot_days:
                              add_finding(
                                  findings,
                                  category="Old Manual RDS Snapshot Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="RDS Snapshot",
                                  resource_id=snap_id,
                                  resource_name=snap_id,
                                  estimated_monthly_savings_usd=estimate_rds_snapshot_monthly(snap.get('AllocatedStorage'), config),
                                  evidence=f"db_instance={snap.get('DBInstanceIdentifier')}; engine={snap.get('Engine')}; allocated_storage_gib={snap.get('AllocatedStorage')}; estimated_snapshot_monthly=${estimate_rds_snapshot_monthly(snap.get('AllocatedStorage'), config)}; age_days={age_days}; status={snap.get('Status')}.",
                                  recommendation="Review whether this manual RDS snapshot is still required for compliance, recovery, rollback, or audit. Estimate is a gross allocated-storage exposure indicator; RDS snapshot billing is incremental and may overlap with backup/restore-test findings. Delete only after retention approval.",
                                  remediation_risk="Medium - snapshots may be retained intentionally for compliance, audit, migration, or rollback."
                              )
              except Exception as e:
                  warnings.append({"region": region, "service": "RDS Snapshot", "operation": "DescribeDBSnapshots", "error": str(e)})

          def scan_ec2_low_utilization_region(region, config, findings, warnings):
              ec2 = boto3.client("ec2", region_name=region)
              cloudwatch = boto3.client("cloudwatch", region_name=region)
              thresholds = config.get("thresholds", {})
              lookback_days = int(thresholds.get("ec2_metrics_lookback_days", 14))
              low_cpu_avg = float(thresholds.get("ec2_low_cpu_average_percent", 5))
              low_cpu_max = float(thresholds.get("ec2_low_cpu_max_percent", 15))
              low_network_daily = float(thresholds.get("ec2_low_network_bytes_daily_avg", 10485760))

              try:
                  paginator = ec2.get_paginator("describe_instances")
                  for page in paginator.paginate(Filters=[{"Name": "instance-state-name", "Values": ["running"]}]):
                      for reservation in page.get("Reservations", []):
                          for inst in reservation.get("Instances", []):
                              instance_id = inst.get("InstanceId")
                              tags = inst.get("Tags", [])
                              if is_excluded(instance_id, tags, config):
                                  continue

                              dimensions = [{"Name": "InstanceId", "Value": instance_id}]
                              try:
                                  cpu_points = get_cloudwatch_values(cloudwatch, "AWS/EC2", "CPUUtilization", dimensions, lookback_days, ["Average", "Maximum"])
                                  net_in_points = get_cloudwatch_values(cloudwatch, "AWS/EC2", "NetworkIn", dimensions, lookback_days, ["Sum"])
                                  net_out_points = get_cloudwatch_values(cloudwatch, "AWS/EC2", "NetworkOut", dimensions, lookback_days, ["Sum"])
                              except Exception as e:
                                  warnings.append({"region": region, "service": "EC2/CloudWatch", "operation": "GetMetricStatistics", "resource": instance_id, "error": str(e)})
                                  continue

                              cpu_avg = metric_avg(cpu_points, "Average")
                              cpu_peak = metric_max(cpu_points, "Maximum")
                              net_in = metric_sum(net_in_points, "Sum") or 0
                              net_out = metric_sum(net_out_points, "Sum") or 0
                              network_daily_avg = (net_in + net_out) / max(lookback_days, 1)

                              if cpu_avg is None or cpu_peak is None:
                                  continue

                              if cpu_avg <= low_cpu_avg and cpu_peak <= low_cpu_max and network_daily_avg <= low_network_daily:
                                  add_finding(
                                      findings,
                                      category="Low Utilization EC2 Instance Review",
                                      confidence="Medium",
                                      severity="Low",
                                      region=region,
                                      service="EC2",
                                      resource_id=instance_id,
                                      resource_name=get_name_tag(tags),
                                      estimated_monthly_savings_usd=None,
                                      evidence=f"instance_type={inst.get('InstanceType')}; lookback_days={lookback_days}; cpu_avg={round(cpu_avg, 2)}%; cpu_max={round(cpu_peak, 2)}%; network_daily_avg_bytes={round(network_daily_avg, 2)}; launch_time={inst.get('LaunchTime')}.",
                                      recommendation="Review this running instance for rightsizing, scheduled start/stop, consolidation, or termination if no longer needed. Confirm owner, workload role, monitoring coverage, backup, and maintenance window before making changes.",
                                      remediation_risk="Medium - low CloudWatch utilization can be normal for standby, batch, management, licensing, or bursty workloads. Use Compute Optimizer and application knowledge before resizing or stopping."
                                  )
              except Exception as e:
                  warnings.append({"region": region, "service": "EC2", "operation": "DescribeInstances running", "error": str(e)})

          def scan_ebs_low_activity_region(region, config, findings, warnings):
              ec2 = boto3.client("ec2", region_name=region)
              cloudwatch = boto3.client("cloudwatch", region_name=region)
              thresholds = config.get("thresholds", {})
              min_size_gib = int(thresholds.get("large_ebs_volume_gib", 100))
              lookback_days = int(thresholds.get("ebs_metrics_lookback_days", 14))
              low_ops_daily = float(thresholds.get("ebs_low_ops_daily_avg", 100))
              low_bytes_daily = float(thresholds.get("ebs_low_bytes_daily_avg", 10485760))

              try:
                  paginator = ec2.get_paginator("describe_volumes")
                  for page in paginator.paginate():
                      for vol in page.get("Volumes", []):
                          volume_id = vol.get("VolumeId")
                          tags = vol.get("Tags", [])
                          if is_excluded(volume_id, tags, config):
                              continue

                          size_gib = int(vol.get("Size", 0) or 0)
                          if size_gib < min_size_gib or vol.get("State") != "in-use":
                              continue

                          dimensions = [{"Name": "VolumeId", "Value": volume_id}]
                          try:
                              read_ops_points = get_cloudwatch_values(cloudwatch, "AWS/EBS", "VolumeReadOps", dimensions, lookback_days, ["Sum"])
                              write_ops_points = get_cloudwatch_values(cloudwatch, "AWS/EBS", "VolumeWriteOps", dimensions, lookback_days, ["Sum"])
                              read_bytes_points = get_cloudwatch_values(cloudwatch, "AWS/EBS", "VolumeReadBytes", dimensions, lookback_days, ["Sum"])
                              write_bytes_points = get_cloudwatch_values(cloudwatch, "AWS/EBS", "VolumeWriteBytes", dimensions, lookback_days, ["Sum"])
                          except Exception as e:
                              warnings.append({"region": region, "service": "EBS/CloudWatch", "operation": "GetMetricStatistics", "resource": volume_id, "error": str(e)})
                              continue

                          total_ops = (metric_sum(read_ops_points, "Sum") or 0) + (metric_sum(write_ops_points, "Sum") or 0)
                          total_bytes = (metric_sum(read_bytes_points, "Sum") or 0) + (metric_sum(write_bytes_points, "Sum") or 0)
                          ops_daily_avg = total_ops / max(lookback_days, 1)
                          bytes_daily_avg = total_bytes / max(lookback_days, 1)

                          if ops_daily_avg <= low_ops_daily and bytes_daily_avg <= low_bytes_daily:
                              attachments = vol.get("Attachments", [])
                              attached_to = ", ".join([a.get("InstanceId", "unknown") for a in attachments]) if attachments else "unknown"
                              add_finding(
                                  findings,
                                  category="Low Activity EBS Volume Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="EBS",
                                  resource_id=volume_id,
                                  resource_name=get_name_tag(tags),
                                  estimated_monthly_savings_usd=estimate_volume_monthly(vol.get("VolumeType", "unknown"), size_gib, config),
                                  evidence=f"volume_type={vol.get('VolumeType')}; size_gib={size_gib}; attached_to={attached_to}; lookback_days={lookback_days}; ops_daily_avg={round(ops_daily_avg, 2)}; bytes_daily_avg={round(bytes_daily_avg, 2)}.",
                                  recommendation="Review whether this large attached volume is oversized, stale, or storing data that can be archived. Confirm filesystem usage from inside the instance before resizing, snapshotting, or deleting.",
                                  remediation_risk="Medium - attached volumes may hold infrequently accessed but important application, audit, backup, or recovery data. Validate at OS/application level before changing."
                              )
              except Exception as e:
                  warnings.append({"region": region, "service": "EBS", "operation": "DescribeVolumes low activity", "error": str(e)})

          def scan_ebs_gp2_conversion_region(region, config, findings, warnings):
              ec2 = boto3.client("ec2", region_name=region)
              thresholds = config.get("thresholds", {})
              min_size_gib = int(thresholds.get("gp2_min_size_gib", 1))
              assumptions = config.get("cost_assumptions", {})
              gp2_rate = float(assumptions.get("gp2_gib_monthly_usd", 0.10))
              gp3_rate = float(assumptions.get("gp3_gib_monthly_usd", 0.08))

              try:
                  paginator = ec2.get_paginator("describe_volumes")
                  for page in paginator.paginate(Filters=[{"Name": "volume-type", "Values": ["gp2"]}]):
                      for vol in page.get("Volumes", []):
                          volume_id = vol.get("VolumeId")
                          tags = vol.get("Tags", [])
                          if is_excluded(volume_id, tags, config):
                              continue

                          size_gib = int(vol.get("Size", 0) or 0)
                          if size_gib < min_size_gib:
                              continue

                          attachments = vol.get("Attachments", [])
                          attached_to = ", ".join([a.get("InstanceId", "unknown") for a in attachments]) if attachments else "unattached"
                          monthly_delta = round(size_gib * max(gp2_rate - gp3_rate, 0), 2)

                          add_finding(
                              findings,
                              category="EBS gp2 Volume Conversion Review",
                              confidence="High",
                              severity="Low",
                              region=region,
                              service="EBS",
                              resource_id=volume_id,
                              resource_name=get_name_tag(tags),
                              estimated_monthly_savings_usd=monthly_delta,
                              evidence=f"volume_type=gp2; size_gib={size_gib}; state={vol.get('State')}; attached_to={attached_to}; estimated_gp2_monthly=${round(size_gib * gp2_rate, 2)}; estimated_gp3_monthly=${round(size_gib * gp3_rate, 2)}.",
                              recommendation="Review this gp2 volume as a candidate for gp3 conversion. Validate IOPS/throughput requirements and change-management requirements before modifying the volume type.",
                              remediation_risk="Low to Medium - gp3 is often cheaper and more flexible, but verify workload performance, provisioned IOPS/throughput needs, and application maintenance windows."
                          )
              except Exception as e:
                  warnings.append({"region": region, "service": "EBS", "operation": "DescribeVolumes gp2", "error": str(e)})

          def scan_elb_region(region, config, findings, warnings):
              elbv2 = boto3.client("elbv2", region_name=region)
              lb_baseline = config.get("cost_assumptions", {}).get("elbv2_load_balancer_monthly_baseline_usd", 16.43)
              try:
                  lb_paginator = elbv2.get_paginator("describe_load_balancers")
                  all_lbs = []
                  for page in lb_paginator.paginate():
                      all_lbs.extend(page.get("LoadBalancers", []))

                  for lb in all_lbs:
                      lb_arn = lb.get("LoadBalancerArn")
                      lb_name = lb.get("LoadBalancerName")
                      lb_type = lb.get("Type")
                      lb_state = lb.get("State", {}).get("Code")

                      target_groups = []
                      try:
                          tg_pages = elbv2.get_paginator("describe_target_groups").paginate(LoadBalancerArn=lb_arn)
                          for tg_page in tg_pages:
                              target_groups.extend(tg_page.get("TargetGroups", []))
                      except Exception as e:
                          warnings.append({"region": region, "service": "ELBv2", "operation": "DescribeTargetGroups", "resource": lb_name, "error": str(e)})
                          continue

                      if not target_groups:
                          add_finding(
                              findings,
                              category="Load Balancer With No Target Groups",
                              confidence="High",
                              severity="Medium",
                              region=region,
                              service="ELBv2",
                              resource_id=lb_arn,
                              resource_name=lb_name,
                              estimated_monthly_savings_usd=lb_baseline,
                              evidence=f"Load balancer type={lb_type}, state={lb_state}, has no target groups.",
                              recommendation="Confirm whether this load balancer is still needed. Delete after owner approval if unused.",
                              remediation_risk="High - load balancers may be externally referenced by DNS or third-party integrations."
                          )
                          continue

                      total_targets = 0
                      healthy_targets = 0
                      unhealthy_targets = 0
                      empty_tgs = []

                      for tg in target_groups:
                          tg_arn = tg.get("TargetGroupArn")
                          tg_name = tg.get("TargetGroupName")
                          try:
                              health = elbv2.describe_target_health(TargetGroupArn=tg_arn).get("TargetHealthDescriptions", [])
                              if not health:
                                  empty_tgs.append(tg_name)
                              for item in health:
                                  total_targets += 1
                                  state = item.get("TargetHealth", {}).get("State", "unknown")
                                  if state == "healthy":
                                      healthy_targets += 1
                                  else:
                                      unhealthy_targets += 1
                          except Exception as e:
                              warnings.append({"region": region, "service": "ELBv2", "operation": "DescribeTargetHealth", "resource": tg_name, "error": str(e)})

                      if empty_tgs:
                          add_finding(
                              findings,
                              category="Load Balancer With Empty Target Groups Review",
                              confidence="Medium",
                              severity="Medium",
                              region=region,
                              service="ELBv2",
                              resource_id=lb_arn,
                              resource_name=lb_name,
                              estimated_monthly_savings_usd=lb_baseline,
                              evidence=f"Load balancer type={lb_type}, state={lb_state}, target_group_count={len(target_groups)}, empty_target_groups={len(empty_tgs)}: {', '.join(empty_tgs)}.",
                              recommendation="Review whether this load balancer is still required. The load balancer is the billable resource; empty target groups are supporting evidence that it may be stale or incomplete.",
                              remediation_risk="High - confirm DNS records, listener rules, certificates, integrations, and application ownership before deleting a load balancer."
                          )

                      for empty_tg in empty_tgs:
                          add_finding(
                              findings,
                              category="Empty Target Group",
                              confidence="High",
                              severity="Medium",
                              region=region,
                              service="ELBv2",
                              resource_id=lb_arn,
                              resource_name=empty_tg,
                              estimated_monthly_savings_usd=None,
                              evidence=f"Target group {empty_tg} has no registered targets and is associated with load balancer {lb_name}.",
                              recommendation="Confirm whether this target group and any related listener rules are still needed.",
                              remediation_risk="Medium - listener rules may still route traffic to this target group."
                          )

                      if total_targets > 0 and healthy_targets == 0:
                          add_finding(
                              findings,
                              category="Load Balancer With No Healthy Targets",
                              confidence="Medium",
                              severity="Medium",
                              region=region,
                              service="ELBv2",
                              resource_id=lb_arn,
                              resource_name=lb_name,
                              estimated_monthly_savings_usd=lb_baseline,
                              evidence=f"Load balancer has total_targets={total_targets}, healthy_targets=0, unhealthy_targets={unhealthy_targets}.",
                              recommendation="Review whether the application is decommissioned, misconfigured, or intentionally offline. Delete only after owner approval.",
                              remediation_risk="High - could indicate outage or maintenance rather than waste."
                          )
              except Exception as e:
                  warnings.append({"region": region, "service": "ELBv2", "operation": "DescribeLoadBalancers", "error": str(e)})

          def scan_logs_region(region, config, findings, warnings):
              logs = boto3.client("logs", region_name=region)
              thresholds = config.get("thresholds", {})
              large_log_group_bytes = int(thresholds.get("large_log_group_bytes", 1073741824))
              try:
                  paginator = logs.get_paginator("describe_log_groups")
                  for page in paginator.paginate():
                      for lg in page.get("logGroups", []):
                          name = lg.get("logGroupName")
                          retention = lg.get("retentionInDays")
                          stored_bytes = int(lg.get("storedBytes", 0) or 0)
                          stored_gib = round(stored_bytes / 1073741824, 3)

                          if is_check_enabled(config, "cloudwatch_log_retention", True) and retention is None:
                              add_finding(
                                  findings,
                                  category="CloudWatch Log Group Without Retention",
                                  confidence="High",
                                  severity="Low",
                                  region=region,
                                  service="CloudWatch Logs",
                                  resource_id=name,
                                  resource_name=name,
                                  estimated_monthly_savings_usd=estimate_cloudwatch_logs_storage_monthly(stored_bytes, config),
                                  evidence=f"Log group has no retention policy and currently stores {stored_bytes} bytes ({stored_gib} GiB).",
                                  recommendation="Set a retention policy such as 30, 60, 90, or 365 days based on environment and compliance requirements.",
                                  remediation_risk="Low - setting retention deletes older log events after the retention window. Confirm audit requirements first."
                              )

                          if is_check_enabled(config, "large_cloudwatch_logs", True) and stored_bytes >= large_log_group_bytes:
                              add_finding(
                                  findings,
                                  category="Large CloudWatch Log Group Review",
                                  confidence="High",
                                  severity="Low",
                                  region=region,
                                  service="CloudWatch Logs",
                                  resource_id=name,
                                  resource_name=name,
                                  estimated_monthly_savings_usd=estimate_cloudwatch_logs_storage_monthly(stored_bytes, config),
                                  evidence=f"Log group stores {stored_bytes} bytes ({stored_gib} GiB); retention={retention or 'Never expire'}; threshold_bytes={large_log_group_bytes}.",
                                  recommendation="Review this large log group for retention policy, logging volume, debug verbosity, and whether logs should transition to a lower-cost retention/export strategy.",
                                  remediation_risk="Low to Medium - retention changes can permanently delete older log events after the retention window. Confirm audit and troubleshooting requirements first."
                              )
              except Exception as e:
                  warnings.append({"region": region, "service": "CloudWatch Logs", "operation": "DescribeLogGroups", "error": str(e)})

          def bucket_region_name(location_constraint):
              if not location_constraint:
                  return "us-east-1"
              if location_constraint == "EU":
                  return "eu-west-1"
              return location_constraint

          def get_bucket_metric(cloudwatch, bucket_name, storage_type, metric_name, lookback_days):
              end = utc_now()
              start = end - datetime.timedelta(days=lookback_days)
              response = cloudwatch.get_metric_statistics(
                  Namespace="AWS/S3",
                  MetricName=metric_name,
                  Dimensions=[
                      {"Name": "BucketName", "Value": bucket_name},
                      {"Name": "StorageType", "Value": storage_type}
                  ],
                  StartTime=start,
                  EndTime=end,
                  Period=86400,
                  Statistics=["Average"]
              )
              datapoints = response.get("Datapoints", [])
              if not datapoints:
                  return None
              latest = sorted(datapoints, key=lambda x: x.get("Timestamp"))[-1]
              return latest.get("Average")

          def scan_s3_account(config, findings, warnings):
              s3_client = boto3.client("s3")
              cloudwatch = boto3.client("cloudwatch", region_name="us-east-1")
              thresholds = config.get("thresholds", {})
              large_bucket_tib = float(thresholds.get("large_s3_bucket_tib", 1))
              lookback_days = int(thresholds.get("s3_metrics_lookback_days", 7))
              large_bucket_bytes = large_bucket_tib * 1024 * 1024 * 1024 * 1024
              s3_rate = config.get("cost_assumptions", {}).get("s3_standard_storage_gib_monthly_usd", 0.023)

              try:
                  buckets = s3_client.list_buckets().get("Buckets", [])
              except Exception as e:
                  warnings.append({"region": "global", "service": "S3", "operation": "ListBuckets", "error": str(e)})
                  return

              for bucket in buckets:
                  bucket_name = bucket.get("Name")
                  region = "unknown"
                  lifecycle_exists = False
                  lifecycle_rule_count = 0
                  versioning_status = "Unknown"
                  standard_bytes = None
                  object_count = None
                  try:
                      loc = s3_client.get_bucket_location(Bucket=bucket_name).get("LocationConstraint")
                      region = bucket_region_name(loc)
                  except Exception as e:
                      warnings.append({"region": "global", "service": "S3", "operation": "GetBucketLocation", "resource": bucket_name, "error": str(e)})

                  try:
                      versioning_status = s3_client.get_bucket_versioning(Bucket=bucket_name).get("Status", "Disabled")
                  except Exception as e:
                      warnings.append({"region": region, "service": "S3", "operation": "GetBucketVersioning", "resource": bucket_name, "error": str(e)})

                  try:
                      lifecycle = s3_client.get_bucket_lifecycle_configuration(Bucket=bucket_name)
                      rules = lifecycle.get("Rules", [])
                      lifecycle_exists = True
                      lifecycle_rule_count = len(rules)
                  except botocore.exceptions.ClientError as e:
                      code = e.response.get("Error", {}).get("Code", "")
                      if code in ["NoSuchLifecycleConfiguration", "NoSuchBucket"]:
                          lifecycle_exists = False
                      else:
                          warnings.append({"region": region, "service": "S3", "operation": "GetBucketLifecycleConfiguration", "resource": bucket_name, "error": str(e)})
                  except Exception as e:
                      warnings.append({"region": region, "service": "S3", "operation": "GetBucketLifecycleConfiguration", "resource": bucket_name, "error": str(e)})

                  try:
                      standard_bytes = get_bucket_metric(cloudwatch, bucket_name, "StandardStorage", "BucketSizeBytes", lookback_days)
                  except Exception as e:
                      warnings.append({"region": "us-east-1", "service": "CloudWatch/S3", "operation": "GetMetricStatistics BucketSizeBytes", "resource": bucket_name, "error": str(e)})

                  try:
                      object_count = get_bucket_metric(cloudwatch, bucket_name, "AllStorageTypes", "NumberOfObjects", lookback_days)
                  except Exception as e:
                      warnings.append({"region": "us-east-1", "service": "CloudWatch/S3", "operation": "GetMetricStatistics NumberOfObjects", "resource": bucket_name, "error": str(e)})

                  size_gib = None
                  size_tib = None
                  estimate = None
                  if standard_bytes is not None:
                      size_gib = standard_bytes / 1024 / 1024 / 1024
                      size_tib = standard_bytes / 1024 / 1024 / 1024 / 1024
                      estimate = round(size_gib * float(s3_rate), 2)

                  is_large = standard_bytes is not None and standard_bytes >= large_bucket_bytes
                  missing_lifecycle = not lifecycle_exists
                  versioning_without_lifecycle = versioning_status == "Enabled" and not lifecycle_exists

                  if is_large or missing_lifecycle or versioning_without_lifecycle:
                      if is_large:
                          category = "Large S3 Bucket Review"
                          severity = "Medium"
                          confidence = "Medium"
                      elif versioning_without_lifecycle:
                          category = "S3 Versioning Without Lifecycle Review"
                          severity = "Medium"
                          confidence = "Medium"
                      else:
                          category = "S3 Bucket Without Lifecycle Review"
                          severity = "Low"
                          confidence = "Medium"

                      evidence_parts = [
                          f"bucket={bucket_name}",
                          f"versioning={versioning_status}",
                          f"lifecycle_exists={lifecycle_exists}",
                          f"lifecycle_rule_count={lifecycle_rule_count}"
                      ]
                      if size_tib is not None:
                          evidence_parts.append(f"standard_storage_tib={size_tib:.3f}")
                      if object_count is not None:
                          evidence_parts.append(f"object_count={int(object_count)}")

                      add_finding(
                          findings,
                          category=category,
                          confidence=confidence,
                          severity=severity,
                          region=region,
                          service="S3",
                          resource_id=bucket_name,
                          resource_name=bucket_name,
                          estimated_monthly_savings_usd=estimate,
                          evidence="; ".join(evidence_parts),
                          recommendation="Review bucket lifecycle policy, noncurrent version expiration, incomplete multipart cleanup, and appropriate storage-class transitions. Do not delete data without application owner approval.",
                          remediation_risk="High - bucket contents may be business-critical, compliance data, logs, backups, or application state."
                      )

          def scan_global_accelerator(config, findings, warnings):
              ga = boto3.client("globalaccelerator", region_name="us-west-2")
              baseline = config.get("cost_assumptions", {}).get("global_accelerator_monthly_baseline_usd", 18.25)
              try:
                  paginator = ga.get_paginator("list_accelerators")
                  accelerators = []
                  for page in paginator.paginate():
                      accelerators.extend(page.get("Accelerators", []))
              except Exception as e:
                  warnings.append({"region": "global", "service": "Global Accelerator", "operation": "ListAccelerators", "error": str(e)})
                  return

              for acc in accelerators:
                  arn = acc.get("AcceleratorArn")
                  name = acc.get("Name", arn)
                  enabled = acc.get("Enabled")
                  status = acc.get("Status", "unknown")
                  listeners = []
                  endpoint_groups = []
                  endpoint_count = 0
                  unhealthy_count = 0
                  try:
                      listener_pages = ga.get_paginator("list_listeners").paginate(AcceleratorArn=arn)
                      for lp in listener_pages:
                          listeners.extend(lp.get("Listeners", []))
                  except Exception as e:
                      warnings.append({"region": "global", "service": "Global Accelerator", "operation": "ListListeners", "resource": name, "error": str(e)})

                  for listener in listeners:
                      listener_arn = listener.get("ListenerArn")
                      try:
                          eg_pages = ga.get_paginator("list_endpoint_groups").paginate(ListenerArn=listener_arn)
                          for eg_page in eg_pages:
                              for eg in eg_page.get("EndpointGroups", []):
                                  endpoint_groups.append(eg)
                                  for endpoint in eg.get("EndpointDescriptions", []):
                                      endpoint_count += 1
                                      health_state = endpoint.get("HealthState", "UNKNOWN")
                                      if health_state not in ["HEALTHY", "UNKNOWN"]:
                                          unhealthy_count += 1
                      except Exception as e:
                          warnings.append({"region": "global", "service": "Global Accelerator", "operation": "ListEndpointGroups", "resource": name, "error": str(e)})

                  if not enabled or len(listeners) == 0 or len(endpoint_groups) == 0 or endpoint_count == 0 or unhealthy_count > 0:
                      add_finding(
                          findings,
                          category="Global Accelerator Review",
                          confidence="Medium",
                          severity="Medium",
                          region="global",
                          service="Global Accelerator",
                          resource_id=arn,
                          resource_name=name,
                          estimated_monthly_savings_usd=baseline if not enabled or endpoint_count == 0 else None,
                          evidence=f"enabled={enabled}; status={status}; listeners={len(listeners)}; endpoint_groups={len(endpoint_groups)}; endpoints={endpoint_count}; unhealthy_endpoints={unhealthy_count}",
                          recommendation="Review whether this accelerator is still required. Disabled accelerators, accelerators with no endpoints, or unhealthy endpoint configurations may indicate avoidable cost or stale infrastructure.",
                          remediation_risk="High - confirm DNS, client routing, and application ownership before disabling or deleting."
                      )

          def scan_appstream_region(region, config, findings, warnings):
              appstream = boto3.client("appstream", region_name=region)
              try:
                  fleets = appstream.describe_fleets().get("Fleets", [])
              except Exception as e:
                  warnings.append({"region": region, "service": "WorkSpaces Applications/AppStream", "operation": "DescribeFleets", "error": str(e)})
                  return

              for fleet in fleets:
                  name = fleet.get("Name")
                  state = fleet.get("State")
                  fleet_type = fleet.get("FleetType", "UNKNOWN")
                  instance_type = fleet.get("InstanceType", "unknown")
                  compute = fleet.get("ComputeCapacityStatus", {})
                  desired = compute.get("Desired", 0)
                  running = compute.get("Running", 0)
                  in_use = compute.get("InUse", 0)
                  available = compute.get("Available", 0)
                  idle = max((running or 0) - (in_use or 0), 0)
                  sessions_count = None
                  try:
                      sessions = appstream.describe_sessions(FleetName=name).get("Sessions", [])
                      sessions_count = len(sessions)
                  except Exception as e:
                      warnings.append({"region": region, "service": "WorkSpaces Applications/AppStream", "operation": "DescribeSessions", "resource": name, "error": str(e)})

                  if state in ["RUNNING", "STARTING"] or desired > 0 or running > 0 or idle > 0:
                      add_finding(
                          findings,
                          category="WorkSpaces Applications AppStream Fleet Review",
                          confidence="Medium",
                          severity="Medium",
                          region=region,
                          service="WorkSpaces Applications/AppStream",
                          resource_id=name,
                          resource_name=name,
                          estimated_monthly_savings_usd=None,
                          evidence=f"fleet_type={fleet_type}; state={state}; instance_type={instance_type}; desired={desired}; running={running}; in_use={in_use}; available={available}; idle={idle}; sessions={sessions_count}",
                          recommendation="Review fleet type, schedule, scaling policy, session demand, and whether capacity should be reduced or stopped outside business hours.",
                          remediation_risk="High - changes can prevent users from launching applications. Coordinate with application owners."
                      )

              try:
                  builders = appstream.describe_image_builders().get("ImageBuilders", [])
                  for builder in builders:
                      name = builder.get("Name")
                      state = builder.get("State")
                      instance_type = builder.get("InstanceType", "unknown")
                      if state in ["RUNNING", "PENDING", "UPDATING_AGENT"]:
                          add_finding(
                              findings,
                              category="WorkSpaces Applications AppStream Image Builder Review",
                              confidence="Medium",
                              severity="Medium",
                              region=region,
                              service="WorkSpaces Applications/AppStream",
                              resource_id=name,
                              resource_name=name,
                              estimated_monthly_savings_usd=None,
                              evidence=f"image_builder_state={state}; instance_type={instance_type}",
                              recommendation="Review whether this image builder needs to remain running. Stop it when image maintenance is complete.",
                              remediation_risk="Medium - stopping an active image builder can interrupt image creation or maintenance."
                          )
              except Exception as e:
                  warnings.append({"region": region, "service": "WorkSpaces Applications/AppStream", "operation": "DescribeImageBuilders", "error": str(e)})

          def estimate_aws_backup_monthly(size_bytes, config):
              assumptions = config.get("cost_assumptions", {})
              rate = assumptions.get("aws_backup_warm_storage_gib_monthly_usd", 0.05)
              try:
                  gib = float(size_bytes or 0) / 1024 / 1024 / 1024
                  return round(gib * float(rate), 2)
              except Exception:
                  return None

          def estimate_s3_standard_storage_monthly_usd(size_bytes, config):
              assumptions = config.get("cost_assumptions", {})
              rate = assumptions.get("s3_standard_storage_gib_monthly_usd", assumptions.get("s3_standard_gib_monthly_usd", 0.023))
              try:
                  if size_bytes is None:
                      return None
                  gib = float(size_bytes or 0) / 1024 / 1024 / 1024
                  return round(gib * float(rate), 2)
              except Exception:
                  return None

          def get_bucket_region(s3_client, bucket_name):
              try:
                  loc = s3_client.get_bucket_location(Bucket=bucket_name).get("LocationConstraint")
                  if loc in [None, ""]:
                      return "us-east-1"
                  if loc == "EU":
                      return "eu-west-1"
                  return loc
              except Exception:
                  return "unknown"

          def get_s3_bucket_size_bytes(bucket_name, bucket_region, lookback_days, warnings):
              try:
                  cw = boto3.client("cloudwatch", region_name="us-east-1")
                  end = utc_now()
                  start = end - datetime.timedelta(days=lookback_days)
                  response = cw.get_metric_statistics(
                      Namespace="AWS/S3",
                      MetricName="BucketSizeBytes",
                      Dimensions=[
                          {"Name": "BucketName", "Value": bucket_name},
                          {"Name": "StorageType", "Value": "StandardStorage"}
                      ],
                      StartTime=start,
                      EndTime=end,
                      Period=86400,
                      Statistics=["Average"]
                  )
                  datapoints = response.get("Datapoints", [])
                  if not datapoints:
                      return None
                  latest = sorted(datapoints, key=lambda d: d.get("Timestamp"), reverse=True)[0]
                  return int(latest.get("Average", 0) or 0)
              except Exception as e:
                  warnings.append({"region": bucket_region, "service": "S3/CloudWatch", "operation": "GetMetricStatistics BucketSizeBytes", "resource": bucket_name, "error": str(e)})
                  return None

          def scan_s3_global(config, findings, warnings):
              s3_global = boto3.client("s3")
              thresholds = config.get("thresholds", {})
              lookback_days = int(thresholds.get("s3_metrics_lookback_days", 14))
              large_bucket_tib = float(thresholds.get("large_s3_bucket_tib", 1))
              large_bucket_bytes = int(large_bucket_tib * 1024 * 1024 * 1024 * 1024)

              try:
                  buckets = s3_global.list_buckets().get("Buckets", [])
              except Exception as e:
                  warnings.append({"region": "global", "service": "S3", "operation": "ListBuckets", "error": str(e)})
                  return

              total_buckets = len(buckets)
              missing_lifecycle_count = 0
              versioning_without_lifecycle_count = 0
              large_bucket_count = 0
              lifecycle_check_errors = 0

              for bucket in buckets:
                  bucket_name = bucket.get("Name")
                  bucket_region = get_bucket_region(s3_global, bucket_name)
                  s3_bucket = boto3.client("s3", region_name=bucket_region if bucket_region != "unknown" else "us-east-1")

                  versioning_status = "Unknown"
                  lifecycle_exists = False
                  lifecycle_rule_count = 0

                  try:
                      versioning_status = s3_bucket.get_bucket_versioning(Bucket=bucket_name).get("Status", "Disabled")
                  except Exception as e:
                      warnings.append({"region": bucket_region, "service": "S3", "operation": "GetBucketVersioning", "resource": bucket_name, "error": str(e)})

                  try:
                      lifecycle = s3_bucket.get_bucket_lifecycle_configuration(Bucket=bucket_name)
                      rules = lifecycle.get("Rules", [])
                      lifecycle_rule_count = len(rules)
                      lifecycle_exists = lifecycle_rule_count > 0
                  except botocore.exceptions.ClientError as e:
                      code = e.response.get("Error", {}).get("Code", "")
                      status_code = e.response.get("ResponseMetadata", {}).get("HTTPStatusCode")
                      if code in ["NoSuchLifecycleConfiguration", "NoSuchBucket"] or status_code == 404:
                          lifecycle_exists = False
                      else:
                          lifecycle_check_errors += 1
                          warnings.append({"region": bucket_region, "service": "S3", "operation": "GetLifecycleConfiguration", "resource": bucket_name, "error": str(e)})
                          continue
                  except Exception as e:
                      lifecycle_check_errors += 1
                      warnings.append({"region": bucket_region, "service": "S3", "operation": "GetLifecycleConfiguration", "resource": bucket_name, "error": str(e)})
                      continue

                  size_bytes = get_s3_bucket_size_bytes(bucket_name, bucket_region, lookback_days, warnings)
                  size_tib = round(size_bytes / 1024 / 1024 / 1024 / 1024, 3) if size_bytes is not None else None
                  estimated = estimate_s3_standard_storage_monthly_usd(size_bytes, config) if size_bytes is not None else None

                  if not lifecycle_exists:
                      missing_lifecycle_count += 1
                      add_finding(
                          findings,
                          category="S3 Bucket Without Lifecycle Review",
                          confidence="Medium",
                          severity="Low",
                          region=bucket_region,
                          service="S3",
                          resource_id=bucket_name,
                          resource_name=bucket_name,
                          estimated_monthly_savings_usd=0.0 if estimated is None else estimated,
                          evidence=f"bucket={bucket_name}; region={bucket_region}; versioning={versioning_status}; lifecycle_exists={lifecycle_exists}; lifecycle_rule_count={lifecycle_rule_count}; standard_storage_tib={size_tib}; lookback_days={lookback_days}.",
                          recommendation="Review bucket lifecycle policy, noncurrent version expiration, incomplete multipart cleanup, and appropriate storage-class transitions. Do not delete data without application owner approval.",
                          remediation_risk="Medium - lifecycle policies can expire or transition data. Confirm application, audit, and retention requirements first."
                      )

                  if versioning_status == "Enabled" and not lifecycle_exists:
                      versioning_without_lifecycle_count += 1
                      add_finding(
                          findings,
                          category="S3 Versioning Without Lifecycle Review",
                          confidence="Medium",
                          severity="Medium",
                          region=bucket_region,
                          service="S3",
                          resource_id=bucket_name,
                          resource_name=bucket_name,
                          estimated_monthly_savings_usd=0.0 if estimated is None else estimated,
                          evidence=f"bucket={bucket_name}; region={bucket_region}; versioning={versioning_status}; lifecycle_exists={lifecycle_exists}; lifecycle_rule_count={lifecycle_rule_count}; standard_storage_tib={size_tib}; lookback_days={lookback_days}.",
                          recommendation="Review noncurrent-version lifecycle expiration and storage-class transitions. Versioned buckets without lifecycle policies can accumulate hidden storage cost.",
                          remediation_risk="Medium - expiring noncurrent versions can permanently remove rollback points. Confirm owner and retention requirements first."
                      )

                  if size_bytes is not None and size_bytes >= large_bucket_bytes:
                      large_bucket_count += 1
                      add_finding(
                          findings,
                          category="Large S3 Bucket Review",
                          confidence="High",
                          severity="Low",
                          region=bucket_region,
                          service="S3",
                          resource_id=bucket_name,
                          resource_name=bucket_name,
                          estimated_monthly_savings_usd=estimated,
                          evidence=f"bucket={bucket_name}; region={bucket_region}; standard_storage_tib={size_tib}; threshold_tib={large_bucket_tib}; versioning={versioning_status}; lifecycle_exists={lifecycle_exists}; lifecycle_rule_count={lifecycle_rule_count}.",
                          recommendation="Review object age, storage classes, lifecycle transitions, noncurrent versions, and whether S3 Storage Lens should be used for deeper analysis.",
                          remediation_risk="Medium - large buckets may contain production data, backups, logs, or compliance records. Review before changing lifecycle or storage class."
                      )

              add_finding(
                  findings,
                  category="S3 Account Inventory Summary",
                  confidence="Info",
                  severity="Info",
                  region="global",
                  service="S3 Inventory",
                  resource_id="account-level-s3-summary",
                  resource_name="S3 Account Inventory Summary",
                  estimated_monthly_savings_usd=0.0,
                  evidence=f"total_buckets={total_buckets}; missing_lifecycle_count={missing_lifecycle_count}; versioning_without_lifecycle_count={versioning_without_lifecycle_count}; large_bucket_count={large_bucket_count}; lifecycle_check_errors={lifecycle_check_errors}.",
                  recommendation="This is one account-level summary row, not one bucket. Use total_buckets and missing_lifecycle_count to validate the S3 scanner against CLI list-buckets. Investigate any lifecycle_check_errors in Collection Warnings.",
                  remediation_risk="None - informational summary only."
              )

          def estimate_efs_standard_monthly(size_bytes, config):
              assumptions = config.get("cost_assumptions", {})
              rate = assumptions.get("efs_standard_gib_monthly_usd", 0.30)
              try:
                  gib = float(size_bytes or 0) / 1024 / 1024 / 1024
                  return round(gib * float(rate), 2)
              except Exception:
                  return None

          def scan_efs_region(region, config, findings, warnings):
              efs = boto3.client("efs", region_name=region)
              thresholds = config.get("thresholds", {})
              large_gib_threshold = float(thresholds.get("efs_large_filesystem_gib", 100))
              standard_gib_threshold = float(thresholds.get("efs_standard_storage_gib_threshold", 100))

              try:
                  paginator = efs.get_paginator("describe_file_systems")
                  pages = paginator.paginate()
              except Exception as e:
                  warnings.append({"region": region, "service": "EFS", "operation": "DescribeFileSystems", "error": str(e)})
                  return

              for page in pages:
                  for fs in page.get("FileSystems", []):
                      fs_id = fs.get("FileSystemId")
                      tags = fs.get("Tags", [])
                      if is_excluded(fs_id, tags, config):
                          continue

                      name = get_name_tag(tags)
                      lifecycle_policies = []
                      lifecycle_exists = False
                      mount_target_count = 0

                      size = fs.get("SizeInBytes", {}) or {}
                      total_bytes = int(size.get("Value", 0) or 0)
                      standard_bytes = int(size.get("ValueInStandard", 0) or 0)
                      ia_bytes = int(size.get("ValueInIA", 0) or 0)
                      archive_bytes = int(size.get("ValueInArchive", 0) or 0)
                      total_gib = round(total_bytes / 1024 / 1024 / 1024, 3)
                      standard_gib = round(standard_bytes / 1024 / 1024 / 1024, 3)
                      ia_gib = round(ia_bytes / 1024 / 1024 / 1024, 3)
                      archive_gib = round(archive_bytes / 1024 / 1024 / 1024, 3)

                      try:
                          lifecycle_policies = efs.describe_lifecycle_configuration(FileSystemId=fs_id).get("LifecyclePolicies", [])
                          lifecycle_exists = len(lifecycle_policies) > 0
                      except botocore.exceptions.ClientError as e:
                          code = e.response.get("Error", {}).get("Code", "")
                          if code in ["LifecyclePolicyNotFound", "PolicyNotFound", "NotFound"]:
                              lifecycle_exists = False
                          else:
                              warnings.append({"region": region, "service": "EFS", "operation": "DescribeLifecycleConfiguration", "resource": fs_id, "error": str(e)})
                      except Exception as e:
                          warnings.append({"region": region, "service": "EFS", "operation": "DescribeLifecycleConfiguration", "resource": fs_id, "error": str(e)})

                      try:
                          mt_pages = efs.get_paginator("describe_mount_targets").paginate(FileSystemId=fs_id)
                          for mt_page in mt_pages:
                              mount_target_count += len(mt_page.get("MountTargets", []))
                      except Exception as e:
                          warnings.append({"region": region, "service": "EFS", "operation": "DescribeMountTargets", "resource": fs_id, "error": str(e)})

                      lifecycle_text = json.dumps(lifecycle_policies, default=json_safe)
                      base_evidence = f"filesystem_id={fs_id}; name={name}; lifecycle_exists={lifecycle_exists}; lifecycle_policies={lifecycle_text}; mount_target_count={mount_target_count}; total_gib={total_gib}; standard_gib={standard_gib}; ia_gib={ia_gib}; archive_gib={archive_gib}; performance_mode={fs.get('PerformanceMode')}; throughput_mode={fs.get('ThroughputMode')}; encrypted={fs.get('Encrypted')}."

                      if not lifecycle_exists:
                          add_finding(
                              findings,
                              category="EFS File System Without Lifecycle Review",
                              confidence="Medium",
                              severity="Low",
                              region=region,
                              service="EFS",
                              resource_id=fs_id,
                              resource_name=name,
                              estimated_monthly_savings_usd=estimate_efs_standard_monthly(standard_bytes, config),
                              evidence=base_evidence,
                              recommendation="Review whether EFS lifecycle management should transition infrequently accessed files to EFS IA or Archive based on application and compliance requirements.",
                              remediation_risk="Medium - lifecycle transitions can affect access latency and retrieval cost. Confirm application behavior before changing lifecycle policies."
                          )

                      if total_gib >= large_gib_threshold:
                          add_finding(
                              findings,
                              category="EFS Large File System Review",
                              confidence="High",
                              severity="Low",
                              region=region,
                              service="EFS",
                              resource_id=fs_id,
                              resource_name=name,
                              estimated_monthly_savings_usd=estimate_efs_standard_monthly(standard_bytes, config),
                              evidence=base_evidence + f" threshold_gib={large_gib_threshold}.",
                              recommendation="Review this large EFS file system for lifecycle policy, stale data, application ownership, and whether lower-cost EFS storage classes are appropriate.",
                              remediation_risk="Medium - file systems may contain shared production data. Do not delete or transition data without owner approval."
                          )

                      if standard_gib >= standard_gib_threshold and not lifecycle_exists:
                          add_finding(
                              findings,
                              category="EFS Standard Storage Optimization Review",
                              confidence="Medium",
                              severity="Low",
                              region=region,
                              service="EFS",
                              resource_id=fs_id,
                              resource_name=name,
                              estimated_monthly_savings_usd=estimate_efs_standard_monthly(standard_bytes, config),
                              evidence=base_evidence + f" standard_storage_threshold_gib={standard_gib_threshold}.",
                              recommendation="Review whether a lifecycle policy can reduce Standard storage by moving cold files to IA or Archive.",
                              remediation_risk="Medium - moving data to IA or Archive can affect latency and retrieval cost. Confirm access patterns first."
                          )

                      if mount_target_count == 0:
                          add_finding(
                              findings,
                              category="EFS File System Without Mount Targets Review",
                              confidence="Medium",
                              severity="Medium",
                              region=region,
                              service="EFS",
                              resource_id=fs_id,
                              resource_name=name,
                              estimated_monthly_savings_usd=estimate_efs_standard_monthly(total_bytes, config),
                              evidence=base_evidence,
                              recommendation="Review whether this EFS file system is still required. A file system with no mount targets may be stale, but confirm backup, migration, and recovery requirements before action.",
                              remediation_risk="High - deleting EFS permanently removes shared file data. Confirm owner, backups, and retention requirements first."
                          )

          def estimate_opensearch_storage_monthly(volume_type, volume_size_gib, instance_count, config):
              assumptions = config.get("cost_assumptions", {})
              vt = str(volume_type or "gp3").lower()
              if vt == "gp2":
                  rate = assumptions.get("opensearch_gp2_gib_monthly_usd", 0.135)
              else:
                  rate = assumptions.get("opensearch_gp3_gib_monthly_usd", 0.122)
              try:
                  return round(float(volume_size_gib or 0) * float(instance_count or 1) * float(rate), 2)
              except Exception:
                  return None

          def scan_opensearch_region(region, config, findings, warnings):
              os_client = boto3.client("opensearch", region_name=region)
              thresholds = config.get("thresholds", {})
              large_storage_gib = float(thresholds.get("opensearch_large_domain_storage_gib", 100))
              old_domain_days = int(thresholds.get("opensearch_old_domain_days", 180))
              large_instance_count = int(thresholds.get("opensearch_large_instance_count", 3))
              instance_baseline = float(config.get("cost_assumptions", {}).get("opensearch_instance_baseline_monthly_usd", 75.0))
              now = utc_now()

              try:
                  domains = os_client.list_domain_names().get("DomainNames", [])
              except Exception as e:
                  warnings.append({"region": region, "service": "OpenSearch", "operation": "ListDomainNames", "error": str(e)})
                  return

              for domain_ref in domains:
                  domain_name = domain_ref.get("DomainName")
                  try:
                      domain = os_client.describe_domain(DomainName=domain_name).get("DomainStatus", {})
                  except Exception as e:
                      warnings.append({"region": region, "service": "OpenSearch", "operation": "DescribeDomain", "resource": domain_name, "error": str(e)})
                      continue

                  domain_arn = domain.get("ARN", domain_name)
                  tags = []
                  try:
                      tag_resp = os_client.list_tags(ARN=domain_arn).get("TagList", [])
                      tags = [{"Key": t.get("Key", ""), "Value": t.get("Value", "")} for t in tag_resp]
                  except Exception as e:
                      warnings.append({"region": region, "service": "OpenSearch", "operation": "ListTags", "resource": domain_name, "error": str(e)})

                  if is_excluded(domain_name, tags, config) or is_excluded(domain_arn, tags, config):
                      continue

                  cluster = domain.get("ClusterConfig", {}) or {}
                  ebs = domain.get("EBSOptions", {}) or {}
                  instance_type = cluster.get("InstanceType", "unknown")
                  instance_count = int(cluster.get("InstanceCount", 0) or 0)
                  zone_awareness = bool(cluster.get("ZoneAwarenessEnabled", False))
                  dedicated_master = bool(cluster.get("DedicatedMasterEnabled", False))
                  warm_enabled = bool(cluster.get("WarmEnabled", False))
                  standby_enabled = bool(cluster.get("MultiAZWithStandbyEnabled", False))
                  ebs_enabled = bool(ebs.get("EBSEnabled", False))
                  volume_type = ebs.get("VolumeType", "unknown")
                  volume_size = int(ebs.get("VolumeSize", 0) or 0)
                  total_storage_gib = volume_size * max(instance_count, 1) if ebs_enabled else 0
                  created = domain.get("Created")
                  processing = domain.get("Processing")
                  engine_version = domain.get("EngineVersion", domain.get("ElasticsearchVersion", "unknown"))
                  estimated_storage = estimate_opensearch_storage_monthly(volume_type, volume_size, max(instance_count, 1), config) if ebs_enabled else None
                  estimated_instance_baseline = round(instance_count * instance_baseline, 2) if instance_count else None

                  evidence_base = f"domain={domain_name}; engine_version={engine_version}; instance_type={instance_type}; instance_count={instance_count}; zone_awareness={zone_awareness}; multi_az_standby={standby_enabled}; dedicated_master={dedicated_master}; warm_enabled={warm_enabled}; ebs_enabled={ebs_enabled}; volume_type={volume_type}; volume_size_gib_per_node={volume_size}; total_ebs_storage_gib={total_storage_gib}; processing={processing}."

                  add_finding(
                      findings,
                      category="OpenSearch Domain Inventory Summary",
                      confidence="Info",
                      severity="Info",
                      region=region,
                      service="OpenSearch",
                      resource_id=domain_arn,
                      resource_name=domain_name,
                      estimated_monthly_savings_usd=0.0,
                      evidence=evidence_base,
                      recommendation="Inventory summary only. Review OpenSearch domain sizing, storage, instance count, and availability configuration for cost optimization opportunities.",
                      remediation_risk="None - informational summary only."
                  )

                  if total_storage_gib >= large_storage_gib:
                      add_finding(
                          findings,
                          category="OpenSearch Large Storage Review",
                          confidence="Medium",
                          severity="Low",
                          region=region,
                          service="OpenSearch",
                          resource_id=domain_arn,
                          resource_name=domain_name,
                          estimated_monthly_savings_usd=estimated_storage,
                          evidence=evidence_base + f" large_storage_gib_threshold={large_storage_gib}.",
                          recommendation="Review index retention, shard strategy, UltraWarm/cold storage options, snapshots, and whether old indexes can be deleted or moved to lower-cost storage.",
                          remediation_risk="Medium - deleting indexes or changing storage can impact search, analytics, audit, and restore requirements. Confirm owner and retention needs first."
                      )

                  if instance_count >= large_instance_count:
                      add_finding(
                          findings,
                          category="OpenSearch Instance Count Review",
                          confidence="Medium",
                          severity="Low",
                          region=region,
                          service="OpenSearch",
                          resource_id=domain_arn,
                          resource_name=domain_name,
                          estimated_monthly_savings_usd=estimated_instance_baseline,
                          evidence=evidence_base + f" large_instance_count_threshold={large_instance_count}; instance_baseline_monthly_usd={instance_baseline}.",
                          recommendation="Review CPU, JVM memory pressure, storage, query/indexing workload, replica/shard count, and whether the domain is oversized for current demand.",
                          remediation_risk="High - resizing OpenSearch domains can affect availability, performance, and shard placement. Validate with application owners and maintenance windows."
                      )

                  if zone_awareness or standby_enabled or dedicated_master or warm_enabled:
                      add_finding(
                          findings,
                          category="OpenSearch Advanced Capacity Review",
                          confidence="Medium",
                          severity="Low",
                          region=region,
                          service="OpenSearch",
                          resource_id=domain_arn,
                          resource_name=domain_name,
                          estimated_monthly_savings_usd=None,
                          evidence=evidence_base,
                          recommendation="Review whether zone awareness, standby, dedicated masters, and warm nodes are appropriate for this environment. These are often correct for production but may be excessive for dev/test.",
                          remediation_risk="High - availability and durability settings protect production workloads. Do not reduce without architecture review."
                      )

          def estimate_dynamodb_storage_monthly(size_bytes, config):
              assumptions = config.get("cost_assumptions", {})
              rate = assumptions.get("dynamodb_standard_storage_gib_monthly_usd", 0.25)
              try:
                  gib = float(size_bytes or 0) / 1024 / 1024 / 1024
                  return round(gib * float(rate), 2)
              except Exception:
                  return None

          def scan_dynamodb_region(region, config, findings, warnings):
              dynamodb = boto3.client("dynamodb", region_name=region)
              cloudwatch = boto3.client("cloudwatch", region_name=region)
              thresholds = config.get("thresholds", {})
              old_table_days = int(thresholds.get("dynamodb_old_table_days", 180))
              large_table_gib = float(thresholds.get("dynamodb_large_table_gib", 10))
              large_table_bytes = large_table_gib * 1024 * 1024 * 1024
              lookback_days = int(thresholds.get("dynamodb_low_activity_lookback_days", 14))
              low_activity_threshold = float(thresholds.get("dynamodb_low_activity_request_threshold", 10))
              now = utc_now()

              try:
                  table_pages = dynamodb.get_paginator("list_tables").paginate()
              except Exception as e:
                  warnings.append({"region": region, "service": "DynamoDB", "operation": "ListTables", "error": str(e)})
                  return

              for page in table_pages:
                  for table_name in page.get("TableNames", []):
                      try:
                          table = dynamodb.describe_table(TableName=table_name).get("Table", {})
                      except Exception as e:
                          warnings.append({"region": region, "service": "DynamoDB", "operation": "DescribeTable", "resource": table_name, "error": str(e)})
                          continue

                      table_arn = table.get("TableArn", table_name)
                      tags = []
                      try:
                          tag_resp = dynamodb.list_tags_of_resource(ResourceArn=table_arn).get("Tags", [])
                          tags = [{"Key": t.get("Key", ""), "Value": t.get("Value", "")} for t in tag_resp]
                      except Exception as e:
                          warnings.append({"region": region, "service": "DynamoDB", "operation": "ListTagsOfResource", "resource": table_name, "error": str(e)})

                      if is_excluded(table_name, tags, config) or is_excluded(table_arn, tags, config):
                          continue

                      size_bytes = int(table.get("TableSizeBytes", 0) or 0)
                      item_count = int(table.get("ItemCount", 0) or 0)
                      size_gib = round(size_bytes / 1024 / 1024 / 1024, 3)
                      billing_mode = table.get("BillingModeSummary", {}).get("BillingMode", "PROVISIONED")
                      table_status = table.get("TableStatus", "unknown")
                      created = table.get("CreationDateTime")
                      age_days = (now - created).days if created else None
                      estimated = estimate_dynamodb_storage_monthly(size_bytes, config)

                      read_units = None
                      write_units = None
                      try:
                          read_units = get_cloudwatch_stat(
                              cloudwatch,
                              "AWS/DynamoDB",
                              "ConsumedReadCapacityUnits",
                              [{"Name": "TableName", "Value": table_name}],
                              lookback_days,
                              "Sum"
                          )
                          write_units = get_cloudwatch_stat(
                              cloudwatch,
                              "AWS/DynamoDB",
                              "ConsumedWriteCapacityUnits",
                              [{"Name": "TableName", "Value": table_name}],
                              lookback_days,
                              "Sum"
                          )
                      except Exception as e:
                          warnings.append({"region": region, "service": "DynamoDB/CloudWatch", "operation": "GetMetricStatistics", "resource": table_name, "error": str(e)})

                      evidence_base = f"table={table_name}; status={table_status}; billing_mode={billing_mode}; size_gib={size_gib}; item_count={item_count}; age_days={age_days}; read_units_sum={read_units}; write_units_sum={write_units}; lookback_days={lookback_days}."

                      if size_bytes >= large_table_bytes:
                          add_finding(
                              findings,
                              category="DynamoDB Large Table Review",
                              confidence="Medium",
                              severity="Low",
                              region=region,
                              service="DynamoDB",
                              resource_id=table_arn,
                              resource_name=table_name,
                              estimated_monthly_savings_usd=estimated,
                              evidence=evidence_base + f" large_table_gib_threshold={large_table_gib}.",
                              recommendation="Review table storage, TTL usage, old items, backup/PITR settings, and whether archival/export strategy is appropriate.",
                              remediation_risk="Medium - DynamoDB data may be production-critical. Confirm owner, retention, and application behavior before deleting or archiving data."
                          )

                      if age_days is not None and age_days >= old_table_days and (read_units is None or read_units <= low_activity_threshold) and (write_units is None or write_units <= low_activity_threshold):
                          add_finding(
                              findings,
                              category="DynamoDB Low Activity Table Review",
                              confidence="Medium",
                              severity="Low",
                              region=region,
                              service="DynamoDB",
                              resource_id=table_arn,
                              resource_name=table_name,
                              estimated_monthly_savings_usd=estimated,
                              evidence=evidence_base + f" old_table_days_threshold={old_table_days}; low_activity_request_threshold={low_activity_threshold}.",
                              recommendation="Review whether this older low-activity table is still needed, should be archived, or can be cleaned up after owner approval.",
                              remediation_risk="Medium - low activity does not prove unused; tables may support monthly jobs, audit lookup, DR, or infrequent workflows."
                          )

                      if billing_mode == "PROVISIONED":
                          provisioned = table.get("ProvisionedThroughput", {})
                          read_capacity = provisioned.get("ReadCapacityUnits")
                          write_capacity = provisioned.get("WriteCapacityUnits")
                          add_finding(
                              findings,
                              category="DynamoDB Provisioned Capacity Review",
                              confidence="Medium",
                              severity="Low",
                              region=region,
                              service="DynamoDB",
                              resource_id=table_arn,
                              resource_name=table_name,
                              estimated_monthly_savings_usd=None,
                              evidence=evidence_base + f" provisioned_read_capacity={read_capacity}; provisioned_write_capacity={write_capacity}.",
                              recommendation="Review whether provisioned capacity, auto scaling, or on-demand billing is the best fit for current usage patterns.",
                              remediation_risk="Medium - changing capacity or billing mode can affect performance and cost. Review traffic patterns and application requirements first."
                          )

          def estimate_ecr_storage_monthly(size_bytes, config):
              assumptions = config.get("cost_assumptions", {})
              rate = assumptions.get("ecr_storage_gib_monthly_usd", 0.10)
              try:
                  gib = float(size_bytes or 0) / 1024 / 1024 / 1024
                  return round(gib * float(rate), 2)
              except Exception:
                  return None

          def scan_ecr_region(region, config, findings, warnings):
              ecr = boto3.client("ecr", region_name=region)
              thresholds = config.get("thresholds", {})
              old_image_days = int(thresholds.get("ecr_old_image_days", 180))
              large_image_count = int(thresholds.get("ecr_large_repository_image_count", 100))
              untagged_threshold = int(thresholds.get("ecr_untagged_image_count", 10))
              large_storage_gib = float(thresholds.get("ecr_large_repository_storage_gib", 10))
              large_storage_bytes = large_storage_gib * 1024 * 1024 * 1024
              now = utc_now()

              try:
                  repo_pages = ecr.get_paginator("describe_repositories").paginate()
              except Exception as e:
                  warnings.append({"region": region, "service": "ECR", "operation": "DescribeRepositories", "error": str(e)})
                  return

              for repo_page in repo_pages:
                  for repo in repo_page.get("repositories", []):
                      repo_name = repo.get("repositoryName")
                      repo_arn = repo.get("repositoryArn", repo_name)
                      tags = []
                      try:
                          tag_dict = ecr.list_tags_for_resource(resourceArn=repo_arn).get("tags", [])
                          tags = [{"Key": t.get("Key", ""), "Value": t.get("Value", "")} for t in tag_dict]
                      except Exception as e:
                          warnings.append({"region": region, "service": "ECR", "operation": "ListTagsForResource", "resource": repo_name, "error": str(e)})
                      if is_excluded(repo_name, tags, config) or is_excluded(repo_arn, tags, config):
                          continue

                      lifecycle_exists = False
                      lifecycle_policy_text = ""
                      try:
                          lifecycle_policy_text = ecr.get_lifecycle_policy(repositoryName=repo_name).get("lifecyclePolicyText", "")
                          lifecycle_exists = bool(lifecycle_policy_text)
                      except botocore.exceptions.ClientError as e:
                          code = e.response.get("Error", {}).get("Code", "")
                          if code in ["LifecyclePolicyNotFoundException", "RepositoryNotFoundException"]:
                              lifecycle_exists = False
                          else:
                              warnings.append({"region": region, "service": "ECR", "operation": "GetLifecyclePolicy", "resource": repo_name, "error": str(e)})
                      except Exception as e:
                          warnings.append({"region": region, "service": "ECR", "operation": "GetLifecyclePolicy", "resource": repo_name, "error": str(e)})

                      image_count = 0
                      untagged_count = 0
                      old_image_count = 0
                      total_size_bytes = 0
                      oldest_days = None

                      try:
                          image_pages = ecr.get_paginator("describe_images").paginate(repositoryName=repo_name)
                          for image_page in image_pages:
                              for image in image_page.get("imageDetails", []):
                                  image_count += 1
                                  image_tags = image.get("imageTags", []) or []
                                  if not image_tags:
                                      untagged_count += 1
                                  total_size_bytes += int(image.get("imageSizeInBytes", 0) or 0)
                                  pushed_at = image.get("imagePushedAt")
                                  if pushed_at:
                                      age_days = (now - pushed_at).days
                                      oldest_days = age_days if oldest_days is None else max(oldest_days, age_days)
                                      if age_days >= old_image_days:
                                          old_image_count += 1
                      except Exception as e:
                          warnings.append({"region": region, "service": "ECR", "operation": "DescribeImages", "resource": repo_name, "error": str(e)})
                          continue

                      storage_gib = round(total_size_bytes / 1024 / 1024 / 1024, 3)
                      estimated = estimate_ecr_storage_monthly(total_size_bytes, config)
                      evidence_base = f"repository={repo_name}; image_count={image_count}; untagged_image_count={untagged_count}; old_image_count={old_image_count}; oldest_image_age_days={oldest_days}; estimated_image_storage_gib={storage_gib}; lifecycle_exists={lifecycle_exists}; scan_on_push={repo.get('imageScanningConfiguration', {}).get('scanOnPush')}; encryption={repo.get('encryptionConfiguration', {}).get('encryptionType')}."

                      if not lifecycle_exists and image_count > 0:
                          add_finding(
                              findings,
                              category="ECR Repository Without Lifecycle Policy Review",
                              confidence="High",
                              severity="Low",
                              region=region,
                              service="ECR",
                              resource_id=repo_arn,
                              resource_name=repo_name,
                              estimated_monthly_savings_usd=estimated,
                              evidence=evidence_base,
                              recommendation="Review whether this ECR repository should have a lifecycle policy to expire old or untagged images. Do not delete images without confirming deployment rollback requirements.",
                              remediation_risk="Medium - ECR images may be needed for rollback, redeployment, audit, or disaster recovery. Confirm image retention requirements before lifecycle cleanup."
                          )

                      if untagged_count >= untagged_threshold:
                          add_finding(
                              findings,
                              category="ECR Untagged Image Review",
                              confidence="High",
                              severity="Low",
                              region=region,
                              service="ECR",
                              resource_id=repo_arn,
                              resource_name=repo_name,
                              estimated_monthly_savings_usd=estimated,
                              evidence=evidence_base + f" untagged_threshold={untagged_threshold}.",
                              recommendation="Review untagged images and deployment workflows. Untagged images are often safe lifecycle candidates after rollback and release-retention requirements are confirmed.",
                              remediation_risk="Medium - untagged images can still be referenced by digest. Confirm deployment tooling and rollback requirements before deletion."
                          )

                      if old_image_count > 0:
                          add_finding(
                              findings,
                              category="ECR Old Image Review",
                              confidence="Medium",
                              severity="Low",
                              region=region,
                              service="ECR",
                              resource_id=repo_arn,
                              resource_name=repo_name,
                              estimated_monthly_savings_usd=estimated,
                              evidence=evidence_base + f" old_image_threshold_days={old_image_days}.",
                              recommendation="Review old container images for lifecycle expiration. Keep enough image history for rollback, audit, and release traceability.",
                              remediation_risk="Medium - old images may be needed for rollback, rebuild, audit, or incident recovery. Confirm retention policy before cleanup."
                          )

                      if image_count >= large_image_count or total_size_bytes >= large_storage_bytes:
                          add_finding(
                              findings,
                              category="ECR Large Repository Review",
                              confidence="Medium",
                              severity="Low",
                              region=region,
                              service="ECR",
                              resource_id=repo_arn,
                              resource_name=repo_name,
                              estimated_monthly_savings_usd=estimated,
                              evidence=evidence_base + f" large_image_count_threshold={large_image_count}; large_storage_gib_threshold={large_storage_gib}.",
                              recommendation="Review this repository for image retention, lifecycle policy coverage, large image layers, and whether older release images can be expired.",
                              remediation_risk="Medium - large repositories may support active deployment pipelines and rollback windows. Confirm owner and retention needs first."
                          )

          def parse_tagging_resource_type(arn):
              try:
                  parts = str(arn).split(":", 5)
                  service = parts[2] if len(parts) > 2 else "unknown"
                  resource = parts[5] if len(parts) > 5 else "unknown"
                  resource_type = resource.split("/")[0].split(":")[0] if resource else "unknown"
                  return f"{service}:{resource_type}"
              except Exception:
                  return "unknown"

          def estimate_rds_storage_monthly(allocated_gib, storage_type, config):
              assumptions = config.get("cost_assumptions", {})
              # Conservative baseline estimates; actual RDS storage pricing varies by engine, Region, storage type, and deployment model.
              default_rate = assumptions.get("rds_gp2_gib_monthly_usd", assumptions.get("rds_storage_gib_monthly_usd", 0.115))
              rate_map = {
                  "gp2": assumptions.get("rds_gp2_gib_monthly_usd", default_rate),
                  "gp3": assumptions.get("rds_gp3_gib_monthly_usd", default_rate),
                  "io1": assumptions.get("rds_io1_gib_monthly_usd", default_rate),
                  "io2": assumptions.get("rds_io2_gib_monthly_usd", default_rate),
                  "standard": assumptions.get("rds_standard_gib_monthly_usd", default_rate)
              }
              try:
                  return round(float(allocated_gib or 0) * float(rate_map.get(storage_type, default_rate)), 2)
              except Exception:
                  return None

          def estimate_rds_snapshot_monthly(size_gib, config):
              assumptions = config.get("cost_assumptions", {})
              rate = assumptions.get("rds_snapshot_gib_monthly_usd", 0.095)
              try:
                  return round(float(size_gib or 0) * float(rate), 2)
              except Exception:
                  return None

          def scan_rds_phase1_region(region, config, findings, warnings):
              rds = boto3.client("rds", region_name=region)
              thresholds = config.get("thresholds", {})
              large_storage_gib = int(thresholds.get("rds_large_allocated_storage_gib", 500))
              long_retention_days = int(thresholds.get("rds_long_backup_retention_days", 14))
              old_snapshot_days = int(thresholds.get("rds_old_snapshot_days", 180))
              clumio_airgap_review_days = int(thresholds.get("rds_clumio_airgap_review_days", 120))
              now = utc_now()

              db_count = 0
              multi_az_count = 0
              performance_insights_count = 0
              large_storage_count = 0
              long_retention_count = 0
              stopped_count = 0
              total_allocated_gib = 0

              try:
                  paginator = rds.get_paginator("describe_db_instances")
                  for page in paginator.paginate():
                      for db in page.get("DBInstances", []):
                          db_count += 1
                          db_id = db.get("DBInstanceIdentifier")
                          arn = db.get("DBInstanceArn", db_id)
                          engine = db.get("Engine", "unknown")
                          instance_class = db.get("DBInstanceClass", "unknown")
                          status = db.get("DBInstanceStatus", "unknown")
                          allocated_gib = int(db.get("AllocatedStorage", 0) or 0)
                          max_allocated_gib = db.get("MaxAllocatedStorage")
                          storage_type = db.get("StorageType", "unknown")
                          multi_az = bool(db.get("MultiAZ", False))
                          backup_retention = int(db.get("BackupRetentionPeriod", 0) or 0)
                          perf_enabled = bool(db.get("PerformanceInsightsEnabled", False))
                          perf_retention = db.get("PerformanceInsightsRetentionPeriod")
                          deletion_protection = bool(db.get("DeletionProtection", False))
                          publicly_accessible = bool(db.get("PubliclyAccessible", False))

                          total_allocated_gib += allocated_gib
                          if multi_az:
                              multi_az_count += 1
                          if perf_enabled:
                              performance_insights_count += 1
                          if status in ["stopped", "stopping"]:
                              stopped_count += 1

                          monthly_storage = estimate_rds_storage_monthly(allocated_gib, storage_type, config)
                          evidence_base = (
                              f"db_identifier={db_id}; engine={engine}; class={instance_class}; status={status}; "
                              f"allocated_storage_gib={allocated_gib}; max_allocated_storage_gib={max_allocated_gib}; storage_type={storage_type}; "
                              f"multi_az={multi_az}; backup_retention_days={backup_retention}; performance_insights_enabled={perf_enabled}; "
                              f"performance_insights_retention_days={perf_retention}; deletion_protection={deletion_protection}; publicly_accessible={publicly_accessible}."
                          )

                          if allocated_gib >= large_storage_gib:
                              large_storage_count += 1
                              add_finding(
                                  findings,
                                  category="RDS Large Allocated Storage Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="RDS",
                                  resource_id=arn,
                                  resource_name=db_id,
                                  estimated_monthly_savings_usd=monthly_storage,
                                  evidence=f"{evidence_base} threshold_gib={large_storage_gib}.",
                                  recommendation="Review allocated storage, autoscaling maximum storage, actual database size, growth trend, and whether storage can be right-sized during a maintenance window. Do not reduce storage without a migration/restore plan because RDS storage reduction is not a simple in-place operation for many engines.",
                                  remediation_risk="Medium - storage changes can require migration, maintenance windows, or restore workflows. Validate engine limitations and growth requirements."
                              )

                          if backup_retention >= long_retention_days:
                              long_retention_count += 1
                              add_finding(
                                  findings,
                                  category="RDS Long Backup Retention Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="RDS",
                                  resource_id=arn,
                                  resource_name=db_id,
                                  estimated_monthly_savings_usd=None,
                                  evidence=f"{evidence_base} retention_threshold_days={long_retention_days}.",
                                  recommendation="Review whether automated backup retention is longer than required for this environment. Confirm business, audit, and recovery requirements before reducing retention.",
                                  remediation_risk="Medium - reducing backup retention reduces point-in-time recovery window. Confirm RPO/RTO and compliance requirements."
                              )

                          if perf_enabled:
                              add_finding(
                                  findings,
                                  category="RDS Performance Insights / Database Insights Visibility Review",
                                  confidence="Medium",
                                  severity="Low",
                                  region=region,
                                  service="RDS",
                                  resource_id=arn,
                                  resource_name=db_id,
                                  estimated_monthly_savings_usd=None,
                                  evidence=evidence_base,
                                  recommendation="Performance Insights / Database Insights is enabled. This may be appropriate for production databases. Review whether the observability mode and retention period match the database environment, troubleshooting needs, and cost expectations. Do not disable blindly.",
                                  remediation_risk="Low to Medium - disabling or reducing retention can remove performance history used for troubleshooting, incident response, and performance baselining. Confirm operational requirements with the application/database owner first."
                              )

                          if status in ["stopped", "stopping"]:
                              add_finding(
                                  findings,
                                  category="RDS Stopped DB Instance Storage Review",
                                  confidence="High",
                                  severity="Low",
                                  region=region,
                                  service="RDS",
                                  resource_id=arn,
                                  resource_name=db_id,
                                  estimated_monthly_savings_usd=monthly_storage,
                                  evidence=evidence_base,
                                  recommendation="Review whether this stopped DB instance is still needed. Stopped RDS instances still incur storage and backup costs. Snapshot/export as required before deletion.",
                                  remediation_risk="Medium - stopped databases may be retained for rollback, audit, seasonal use, or recovery. Confirm ownership before deletion."
                              )

                  if db_count > 0:
                      add_finding(
                          findings,
                          category="RDS DB Instance Inventory Summary",
                          confidence="Info",
                          severity="Info",
                          region=region,
                          service="RDS",
                          resource_id=f"rds-summary-{region}",
                          resource_name=f"RDS Summary {region}",
                          estimated_monthly_savings_usd=0.0,
                          evidence=f"db_instance_count={db_count}; total_allocated_storage_gib={total_allocated_gib}; multi_az_count={multi_az_count}; performance_insights_enabled_count={performance_insights_count}; stopped_count={stopped_count}; large_storage_count={large_storage_count}; long_backup_retention_count={long_retention_count}.",
                          recommendation="Use this summary to validate RDS coverage in this Region and prioritize deeper review for storage, backup retention, Multi-AZ, and Performance Insights settings.",
                          remediation_risk="None - informational summary only."
                      )
              except Exception as e:
                  warnings.append({"region": region, "service": "RDS", "operation": "DescribeDBInstances", "error": str(e)})

              try:
                  snapshot_count = 0
                  old_manual_snapshot_count = 0
                  paginator = rds.get_paginator("describe_db_snapshots")
                  for page in paginator.paginate(SnapshotType="manual"):
                      for snap in page.get("DBSnapshots", []):
                          snapshot_count += 1
                          snap_id = snap.get("DBSnapshotIdentifier")
                          arn = snap.get("DBSnapshotArn", snap_id)
                          db_id = snap.get("DBInstanceIdentifier", "")
                          engine = snap.get("Engine", "unknown")
                          status = snap.get("Status", "unknown")
                          allocated_gib = int(snap.get("AllocatedStorage", 0) or 0)
                          created = snap.get("SnapshotCreateTime")
                          age_days = (now - created).days if created else None
                          encrypted = bool(snap.get("Encrypted", False))
                          estimated = estimate_rds_snapshot_monthly(allocated_gib, config)

                          is_clumio_snapshot = str(snap_id or "").startswith("clumio-airgap-encrypted")
                          is_restore_test_source = str(db_id or "").startswith("awsbackup-restore-test-")
                          snapshot_review_threshold_days = clumio_airgap_review_days if is_clumio_snapshot else old_snapshot_days

                          if age_days is not None and age_days >= snapshot_review_threshold_days:
                              old_manual_snapshot_count += 1

                              if is_clumio_snapshot:
                                  snapshot_category = "Clumio Air-Gap RDS Snapshot Retention Review"
                                  snapshot_confidence = "High"
                                  snapshot_severity = "Medium" if is_restore_test_source else "Low"
                                  snapshot_recommendation = "This snapshot appears to be Clumio air-gap related and is older than the Clumio air-gap review threshold. It is likely outside the native RDS PITR window, but it may still be required for Clumio retention, ransomware recovery, compliance, legal hold, archive lock, or another backup-policy exception. Do not delete directly from RDS until Clumio/backup ownership confirms it is expired or orphaned. If approved, tag PendingDelete first and use a review window before deletion."
                                  snapshot_risk = "Medium - Clumio air-gap snapshots are backup-system artifacts. Direct deletion from RDS can conflict with backup catalog expectations or remove intended ransomware/compliance recovery points. Prefer Clumio policy cleanup or documented backup-owner approval."
                              else:
                                  snapshot_category = "Old Manual RDS Snapshot Review"
                                  snapshot_confidence = "Medium"
                                  snapshot_severity = "Low"
                                  snapshot_recommendation = "Review whether this manual RDS snapshot is still needed. Delete only after confirming retention, rollback, audit, and compliance requirements."
                                  snapshot_risk = "Medium - manual snapshots may be retained for audit, rollback, migration, or disaster recovery. Confirm owner approval before deletion."

                              add_finding(
                                  findings,
                                  category=snapshot_category,
                                  confidence=snapshot_confidence,
                                  severity=snapshot_severity,
                                  region=region,
                                  service="RDS Snapshot",
                                  resource_id=arn,
                                  resource_name=snap_id,
                                  estimated_monthly_savings_usd=estimated,
                                  evidence=f"snapshot_id={snap_id}; db_instance={db_id}; engine={engine}; status={status}; age_days={age_days}; allocated_storage_gib={allocated_gib}; encrypted={encrypted}; threshold_days={snapshot_review_threshold_days}; rds_old_snapshot_threshold_days={old_snapshot_days}; clumio_airgap_review_threshold_days={clumio_airgap_review_days}; likely_clumio_snapshot={is_clumio_snapshot}; likely_aws_backup_restore_test_source={is_restore_test_source}; deletion_guidance=do_not_delete_directly_if_clumio_tagged.",
                                  recommendation=snapshot_recommendation,
                                  remediation_risk=snapshot_risk
                              )

                  if snapshot_count > 0:


                      add_finding(

                          findings,

                          category="RDS Manual Snapshot Inventory Summary",
                          confidence="Info",
                          severity="Info",
                          region=region,
                          service="RDS",
                          resource_id=f"rds-manual-snapshot-summary-{region}",
                          resource_name=f"RDS Manual Snapshot Summary {region}",
                          estimated_monthly_savings_usd=0.0,
                          evidence=f"manual_snapshot_count={snapshot_count}; old_manual_snapshot_count={old_manual_snapshot_count}; old_snapshot_threshold_days={old_snapshot_days}; clumio_airgap_review_threshold_days={clumio_airgap_review_days}.",
                          recommendation="Use this summary to validate RDS manual snapshot coverage and identify Regions with retained snapshots that may need owner review.",
                          remediation_risk="None - informational summary only."
                      )
              except Exception as e:
                  warnings.append({"region": region, "service": "RDS", "operation": "DescribeDBSnapshots", "error": str(e)})

          def scan_resource_discovery_region(region, config, findings, warnings, discovery_counts):
              tagging = boto3.client("resourcegroupstaggingapi", region_name=region)
              try:
                  paginator = tagging.get_paginator("get_resources")
                  for page in paginator.paginate(ResourcesPerPage=100):
                      for item in page.get("ResourceTagMappingList", []):
                          arn = item.get("ResourceARN", "")
                          rtype = parse_tagging_resource_type(arn)
                          discovery_counts[rtype] = discovery_counts.get(rtype, 0) + 1
              except Exception as e:
                  warnings.append({"region": region, "service": "Resource Groups Tagging API", "operation": "GetResources", "error": str(e)})

          def build_collapsible_list_evidence(title, items, preview_count=25):
              newline = chr(10)
              safe_items = [str(item) for item in items]
              preview = safe_items[:preview_count]
              remaining = safe_items[preview_count:]
              lines = [title]
              lines.extend(preview if preview else ["  - none"])
              if remaining:
                  lines.append(f"[[DETAILS:Show remaining {len(remaining)} items]]")
                  lines.extend(remaining)
                  lines.append("[[/DETAILS]]")
              return newline.join(lines)

          def add_resource_discovery_summary(findings, discovery_counts):
              analyzed_prefixes = [
                  "ec2:instance", "ec2:volume", "ec2:snapshot", "ec2:natgateway", "ec2:elastic-ip",
                  "elasticloadbalancing:loadbalancer", "elasticloadbalancing:targetgroup",
                  "logs:log-group", "lambda:function", "rds:db", "rds:snapshot", "s3:bucket",
                  "globalaccelerator:accelerator", "appstream:fleet", "appstream:image-builder",
                  "backup:recovery-point", "elasticfilesystem:file-system", "ecr:repository", "dynamodb:table", "opensearch:domain"
              ]
              sorted_seen = sorted(discovery_counts.items(), key=lambda item: (-item[1], item[0]))
              not_deeply_analyzed = []
              for rtype, count in sorted_seen:
                  if not any(rtype.startswith(prefix) for prefix in analyzed_prefixes):
                      not_deeply_analyzed.append((rtype, count))

              newline = chr(10)
              top_seen_lines = [f"  - {rtype}: {count}" for rtype, count in sorted_seen]
              gap_lines = [f"  - {rtype}: {count}" for rtype, count in not_deeply_analyzed]

              evidence = newline.join([
                  f"Tagged resource types seen: {len(discovery_counts)}",
                  f"Tagged resources not deeply analyzed types: {len(not_deeply_analyzed)}",
                  "",
                  build_collapsible_list_evidence("Top tagged resource types seen:", top_seen_lines, 25),
                  "",
                  build_collapsible_list_evidence("Discovered tagged resource types not deeply analyzed yet:", gap_lines, 25)
              ])

              add_finding(
                  findings,
                  category="Resource Discovery Coverage Gap Summary",
                  confidence="Info",
                  severity="Info",
                  region="global",
                  service="Resource Discovery",
                  resource_id="resource-discovery-summary",
                  resource_name="Resource Discovery Coverage Gap Summary",
                  estimated_monthly_savings_usd=0.0,
                  evidence=evidence,
                  recommendation="Use this summary as a blind-spot indicator. Resource Groups Tagging API only returns tagged or previously tagged resources, so absence here does not prove a resource type does not exist. Prioritize high-count, cost-impacting services for future deep checks.",
                  remediation_risk="None - informational summary only."
              )

          def get_ec2_pricing_operating_system(instance):
              platform = str(instance.get("Platform", "") or "").lower()
              platform_details = str(instance.get("PlatformDetails", "") or "").lower()
              if "windows" in platform or "windows" in platform_details:
                  return "Windows"
              return "Linux"

          def get_ec2_ondemand_hourly_price(instance_type, region, operating_system, warnings, price_cache):
              cache_key = (instance_type, region, operating_system)
              if cache_key in price_cache:
                  return price_cache[cache_key]
              try:
                  pricing = boto3.client("pricing", region_name="us-east-1")
                  filters = [
                      {"Type": "TERM_MATCH", "Field": "instanceType", "Value": instance_type},
                      {"Type": "TERM_MATCH", "Field": "regionCode", "Value": region},
                      {"Type": "TERM_MATCH", "Field": "operatingSystem", "Value": operating_system},
                      {"Type": "TERM_MATCH", "Field": "tenancy", "Value": "Shared"},
                      {"Type": "TERM_MATCH", "Field": "preInstalledSw", "Value": "NA"},
                      {"Type": "TERM_MATCH", "Field": "capacitystatus", "Value": "Used"}
                  ]
                  response = pricing.get_products(ServiceCode="AmazonEC2", Filters=filters, MaxResults=1)
                  price_list = response.get("PriceList", [])
                  if not price_list:
                      price_cache[cache_key] = None
                      return None
                  product = json.loads(price_list[0])
                  terms = product.get("terms", {}).get("OnDemand", {})
                  for term in terms.values():
                      for dim in term.get("priceDimensions", {}).values():
                          usd = dim.get("pricePerUnit", {}).get("USD")
                          if usd is not None:
                              price_cache[cache_key] = float(usd)
                              return price_cache[cache_key]
                  price_cache[cache_key] = None
                  return None
              except Exception as e:
                  warnings.append({"region": region, "service": "AWS Price List", "operation": "GetProducts", "resource": instance_type, "error": str(e)})
                  price_cache[cache_key] = None
                  return None

          def get_ec2_cpu_stats(cloudwatch, instance_id, lookback_days):
              end = utc_now()
              start = end - datetime.timedelta(days=lookback_days)
              try:
                  response = cloudwatch.get_metric_statistics(
                      Namespace="AWS/EC2",
                      MetricName="CPUUtilization",
                      Dimensions=[{"Name": "InstanceId", "Value": instance_id}],
                      StartTime=start,
                      EndTime=end,
                      Period=3600,
                      Statistics=["Average", "Maximum"]
                  )
                  datapoints = response.get("Datapoints", [])
                  if not datapoints:
                      return None, None, 0
                  avg_values = [float(dp.get("Average", 0.0)) for dp in datapoints if dp.get("Average") is not None]
                  max_values = [float(dp.get("Maximum", 0.0)) for dp in datapoints if dp.get("Maximum") is not None]
                  avg_cpu = sum(avg_values) / len(avg_values) if avg_values else None
                  max_cpu = max(max_values) if max_values else None
                  return avg_cpu, max_cpu, len(datapoints)
              except Exception:
                  return None, None, 0

          def next_generation_instance_type(instance_type):
              if "." not in instance_type:
                  return None
              family, size = instance_type.split(".", 1)
              same_size_map = {
                  "m3": "m6i", "m4": "m6i", "m5": "m7i",
                  "c3": "c6i", "c4": "c6i", "c5": "c7i",
                  "r3": "r6i", "r4": "r6i", "r5": "r7i",
                  "t2": "t3", "t3": "t3"
              }
              candidate_family = same_size_map.get(family)
              if not candidate_family:
                  return None
              return f"{candidate_family}.{size}"

          def downsize_instance_type(instance_type):
              if "." not in instance_type:
                  return None
              family, size = instance_type.split(".", 1)
              size_order = ["nano", "micro", "small", "medium", "large", "xlarge", "2xlarge", "4xlarge", "8xlarge", "9xlarge", "10xlarge", "12xlarge", "16xlarge", "18xlarge", "24xlarge", "32xlarge"]
              if size not in size_order:
                  return None
              idx = size_order.index(size)
              if idx <= 0:
                  return None
              return f"{family}.{size_order[idx - 1]}"

          def scan_ec2_modernization_rightsizing_region(region, config, findings, warnings):
              thresholds = config.get("thresholds", {})
              lookback_days = int(thresholds.get("ec2_rightsizing_metrics_lookback_days", 14))
              low_avg_cpu = float(thresholds.get("ec2_rightsizing_low_cpu_average_percent", 10))
              low_max_cpu = float(thresholds.get("ec2_rightsizing_low_cpu_max_percent", 25))
              min_savings = float(thresholds.get("ec2_rightsizing_min_monthly_savings_usd", 10))
              hours_per_month = 730
              price_cache = {}
              running_instances_seen = 0
              previous_generation_candidates_seen = 0
              low_cpu_candidates_seen = 0
              bursty_low_average_instances_seen = 0
              price_lookup_unavailable_count = 0
              cpu_metrics_missing_count = 0
              findings_emitted = 0
              ec2 = boto3.client("ec2", region_name=region)
              cloudwatch = boto3.client("cloudwatch", region_name=region)

              try:
                  paginator = ec2.get_paginator("describe_instances")
                  for page in paginator.paginate(Filters=[{"Name": "instance-state-name", "Values": ["running"]}]):
                      for reservation in page.get("Reservations", []):
                          for instance in reservation.get("Instances", []):
                              instance_id = instance.get("InstanceId")
                              instance_type = instance.get("InstanceType")
                              if not instance_id or not instance_type:
                                  continue
                              running_instances_seen += 1
                              tags = {t.get("Key", ""): t.get("Value", "") for t in instance.get("Tags", [])}
                              name = tags.get("Name", instance_id)
                              os_label = get_ec2_pricing_operating_system(instance)
                              avg_cpu, max_cpu, datapoints = get_ec2_cpu_stats(cloudwatch, instance_id, lookback_days)
                              current_price = get_ec2_ondemand_hourly_price(instance_type, region, os_label, warnings, price_cache)
                              current_monthly = round(current_price * hours_per_month, 2) if current_price is not None else None

                              modern_candidate = next_generation_instance_type(instance_type)
                              if modern_candidate:
                                  previous_generation_candidates_seen += 1
                                  candidate_price = get_ec2_ondemand_hourly_price(modern_candidate, region, os_label, warnings, price_cache)
                                  candidate_monthly = round(candidate_price * hours_per_month, 2) if candidate_price is not None else None
                                  estimated = None
                                  if current_monthly is not None and candidate_monthly is not None:
                                      estimated = round(max(current_monthly - candidate_monthly, 0.0), 2)
                                  if current_price is None or candidate_price is None:
                                      price_lookup_unavailable_count += 1
                                  if estimated is None or estimated >= min_savings:
                                      findings_emitted += 1
                                      add_finding(
                                          findings,
                                          category="EC2 Previous Generation Instance Review",
                                          confidence="Medium",
                                          severity="Low",
                                          region=region,
                                          service="EC2",
                                          resource_id=instance_id,
                                          resource_name=name,
                                          estimated_monthly_savings_usd=estimated,
                                          evidence=f"instance_id={instance_id}; instance_type={instance_type}; candidate_same_size_modernization={modern_candidate}; operating_system={os_label}; avg_cpu={avg_cpu}; max_cpu={max_cpu}; cpu_datapoints={datapoints}; current_estimated_monthly_usd={current_monthly}; candidate_estimated_monthly_usd={candidate_monthly}; pricing_source=AWS Price List API OnDemand Shared tenancy; savings_threshold={min_savings}.",
                                          recommendation="Review this as a previous-generation EC2 modernization candidate. Same-size modernization may improve price/performance or platform support, but validate AMI, drivers, ENA/NVMe behavior, licensing, application compatibility, Savings Plans/RIs, and change windows before modifying the instance type.",
                                          remediation_risk="Medium - changing instance families can affect CPU generation, network/EBS bandwidth, drivers, licensing, and application behavior. Admin review required."
                                      )

                              if avg_cpu is None or max_cpu is None:
                                  cpu_metrics_missing_count += 1

                              if avg_cpu is not None and max_cpu is not None and avg_cpu <= low_avg_cpu and max_cpu > low_max_cpu:
                                  bursty_low_average_instances_seen += 1
                                  add_finding(
                                      findings,
                                      category="EC2 Bursty Low-Average Instance Review",
                                      confidence="Medium",
                                      severity="Info",
                                      region=region,
                                      service="EC2",
                                      resource_id=instance_id,
                                      resource_name=name,
                                      estimated_monthly_savings_usd=0.0,
                                      evidence=f"instance_id={instance_id}; instance_type={instance_type}; operating_system={os_label}; avg_cpu={round(avg_cpu, 2)}; max_cpu={round(max_cpu, 2)}; cpu_datapoints={datapoints}; lookback_days={lookback_days}; avg_cpu_threshold={low_avg_cpu}; max_cpu_threshold={low_max_cpu}; current_estimated_monthly_usd={current_monthly}; decision=not_a_downsize_candidate_due_to_high_peak_cpu.",
                                      recommendation="Average CPU is low, but peak CPU exceeded the rightsizing threshold. Treat this as a bursty workload signal. Same-size modernization may still be reviewed, but do not downsize based on CPU average alone; validate peak usage, login patterns, memory, network, EBS throughput, and business schedule.",
                                      remediation_risk="None - informational caution only. This finding intentionally avoids recommending a smaller instance type because peak CPU was high."
                                  )

                              if avg_cpu is not None and max_cpu is not None and avg_cpu <= low_avg_cpu and max_cpu <= low_max_cpu:
                                  low_cpu_candidates_seen += 1
                                  downsize_candidate = downsize_instance_type(instance_type)
                                  if downsize_candidate:
                                      downsize_price = get_ec2_ondemand_hourly_price(downsize_candidate, region, os_label, warnings, price_cache)
                                      downsize_monthly = round(downsize_price * hours_per_month, 2) if downsize_price is not None else None
                                      estimated = None
                                      if current_monthly is not None and downsize_monthly is not None:
                                          estimated = round(max(current_monthly - downsize_monthly, 0.0), 2)
                                      if current_price is None or downsize_price is None:
                                          price_lookup_unavailable_count += 1
                                      if estimated is None or estimated >= min_savings:
                                          findings_emitted += 1
                                          add_finding(
                                              findings,
                                              category="EC2 Low Utilization Rightsizing Candidate",
                                              confidence="Low",
                                              severity="Low",
                                              region=region,
                                              service="EC2",
                                              resource_id=instance_id,
                                              resource_name=name,
                                              estimated_monthly_savings_usd=estimated,
                                              evidence=f"instance_id={instance_id}; current_instance_type={instance_type}; possible_downsize_candidate={downsize_candidate}; operating_system={os_label}; avg_cpu={round(avg_cpu, 2)}; max_cpu={round(max_cpu, 2)}; cpu_datapoints={datapoints}; lookback_days={lookback_days}; current_estimated_monthly_usd={current_monthly}; candidate_estimated_monthly_usd={downsize_monthly}; pricing_source=AWS Price List API OnDemand Shared tenancy; memory_not_evaluated=True; savings_threshold={min_savings}.",
                                              recommendation="Review this as a possible EC2 rightsizing candidate, not a direct resize instruction. CPU appears low, but memory, network throughput, EBS bandwidth, burst behavior, licensing, application role, batch schedules, and business context must be validated before choosing any smaller size.",
                                              remediation_risk="Medium to High - downsizing can reduce memory, vCPU, network, and EBS throughput. CPU-only data is insufficient for an automatic recommendation."
                                          )
                  if running_instances_seen > 0 and findings_emitted == 0:
                      add_finding(
                          findings,
                          category="EC2 Modernization and Rightsizing Inventory Summary",
                          confidence="Info",
                          severity="Info",
                          region=region,
                          service="EC2",
                          resource_id=f"ec2-modernization-rightsizing-summary-{region}",
                          resource_name=f"EC2 Modernization and Rightsizing Summary ({region})",
                          estimated_monthly_savings_usd=0.0,
                          evidence=f"running_instances_seen={running_instances_seen}; previous_generation_candidates_seen={previous_generation_candidates_seen}; low_cpu_candidates_seen={low_cpu_candidates_seen}; bursty_low_average_instances_seen={bursty_low_average_instances_seen}; cpu_metrics_missing_count={cpu_metrics_missing_count}; price_lookup_unavailable_count={price_lookup_unavailable_count}; findings_emitted={findings_emitted}; lookback_days={lookback_days}; low_avg_cpu_threshold={low_avg_cpu}; low_max_cpu_threshold={low_max_cpu}; min_savings_threshold={min_savings}.",
                          recommendation="No EC2 modernization or rightsizing findings met the current report thresholds in this Region. This summary confirms the scanner ran and explains the evaluated signals. Review thresholds, running instance inventory, CPU data availability, and pricing lookup access if you expected candidates.",
                          remediation_risk="None - informational scanner summary only."
                      )
              except Exception as e:
                  warnings.append({"region": region, "service": "EC2", "operation": "scan_ec2_modernization_rightsizing_region", "error": str(e), "trace": traceback.format_exc()})

          def scan_aws_backup_region(region, config, findings, warnings):
              backup = boto3.client("backup", region_name=region)
              thresholds = config.get("thresholds", {})
              old_days = int(thresholds.get("aws_backup_old_recovery_point_days", 365))
              large_vault_count = int(thresholds.get("aws_backup_large_vault_recovery_point_count", 100))
              copied_detail_enabled = is_check_enabled(config, "aws_backup_copied_recovery_point_details", False)
              copied_detail_min_age_days = int(thresholds.get("aws_backup_copied_recovery_point_detail_min_age_days", 90))
              copied_summary = {}
              now = utc_now()

              try:
                  vault_pages = backup.get_paginator("list_backup_vaults").paginate()
              except Exception as e:
                  warnings.append({"region": region, "service": "AWS Backup", "operation": "ListBackupVaults", "error": str(e)})
                  return

              for page in vault_pages:
                  for vault in page.get("BackupVaultList", []):
                      vault_name = vault.get("BackupVaultName")
                      vault_arn = vault.get("BackupVaultArn")
                      recovery_point_count = int(vault.get("NumberOfRecoveryPoints", 0) or 0)

                      if recovery_point_count >= large_vault_count:
                          add_finding(
                              findings,
                              category="AWS Backup Large Vault Review",
                              confidence="Medium",
                              severity="Low",
                              region=region,
                              service="AWS Backup",
                              resource_id=vault_arn or vault_name,
                              resource_name=vault_name,
                              estimated_monthly_savings_usd=None,
                              evidence=f"backup_vault={vault_name}; recovery_point_count={recovery_point_count}; threshold={large_vault_count}.",
                              recommendation="Review backup vault retention policy, backup plans, copy actions, and whether old recovery points are still required.",
                              remediation_risk="Medium - backup vault cleanup can remove recovery capability. Confirm compliance, audit, legal hold, and restore requirements first."
                          )

                      try:
                          rp_pages = backup.get_paginator("list_recovery_points_by_backup_vault").paginate(BackupVaultName=vault_name)
                          for rp_page in rp_pages:
                              for rp in rp_page.get("RecoveryPoints", []):
                                  rp_arn = rp.get("RecoveryPointArn")
                                  resource_arn = rp.get("ResourceArn", "")
                                  resource_type = rp.get("ResourceType", "unknown")
                                  status = rp.get("Status", "unknown")
                                  created = rp.get("CreationDate")
                                  calculated_lifecycle = rp.get("CalculatedLifecycle", {}) or {}
                                  lifecycle = rp.get("Lifecycle", {}) or {}
                                  delete_at = calculated_lifecycle.get("DeleteAt") or lifecycle.get("DeleteAfterDays")
                                  move_to_cold = calculated_lifecycle.get("MoveToColdStorageAt") or lifecycle.get("MoveToColdStorageAfterDays")
                                  size_bytes = rp.get("BackupSizeInBytes")
                                  age_days = (now - created).days if created else None
                                  is_cold = bool(rp.get("IsParent", False))
                                  source_vault_arn = rp.get("SourceBackupVaultArn", "")
                                  created_by = rp.get("CreatedBy", {}) or {}
                                  backup_plan_id = created_by.get("BackupPlanId", "")
                                  backup_rule_id = created_by.get("BackupRuleId", "")
                                  estimated = estimate_aws_backup_monthly(size_bytes, config)

                                  if age_days is not None and age_days >= old_days:
                                      add_finding(
                                          findings,
                                          category="AWS Backup Old Recovery Point Review",
                                          confidence="Medium",
                                          severity="Low",
                                          region=region,
                                          service="AWS Backup",
                                          resource_id=rp_arn,
                                          resource_name=vault_name,
                                          estimated_monthly_savings_usd=estimated,
                                          evidence=f"vault={vault_name}; resource_type={resource_type}; resource_arn={resource_arn}; status={status}; age_days={age_days}; size_bytes={size_bytes}; delete_at={delete_at}; move_to_cold={move_to_cold}; backup_plan_id={backup_plan_id}; backup_rule_id={backup_rule_id}.",
                                          recommendation="Review whether this old AWS Backup recovery point is still required. Delete only after confirming retention, compliance, legal hold, and restore requirements.",
                                          remediation_risk="Medium - recovery points may be required for compliance, audit, legal hold, ransomware recovery, or disaster recovery."
                                      )

                                  if not delete_at:
                                      add_finding(
                                          findings,
                                          category="AWS Backup Recovery Point Without Delete Lifecycle Review",
                                          confidence="Medium",
                                          severity="Low",
                                          region=region,
                                          service="AWS Backup",
                                          resource_id=rp_arn,
                                          resource_name=vault_name,
                                          estimated_monthly_savings_usd=estimated,
                                          evidence=f"vault={vault_name}; resource_type={resource_type}; resource_arn={resource_arn}; status={status}; age_days={age_days}; size_bytes={size_bytes}; no calculated DeleteAt/delete-after lifecycle was found; backup_plan_id={backup_plan_id}; backup_rule_id={backup_rule_id}.",
                                          recommendation="Review backup lifecycle rules. Recovery points without a delete lifecycle may be retained indefinitely and continue to add cost.",
                                          remediation_risk="Medium - lifecycle rules must align to business, audit, compliance, and recovery requirements."
                                      )

                                  if source_vault_arn:
                                      summary_key = (vault_name, source_vault_arn, resource_type, bool(delete_at))
                                      if summary_key not in copied_summary:
                                          copied_summary[summary_key] = {
                                              "count": 0,
                                              "total_size_bytes": 0,
                                              "oldest_age_days": None,
                                              "newest_age_days": None,
                                              "missing_delete_lifecycle_count": 0,
                                              "sample_recovery_point_arn": rp_arn,
                                              "sample_resource_arn": resource_arn
                                          }
                                      copied_summary[summary_key]["count"] += 1
                                      copied_summary[summary_key]["total_size_bytes"] += int(size_bytes or 0)
                                      if not delete_at:
                                          copied_summary[summary_key]["missing_delete_lifecycle_count"] += 1
                                      if age_days is not None:
                                          if copied_summary[summary_key]["oldest_age_days"] is None or age_days > copied_summary[summary_key]["oldest_age_days"]:
                                              copied_summary[summary_key]["oldest_age_days"] = age_days
                                          if copied_summary[summary_key]["newest_age_days"] is None or age_days < copied_summary[summary_key]["newest_age_days"]:
                                              copied_summary[summary_key]["newest_age_days"] = age_days

                                      should_emit_copied_detail = copied_detail_enabled or not delete_at or (age_days is not None and age_days >= copied_detail_min_age_days)
                                      if should_emit_copied_detail:
                                          add_finding(
                                              findings,
                                              category="AWS Backup Copied Recovery Point Review",
                                              confidence="Medium",
                                              severity="Low",
                                              region=region,
                                              service="AWS Backup",
                                              resource_id=rp_arn,
                                              resource_name=vault_name,
                                              estimated_monthly_savings_usd=estimated,
                                              evidence=f"vault={vault_name}; source_backup_vault_arn={source_vault_arn}; resource_type={resource_type}; resource_arn={resource_arn}; status={status}; age_days={age_days}; size_bytes={size_bytes}; delete_at={delete_at}; copied_detail_enabled={copied_detail_enabled}; copied_detail_min_age_days={copied_detail_min_age_days}.",
                                              recommendation="Review whether this copied recovery point is still required. Cross-Region or cross-account copies can duplicate storage cost. If size_bytes is 0, treat the $0.0 estimate as unknown/unavailable rather than proof that the copy is free; validate using AWS Backup vault metrics, Cost Explorer, or CUR.",
                                              remediation_risk="Medium - copied backups may be part of disaster recovery, ransomware protection, or compliance architecture."
                                          )
                      except Exception as e:
                          warnings.append({"region": region, "service": "AWS Backup", "operation": "ListRecoveryPointsByBackupVault", "resource": vault_name, "error": str(e)})

              for (summary_vault_name, summary_source_vault_arn, summary_resource_type, summary_has_delete_lifecycle), summary_values in sorted(copied_summary.items(), key=lambda item: (-item[1].get("count", 0), item[0][0], item[0][2])):
                  count = int(summary_values.get("count", 0) or 0)
                  total_size_bytes = int(summary_values.get("total_size_bytes", 0) or 0)
                  estimated_summary = estimate_aws_backup_monthly(total_size_bytes, config)
                  add_finding(
                      findings,
                      category="AWS Backup Copied Recovery Point Summary",
                      confidence="Info",
                      severity="Info",
                      region=region,
                      service="AWS Backup",
                      resource_id=f"aws-backup-copied-summary-{region}-{summary_vault_name}-{summary_resource_type}",
                      resource_name=summary_vault_name,
                      estimated_monthly_savings_usd=estimated_summary,
                      evidence=f"vault={summary_vault_name}; source_backup_vault_arn={summary_source_vault_arn}; resource_type={summary_resource_type}; copied_recovery_point_count={count}; total_size_bytes={total_size_bytes}; oldest_age_days={summary_values.get('oldest_age_days')}; newest_age_days={summary_values.get('newest_age_days')}; has_delete_lifecycle={summary_has_delete_lifecycle}; missing_delete_lifecycle_count={summary_values.get('missing_delete_lifecycle_count')}; sample_recovery_point_arn={summary_values.get('sample_recovery_point_arn')}; sample_resource_arn={summary_values.get('sample_resource_arn')}; detail_rows_emitted_when=details_enabled_or_missing_lifecycle_or_age_gte_{copied_detail_min_age_days}_days.",
                      recommendation="Summary of copied AWS Backup recovery points. Review backup copy policy, retention, lifecycle, and DR/compliance requirements. Detailed copied recovery point rows are suppressed by default unless older than the configured threshold, missing delete lifecycle, or detail mode is enabled. If total_size_bytes is 0, treat the $0.0 estimate as unknown/unavailable rather than proof that copied backup storage is free; validate using AWS Backup vault metrics, Cost Explorer, or CUR.",
                      remediation_risk="None - informational summary only. Do not delete recovery points without confirming restore, compliance, legal hold, and DR requirements."
                  )

          DIRECT_SAVINGS_CATEGORIES = {
              "Unused Elastic IP",
              "Idle NAT Gateway Review",
              "Unattached EBS Volume",
              "Stopped EC2 Instance With Attached EBS",
              "EBS gp2 Volume Conversion Review",
              "Load Balancer With No Target Groups",
              "Load Balancer With No Healthy Targets",
              "Load Balancer With Empty Target Groups Review",
              "Global Accelerator Review",
              "WorkSpaces Applications AppStream Fleet Review",
              "WorkSpaces Applications AppStream Image Builder Review",
          }

          GROSS_REVIEW_EXPOSURE_CATEGORIES = {
              "Old AMI Review",
              "Old Unassociated EBS Snapshot Review",
              "CloudWatch Log Group Without Retention",
              "Large CloudWatch Log Group Review",
              "S3 Bucket Without Lifecycle Review",
              "S3 Versioning Without Lifecycle Review",
              "Large S3 Bucket Review",
              "AWS Backup Old Recovery Point Review",
              "AWS Backup Recovery Point Without Delete Lifecycle Review",
              "AWS Backup Copied Recovery Point Review",
              "AWS Backup Copied Recovery Point Summary",
              "AWS Backup Large Vault Review",
              "RDS Large Allocated Storage Review",
              "RDS Long Backup Retention Review",
              "RDS Manual Snapshot Inventory Summary",
              "Old Manual RDS Snapshot Review",
              "Clumio Air-Gap RDS Snapshot Retention Review",
              "RDS Clumio Manual Snapshot Review",
              "RDS Snapshot From AWS Backup Restore Test + Clumio Tag Inheritance Review",
              "RDS Performance Insights / Database Insights Visibility Review",
              "EFS Standard Storage Optimization Review",
              "EFS Large File System Review",
              "ECR Large Repository Review",
              "ECR Old Image Review",
              "ECR Untagged Image Review",
              "OpenSearch Large Storage Review",
              "OpenSearch Instance Count Review",
              "OpenSearch Advanced Capacity Review",
              "DynamoDB Large Table Review",
          }

          def estimate_class_for_category(category):
              if category in DIRECT_SAVINGS_CATEGORIES:
                  return "Direct Savings"
              if category in GROSS_REVIEW_EXPOSURE_CATEGORIES:
                  return "Gross Review Exposure"
              return "Review / Unclassified"

          def summarize(findings, warnings, config, account_id, account_alias, regions, config_created, timings=None):
              by_category = {}
              by_region = {}
              by_confidence = {}
              gross_monthly = 0.0
              direct_monthly = 0.0
              review_monthly = 0.0
              estimated_count = 0
              direct_estimated_count = 0
              review_estimated_count = 0

              for f in findings:
                  category = f["category"]
                  by_category[category] = by_category.get(category, 0) + 1
                  by_region[f["region"]] = by_region.get(f["region"], 0) + 1
                  by_confidence[f["confidence"]] = by_confidence.get(f["confidence"], 0) + 1
                  estimate_class = estimate_class_for_category(category)
                  f["estimate_class"] = estimate_class
                  val = f.get("estimated_monthly_savings_usd")
                  if isinstance(val, (int, float)):
                      numeric_val = float(val)
                      gross_monthly += numeric_val
                      estimated_count += 1
                      if estimate_class == "Direct Savings":
                          direct_monthly += numeric_val
                          direct_estimated_count += 1
                      else:
                          review_monthly += numeric_val
                          review_estimated_count += 1

              return {
                  "project": PROJECT_NAME,
                  "template_version": TEMPLATE_VERSION,
                  "toolkit_version": TOOLKIT_VERSION,
                  "template_build": TEMPLATE_BUILD,
                  "account_id": account_id,
                  "account_name": config.get("account_name", account_id),
                  "account_alias": account_alias or "",
                  "environment": config.get("environment") or account_alias or "unknown",
                  "generated_at": iso_now(),
                  "mode": "ReportOnly",
                  "regions_scanned": regions,
                  "config_created": config_created,
                  "total_findings": len(findings),
                  "warnings_count": len(warnings),
                  "estimated_monthly_savings_usd_known_items": round(direct_monthly, 2),
                  "estimated_annual_savings_usd_known_items": round(direct_monthly * 12, 2),
                  "direct_monthly_savings_usd_known_items": round(direct_monthly, 2),
                  "direct_annual_savings_usd_known_items": round(direct_monthly * 12, 2),
                  "gross_monthly_review_exposure_usd_known_items": round(gross_monthly, 2),
                  "gross_annual_review_exposure_usd_known_items": round(gross_monthly * 12, 2),
                  "estimated_monthly_review_exposure_usd_known_items": round(gross_monthly, 2),
                  "estimated_annual_review_exposure_usd_known_items": round(gross_monthly * 12, 2),
                  "review_only_monthly_exposure_usd_known_items": round(review_monthly, 2),
                  "review_only_annual_exposure_usd_known_items": round(review_monthly * 12, 2),
                  "estimated_items_count": estimated_count,
                  "direct_estimated_items_count": direct_estimated_count,
                  "review_estimated_items_count": review_estimated_count,
                  "by_category": by_category,
                  "by_region": by_region,
                  "by_confidence": by_confidence,
                  "timings": timings or []
              }

          def shell_quote(value):
              text = str(value or "")
              return "'" + text.replace("'", "'\"'\"'") + "'"

          def build_cli_suggestions(finding):
              category = finding.get("category", "")
              service = finding.get("service", "")
              if finding.get("severity") == "Info" or finding.get("confidence") == "Info" or category == "S3 Account Inventory Summary":
                  return {}
              region = finding.get("region", "")
              resource_id = finding.get("resource_id", "")
              resource_name = finding.get("resource_name", "")
              target = resource_id or resource_name
              region_arg = f" --region {region}" if region and region != "global" else ""

              suggestions = {
                  "validation": [],
                  "possible_remediation": [],
                  "notes": []
              }

              if category == "Unused Elastic IP":
                  suggestions["validation"].append(f"aws ec2 describe-addresses --allocation-ids {shell_quote(resource_id)}{region_arg}")
                  suggestions["possible_remediation"].append(f"aws ec2 release-address --allocation-id {shell_quote(resource_id)}{region_arg}")
                  suggestions["notes"].append("Confirm DNS records, allow lists, vendor references, and owner approval before releasing an Elastic IP.")

              elif category == "Unattached EBS Volume":
                  suggestions["validation"].append(f"aws ec2 describe-volumes --volume-ids {shell_quote(resource_id)}{region_arg}")
                  suggestions["possible_remediation"].append(f"aws ec2 create-snapshot --volume-id {shell_quote(resource_id)} --description {shell_quote('Pre-delete snapshot for ' + resource_id)}{region_arg}")
                  suggestions["possible_remediation"].append(f"aws ec2 delete-volume --volume-id {shell_quote(resource_id)}{region_arg}")
                  suggestions["notes"].append("Create/verify a snapshot only if retention is required. Delete only after owner approval.")

              elif category == "Stopped EC2 Instance With Attached EBS":
                  suggestions["validation"].append(f"aws ec2 describe-instances --instance-ids {shell_quote(resource_id)}{region_arg}")
                  suggestions["possible_remediation"].append(f"aws ec2 create-image --instance-id {shell_quote(resource_id)} --name {shell_quote(resource_id + '-retirement-backup')} --no-reboot{region_arg}")
                  suggestions["notes"].append("Do not terminate until volume delete-on-termination settings, backups, and owner approval are confirmed.")

              elif category == "EBS gp2 Volume Conversion Review":
                  suggestions["validation"].append(f"aws ec2 describe-volumes --volume-ids {shell_quote(resource_id)}{region_arg}")
                  suggestions["possible_remediation"].append(f"aws ec2 modify-volume --volume-id {shell_quote(resource_id)} --volume-type gp3{region_arg}")
                  suggestions["notes"].append("Validate IOPS and throughput requirements before converting gp2 to gp3.")

              elif category == "Low Activity EBS Volume Review":
                  suggestions["validation"].append(f"aws ec2 describe-volumes --volume-ids {shell_quote(resource_id)}{region_arg}")
                  suggestions["validation"].append(f"aws cloudwatch get-metric-statistics --namespace AWS/EBS --metric-name VolumeReadOps --dimensions Name=VolumeId,Value={resource_id} --statistics Sum --period 86400 --start-time <START_UTC> --end-time <END_UTC>{region_arg}")
                  suggestions["notes"].append("Confirm filesystem and application usage from the attached instance before resizing, snapshotting, detaching, or deleting.")

              elif category == "Old Unassociated EBS Snapshot Review":
                  suggestions["validation"].append(f"aws ec2 describe-snapshots --snapshot-ids {shell_quote(resource_id)}{region_arg}")
                  suggestions["possible_remediation"].append(f"aws ec2 delete-snapshot --snapshot-id {shell_quote(resource_id)}{region_arg}")
                  suggestions["notes"].append("Confirm the snapshot is not required for compliance, rollback, migration, or disaster recovery before deletion.")

              elif category == "Old AMI Review":
                  suggestions["validation"].append(f"aws ec2 describe-images --image-ids {shell_quote(resource_id)}{region_arg}")
                  suggestions["possible_remediation"].append(f"aws ec2 deregister-image --image-id {shell_quote(resource_id)}{region_arg}")
                  suggestions["notes"].append("After deregistering an AMI, separately review backing snapshots before deleting them.")

              elif category in ["CloudWatch Log Group Without Retention", "Large CloudWatch Log Group Review"]:
                  suggestions["validation"].append(f"aws logs describe-log-groups --log-group-name-prefix {shell_quote(resource_id)}{region_arg}")
                  suggestions["possible_remediation"].append(f"aws logs put-retention-policy --log-group-name {shell_quote(resource_id)} --retention-in-days 90{region_arg}")
                  suggestions["notes"].append("Confirm audit, security, and troubleshooting retention requirements before setting or reducing retention.")

              elif category == "Lambda No Invocations Review":
                  suggestions["validation"].append(f"aws lambda get-function --function-name {shell_quote(resource_name or resource_id)}{region_arg}")
                  suggestions["validation"].append(f"aws lambda list-event-source-mappings --function-name {shell_quote(resource_name or resource_id)}{region_arg}")
                  suggestions["notes"].append("Check EventBridge rules, aliases, custom-resource usage, and break-glass procedures before deleting inactive functions.")

              elif "Load Balancer" in category or category == "Empty Target Group":
                  suggestions["validation"].append(f"aws elbv2 describe-load-balancers{region_arg}")
                  suggestions["validation"].append(f"aws elbv2 describe-target-groups --load-balancer-arn {shell_quote(resource_id)}{region_arg}")
                  suggestions["notes"].append("Confirm DNS records, listeners, certificates, listener rules, and application ownership before deleting load balancers or target groups.")

              elif category == "Idle NAT Gateway Review":
                  suggestions["validation"].append(f"aws ec2 describe-nat-gateways --nat-gateway-ids {shell_quote(resource_id)}{region_arg}")
                  suggestions["possible_remediation"].append(f"aws ec2 delete-nat-gateway --nat-gateway-id {shell_quote(resource_id)}{region_arg}")
                  suggestions["notes"].append("Confirm route tables, private subnet egress, patching paths, and VPC endpoint alternatives before deleting NAT Gateways.")

              elif category in ["Low Utilization EC2 Instance Review", "EC2 Previous Generation Instance Review", "EC2 Low Utilization Rightsizing Candidate"]:
                  suggestions["validation"].append(f"aws ec2 describe-instances --instance-ids {shell_quote(resource_id)}{region_arg}")
                  suggestions["validation"].append(f"aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization --dimensions Name=InstanceId,Value={resource_id} --statistics Average Maximum --period 86400 --start-time <START_UTC> --end-time <END_UTC>{region_arg}")
                  suggestions["notes"].append("Use owner knowledge, memory data, network/EBS throughput, licensing context, Savings Plans/RIs, and Compute Optimizer before resizing, scheduling, stopping, or terminating.")

              elif category.startswith("RDS") or category.startswith("Old Manual RDS") or service.startswith("RDS"):
                  if service == "RDS Snapshot" or "Snapshot" in category:
                      suggestions["validation"].append(f"aws rds describe-db-snapshots --db-snapshot-identifier {shell_quote(resource_id)}{region_arg}")
                      suggestions["possible_remediation"].append(f"aws rds delete-db-snapshot --db-snapshot-identifier {shell_quote(resource_id)}{region_arg}")
                      suggestions["notes"].append("Confirm snapshot retention, compliance, recovery, and rollback requirements before deletion.")
                  else:
                      suggestions["validation"].append(f"aws rds describe-db-instances --db-instance-identifier {shell_quote(resource_id)}{region_arg}")
                      suggestions["notes"].append("Database remediation is high-risk. Confirm application owner, backup, recovery, and maintenance requirements first.")

              elif category.startswith("S3"):
                  suggestions["validation"].append(f"aws s3api get-bucket-versioning --bucket {shell_quote(resource_id)}")
                  suggestions["validation"].append(f"aws s3api get-bucket-lifecycle-configuration --bucket {shell_quote(resource_id)} || echo 'No lifecycle configuration found - this confirms the finding if the error is NoSuchLifecycleConfiguration.'")
                  suggestions["possible_remediation"].append(f"aws s3api put-bucket-lifecycle-configuration --bucket {shell_quote(resource_id)} --lifecycle-configuration file://reviewed-lifecycle-policy.json")
                  suggestions["notes"].append("The lifecycle validation command may return NoSuchLifecycleConfiguration. That is expected for this finding and means no lifecycle policy currently exists.")
                  suggestions["notes"].append("Create reviewed-lifecycle-policy.json only after confirming object retention, versioning, legal hold, object lock, compliance, and application owner requirements.")

              elif category == "Global Accelerator Review":
                  suggestions["validation"].append(f"aws globalaccelerator describe-accelerator --accelerator-arn {shell_quote(resource_id)}")
                  suggestions["notes"].append("Confirm DNS, static anycast IP usage, listeners, endpoint groups, and application ownership before disabling or deleting accelerators.")

              suggestions = {k: v for k, v in suggestions.items() if v}
              return suggestions

          def enrich_findings_with_cli(findings):
              remediation_plan = []
              for f in findings:
                  cli = build_cli_suggestions(f)
                  if cli:
                      f["cli_suggestions"] = cli
                      remediation_plan.append({
                          "finding_id": f.get("finding_id"),
                          "category": f.get("category"),
                          "region": f.get("region"),
                          "service": f.get("service"),
                          "resource_id": f.get("resource_id"),
                          "resource_name": f.get("resource_name"),
                          "confidence": f.get("confidence"),
                          "severity": f.get("severity"),
                          "remediation_risk": f.get("remediation_risk"),
                          "cli_suggestions": cli
                      })
              return remediation_plan

          def make_csv(findings):
              output = io.StringIO()
              fieldnames = [
                  "finding_id", "category", "estimate_class", "confidence", "severity", "region", "service",
                  "resource_id", "resource_name", "estimated_monthly_savings_usd",
                  "evidence", "recommendation", "remediation_risk", "cli_suggestions", "mode", "timestamp"
              ]
              writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore")
              writer.writeheader()
              for f in findings:
                  writer.writerow(f)
              return output.getvalue()

          BACKUP_SNAPSHOT_REVIEW_CATEGORIES = {
              "Clumio Air-Gap RDS Snapshot Retention Review",
              "Old Manual RDS Snapshot Review",
              "RDS Manual Snapshot Inventory Summary",
              "AWS Backup Old Recovery Point Review",
              "AWS Backup Recovery Point Without Delete Lifecycle Review",
              "AWS Backup Copied Recovery Point Summary",
              "AWS Backup Large Vault Review"
          }

          def evidence_lookup(evidence, *keys):
              if not evidence:
                  return ""
              parts = [p.strip() for p in str(evidence).split(";")]
              kv = {}
              for part in parts:
                  if "=" in part:
                      k, v = part.split("=", 1)
                      kv[k.strip().lower()] = v.strip()
              for key in keys:
                  value = kv.get(str(key).lower())
                  if value not in (None, "", "None"):
                      return value
              return ""

          def basename_from_arn_or_name(value):
              if not value:
                  return ""
              value = str(value)
              if ":" in value:
                  return value.split(":")[-1].split("/")[-1]
              return value

          def make_backup_snapshot_review_csv(findings):
              output = io.StringIO()
              fieldnames = [
                  "FindingId", "Category", "EstimateType", "Confidence", "Severity", "Region", "Service",
                  "Resource", "MonthlyReviewEstimate", "SnapshotOrRecoveryPointArn", "SourceResourceArn",
                  "SourceDatabaseOrBucket", "AgeDays", "SizeBytes", "AllocatedGiB", "BackupVault", "Status",
                  "DeleteAt", "Evidence", "Recommendation", "ReviewOwner", "ReviewStatus", "ReviewNotes"
              ]
              writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore")
              writer.writeheader()
              for f in findings:
                  category = f.get("category", "")
                  if category not in BACKUP_SNAPSHOT_REVIEW_CATEGORIES:
                      continue
                  evidence = f.get("evidence", "")
                  resource = f.get("resource_id") or f.get("resource_name") or ""
                  snapshot_or_recovery_point_arn = evidence_lookup(
                      evidence,
                      "recovery_point_arn", "sample_recovery_point_arn", "snapshot_arn", "db_snapshot_arn",
                      "arn"
                  )
                  if not snapshot_or_recovery_point_arn and str(resource).startswith("arn:"):
                      snapshot_or_recovery_point_arn = resource
                  source_resource_arn = evidence_lookup(
                      evidence,
                      "resource_arn", "sample_resource_arn", "source_resource_arn", "source_db_snapshot_identifier",
                      "source_backup_vault_arn"
                  )
                  source_name = evidence_lookup(
                      evidence,
                      "db", "db_instance_identifier", "source_database", "source_db", "bucket", "resource_type"
                  )
                  if not source_name:
                      source_name = basename_from_arn_or_name(source_resource_arn)

                  writer.writerow({
                      "FindingId": f.get("finding_id", ""),
                      "Category": category,
                      "EstimateType": f.get("estimate_class", ""),
                      "Confidence": f.get("confidence", ""),
                      "Severity": f.get("severity", ""),
                      "Region": f.get("region", ""),
                      "Service": f.get("service", ""),
                      "Resource": resource,
                      "MonthlyReviewEstimate": f.get("estimated_monthly_savings_usd", ""),
                      "SnapshotOrRecoveryPointArn": snapshot_or_recovery_point_arn,
                      "SourceResourceArn": source_resource_arn,
                      "SourceDatabaseOrBucket": source_name,
                      "AgeDays": evidence_lookup(evidence, "age_days", "oldest_age_days", "newest_age_days"),
                      "SizeBytes": evidence_lookup(evidence, "size_bytes", "total_size_bytes"),
                      "AllocatedGiB": evidence_lookup(evidence, "allocated_gib", "allocated_storage", "allocated_storage_gib", "AllocatedGiB"),
                      "BackupVault": evidence_lookup(evidence, "vault", "backup_vault"),
                      "Status": evidence_lookup(evidence, "status"),
                      "DeleteAt": evidence_lookup(evidence, "delete_at", "DeleteAt"),
                      "Evidence": evidence,
                      "Recommendation": f.get("recommendation", ""),
                      "ReviewOwner": "",
                      "ReviewStatus": "",
                      "ReviewNotes": ""
                  })
              return output.getvalue()

          def html_anchor(value):
              safe = "".join(ch.lower() if ch.isalnum() else "-" for ch in str(value))
              while "--" in safe:
                  safe = safe.replace("--", "-")
              return safe.strip("-") or "section"

          def make_html(summary, findings, warnings, config):
              category_costs = {}
              category_classes = {}
              for f in findings:
                  category = f.get("category", "Uncategorized")
                  category_classes[category] = f.get("estimate_class") or estimate_class_for_category(category)
                  value = f.get("estimated_monthly_savings_usd")
                  if isinstance(value, (int, float)):
                      category_costs[category] = category_costs.get(category, 0.0) + float(value)

              category_items = []
              for category, count in summary["by_category"].items():
                  category_items.append({
                      "category": category,
                      "count": count,
                      "estimate_class": category_classes.get(category) or estimate_class_for_category(category),
                      "monthly": round(category_costs.get(category, 0.0), 2)
                  })
              category_items.sort(key=lambda item: (item["monthly"], item["count"], item["category"]), reverse=True)

              category_rows = "".join(
                  f"<tr><td><a href='#cat-{html_anchor(item['category'])}'>{escape(item['category'])}</a></td>"
                  f"<td><a href='#cat-{html_anchor(item['category'])}'>{item['count']}</a></td>"
                  f"<td>{escape(item['estimate_class'])}</td>"
                  f"<td>${item['monthly']}</td></tr>"
                  for item in category_items
              )

              chart_items = [item for item in category_items if item["monthly"] > 0]
              chart_total = round(sum(item["monthly"] for item in chart_items), 2)
              chart_rows = "".join(
                  f"<div class=\"bar-row\"><div class=\"bar-label\">{escape(item['category'])}</div>"
                  f"<div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:{(item['monthly'] / chart_total * 100) if chart_total else 0:.2f}%\"></div></div>"
                  f"<div class=\"bar-value\">${item['monthly']}</div></div>"
                  for item in chart_items
              )

              expected_checks = [
                  {"name": "Unused Elastic IP Review", "key": "unused_eips", "services": ["EC2"], "categories": ["Unused Elastic IP"]},
                  {"name": "Unattached EBS Volume Review", "key": "unattached_ebs", "services": ["EBS"], "categories": ["Unattached EBS Volume"]},
                  {"name": "Stopped EC2 With Attached EBS Review", "key": "stopped_ec2_with_ebs", "services": ["EC2/EBS"], "categories": ["Stopped EC2 Instance With Attached EBS"]},
                  {"name": "EBS gp2 Volume Conversion Review", "key": "ebs_gp2_conversion", "services": ["EBS"], "categories": ["EBS gp2 Volume Conversion Review"]},
                  {"name": "EC2 Modernization and Rightsizing Review", "key": "ec2_modernization_rightsizing_review", "services": ["EC2", "AWS Price List", "EC2/CloudWatch"], "categories": ["EC2 Previous Generation Instance Review", "EC2 Low Utilization Rightsizing Candidate", "EC2 Bursty Low-Average Instance Review", "EC2 Pricing Estimate Unavailable", "EC2 Modernization and Rightsizing Inventory Summary"]},
                  {"name": "Old AMI Review", "key": "snapshots_and_amis", "services": ["EC2/AMI"], "categories": ["Old AMI Review"]},
                  {"name": "Old EBS Snapshot Review", "key": "snapshots_and_amis", "services": ["EBS Snapshot"], "categories": ["Old Unassociated EBS Snapshot Review"]},
                  {"name": "Idle NAT Gateway Review", "key": "nat_gateway_idle", "services": ["VPC/NAT Gateway", "NAT Gateway/CloudWatch"], "categories": ["Idle NAT Gateway Review"]},
                  {"name": "ELBv2 Load Balancer Review", "key": "elb_review", "services": ["ELBv2"], "categories": ["Load Balancer With No Target Groups", "Load Balancer With No Healthy Targets", "Load Balancer With Empty Target Groups Review", "Empty Target Group"]},
                  {"name": "CloudWatch Log Retention Review", "key": "cloudwatch_log_retention", "services": ["CloudWatch Logs"], "categories": ["CloudWatch Log Group Without Retention"]},
                  {"name": "Large CloudWatch Log Group Review", "key": "large_cloudwatch_logs", "services": ["CloudWatch Logs"], "categories": ["Large CloudWatch Log Group Review"]},
                  {"name": "Lambda No Invocations Review", "key": "lambda_no_invocations", "services": ["Lambda", "Lambda/CloudWatch"], "categories": ["Lambda No Invocations Review"]},
                  {"name": "S3 Lifecycle / Versioning / Large Bucket Review", "key": "s3_lifecycle_review", "services": ["S3", "S3/CloudWatch"], "categories": ["S3 Bucket Without Lifecycle Review", "S3 Versioning Without Lifecycle Review", "Large S3 Bucket Review"]},
                  {"name": "Global Accelerator Review", "key": "global_accelerator_review", "services": ["Global Accelerator"], "categories": ["Global Accelerator Review"]},
                  {"name": "AppStream / WorkSpaces Applications Review", "key": "appstream_review", "services": ["WorkSpaces Applications/AppStream", "AppStream"], "categories": ["WorkSpaces Applications AppStream Fleet Review", "WorkSpaces Applications AppStream Image Builder Review"]},
                  {"name": "AWS Backup Recovery Point Review", "key": "aws_backup_review", "services": ["AWS Backup"], "categories": ["AWS Backup Large Vault Review", "AWS Backup Old Recovery Point Review", "AWS Backup Recovery Point Without Delete Lifecycle Review", "AWS Backup Copied Recovery Point Review", "AWS Backup Copied Recovery Point Summary"]},
                  {"name": "EFS File System Review", "key": "efs_review", "services": ["EFS"], "categories": ["EFS File System Without Lifecycle Review", "EFS Large File System Review", "EFS Standard Storage Optimization Review", "EFS File System Without Mount Targets Review"]},
                  {"name": "ECR Repository Storage / Lifecycle Review", "key": "ecr_review", "services": ["ECR"], "categories": ["ECR Repository Without Lifecycle Policy Review", "ECR Untagged Image Review", "ECR Old Image Review", "ECR Large Repository Review"]},
                  {"name": "DynamoDB Table Cost Review", "key": "dynamodb_review", "services": ["DynamoDB", "DynamoDB/CloudWatch"], "categories": ["DynamoDB Large Table Review", "DynamoDB Low Activity Table Review", "DynamoDB Provisioned Capacity Review"]},
                  {"name": "OpenSearch Service Domain Cost Review", "key": "opensearch_review", "services": ["OpenSearch"], "categories": ["OpenSearch Domain Inventory Summary", "OpenSearch Large Storage Review", "OpenSearch Instance Count Review", "OpenSearch Advanced Capacity Review"]},
                  {"name": "RDS Cost Review", "key": "rds_basic_review", "services": ["RDS", "RDS/CloudWatch", "RDS Snapshot"], "categories": ["RDS DB Instance Inventory Summary", "RDS Large Allocated Storage Review", "RDS Long Backup Retention Review", "RDS Performance Insights / Database Insights Visibility Review", "RDS Stopped DB Instance Storage Review", "RDS Manual Snapshot Inventory Summary", "RDS Low Utilization Review", "Old Manual RDS Snapshot Review", "Clumio Air-Gap RDS Snapshot Retention Review", "RDS Clumio Manual Snapshot Review", "RDS Snapshot From AWS Backup Restore Test + Clumio Tag Inheritance Review"]}
              ]

              def check_enabled_for_report(key):
                  return is_check_enabled(config, key, True)

              coverage_rows_parts = []
              for check in expected_checks:
                  related_findings = [f for f in findings if f.get("category") in check["categories"]]
                  related_warnings = [w for w in warnings if w.get("service") in check["services"]]
                  enabled = check_enabled_for_report(check["key"])
                  if not enabled:
                      status = "DISABLED_BY_CONFIG"
                  elif related_findings:
                      status = "RAN_WITH_FINDINGS"
                  elif related_warnings:
                      status = "WARNING_OR_ACCESS_DENIED"
                  else:
                      status = "NO_FINDINGS_OBSERVED"
                  category_links = ", ".join(
                      f"<a href=\"#cat-{html_anchor(cat)}\">{escape(cat)}</a>" if cat in summary["by_category"] else escape(cat)
                      for cat in check["categories"]
                  )
                  coverage_rows_parts.append(
                      "<tr>"
                      f"<td>{escape(check['name'])}</td>"
                      f"<td>{escape(status)}</td>"
                      f"<td>{'true' if enabled else 'false'}</td>"
                      f"<td>{len(related_findings)}</td>"
                      f"<td>{len(related_warnings)}</td>"
                      f"<td>{category_links}</td>"
                      "</tr>"
                  )
              coverage_rows = "".join(coverage_rows_parts)

              enabled_checks = config.get("enabled_checks", {}) or {}
              thresholds = config.get("thresholds", {}) or {}
              config_summary_rows = "".join([
                  f"<tr><td>Regions</td><td>{escape(str(config.get('regions', 'all-enabled')))}</td></tr>",
                  f"<tr><td>Excluded Regions</td><td><code>{escape(json.dumps(config.get('excluded_regions', []), default=json_safe))}</code></td></tr>",
                  f"<tr><td>Service/Region Exclusions</td><td><code>{escape(json.dumps(config.get('service_region_exclusions', {}), default=json_safe))}</code></td></tr>",
                  f"<tr><td>Enabled Checks Overrides</td><td><code>{escape(json.dumps(enabled_checks, default=json_safe))}</code></td></tr>",
                  f"<tr><td>Thresholds</td><td><code>{escape(json.dumps(thresholds, default=json_safe))}</code></td></tr>",
                  f"<tr><td>Mode</td><td>{escape(str(config.get('mode', 'ReportOnly')))}</td></tr>"
              ])

              timing_rows = "".join(
                  "<tr>"
                  f"<td>{escape(t.get('check', ''))}</td>"
                  f"<td>{escape(t.get('status', ''))}</td>"
                  f"<td>{escape(str(t.get('duration_seconds', '')))}</td>"
                  f"<td>{escape(t.get('error', ''))}</td>"
                  "</tr>"
                  for t in summary.get("timings", [])
              )

              grouped = {}
              for f in findings:
                  grouped.setdefault(f.get("category", "Uncategorized"), []).append(f)

              def format_evidence_html(finding):
                  evidence = str(finding.get("evidence", ""))
                  category = finding.get("category", "")
                  service = finding.get("service", "")
                  if service != "CloudWatch Logs" and "Log Group" not in category:
                      return escape(evidence)

                  thresholds = config.get("thresholds", {})
                  warn_gib = float(thresholds.get("log_group_warning_gib", 5))
                  critical_gib = float(thresholds.get("log_group_critical_gib", 50))

                  def repl(match):
                      value = float(match.group(1))
                      css_class = "size-normal"
                      if value >= critical_gib:
                          css_class = "size-critical"
                      elif value >= warn_gib:
                          css_class = "size-warning"
                      return f'<span class="{css_class}">{escape(match.group(0))}</span>'

                  escaped = escape(evidence)
                  return re.sub(r"([0-9]+(?:[.][0-9]+)?) GiB", repl, escaped)

              def render_cli(cli):
                  if not cli:
                      return ""
                  parts = []
                  for label, commands in [("Validation", cli.get("validation", [])), ("Possible remediation", cli.get("possible_remediation", [])), ("Notes", cli.get("notes", []))]:
                      if commands:
                          if label == "Notes":
                              body = "<br>".join(escape(str(item)) for item in commands)
                              parts.append(f'<div class="cli-label">{label}</div><div class="cli-notes">{body}</div>')
                          else:
                              body = chr(10).join(commands)
                              parts.append(f'<div class="cli-label">{label}</div><pre class="cli-block">{escape(body)}</pre>')
                  return '<details class="cli-details"><summary>Suggested CLI</summary>' + "".join(parts) + "</details>"

              finding_rows_parts = []
              for category in sorted(grouped.keys()):
                  anchor = html_anchor(category)
                  items = grouped[category]
                  finding_rows_parts.append(
                      f"<tr id=\"cat-{anchor}\" class=\"category-section\">"
                      f"<td colspan=\"11\">{escape(category.upper())} ? {len(items)} FINDING(S) "
                      f"<a class=\"top-link\" href=\"#findings-summary\">Back to summary</a></td>"
                      f"</tr>"
                  )
                  for f in items:
                      finding_rows_parts.append(
                          "<tr>"
                          f"<td>{escape(f.get('finding_id',''))}</td>"
                          f"<td>{escape(f.get('category',''))}</td>"
                          f"<td>{escape(f.get('estimate_class') or estimate_class_for_category(f.get('category','')))}</td>"
                          f"<td>{escape(f.get('confidence',''))}</td>"
                          f"<td>{escape(f.get('severity',''))}</td>"
                          f"<td>{escape(f.get('region',''))}</td>"
                          f"<td>{escape(f.get('service',''))}</td>"
                          f"<td><code>{escape(str(f.get('resource_id','')))}</code></td>"
                          f"<td>{escape(str(f.get('estimated_monthly_savings_usd','')))}</td>"
                          f"<td>{format_evidence_html(f)}</td>"
                          f"<td>{escape(f.get('recommendation',''))}{render_cli(f.get('cli_suggestions'))}</td>"
                          f"</tr>"
                      )
              finding_rows = "".join(finding_rows_parts)

              expected_checks = [
                  {"name": "Unused Elastic IP Review", "services": ["EC2"]},
                  {"name": "Unattached EBS Volume Review", "services": ["EBS"]},
                  {"name": "Stopped EC2 With Attached EBS Review", "services": ["EC2/EBS"]},
                  {"name": "EBS gp2 Volume Conversion Review", "services": ["EBS"]},
                  {"name": "EC2 Modernization and Rightsizing Review", "services": ["EC2", "AWS Price List", "EC2/CloudWatch"]},
                  {"name": "Low Utilization EC2 Instance Review", "services": ["EC2", "EC2/CloudWatch"]},
                  {"name": "Low Activity EBS Volume Review", "services": ["EBS", "EBS/CloudWatch"]},
                  {"name": "NAT Gateway Idle Review", "services": ["VPC/NAT Gateway", "NAT Gateway/CloudWatch"]},
                  {"name": "Old AMI Review", "services": ["EC2/AMI"]},
                  {"name": "Old EBS Snapshot Review", "services": ["EBS Snapshot"]},
                  {"name": "Lambda No-Invocations Review", "services": ["Lambda", "Lambda/CloudWatch"]},
                  {"name": "ELBv2 Load Balancer / Target Group Review", "services": ["ELBv2"]},
                  {"name": "CloudWatch Log Retention Review", "services": ["CloudWatch Logs"]},
                  {"name": "Large CloudWatch Log Group Review", "services": ["CloudWatch Logs"]},
                  {"name": "RDS Basic / Performance / Snapshot Review", "services": ["RDS", "RDS/CloudWatch", "RDS Snapshot"]},
                  {"name": "S3 Lifecycle / Versioning / Large Bucket Review", "services": ["S3", "S3/CloudWatch"]},
                  {"name": "Global Accelerator Review", "services": ["Global Accelerator"]},
                  {"name": "AppStream / WorkSpaces Applications Review", "services": ["AppStream", "WorkSpaces Applications", "WorkSpaces Applications/AppStream"]},
                  {"name": "AWS Backup Recovery Point Review", "services": ["AWS Backup"]},
                  {"name": "EFS File System Review", "services": ["EFS"]},
                  {"name": "ECR Repository Storage / Lifecycle Review", "services": ["ECR"]},
                  {"name": "DynamoDB Table Cost Review", "services": ["DynamoDB", "DynamoDB/CloudWatch"]},
                  {"name": "OpenSearch Service Domain Cost Review", "services": ["OpenSearch"]}
              ]

              def check_warning_count(services):
                  return sum(1 for w in warnings if w.get("service") in services)

              def check_finding_count(name):
                  name_lower = name.lower()
                  if "appstream" in name_lower:
                      return sum(1 for f in findings if "appstream" in f.get("category", "").lower() or "workspaces applications" in f.get("category", "").lower() or "appstream" in f.get("service", "").lower())
                  if "aws backup" in name_lower:
                      return sum(1 for f in findings if f.get("service") == "AWS Backup")
                  if "efs" in name_lower:
                      return sum(1 for f in findings if f.get("service") == "EFS")
                  if "ecr" in name_lower:
                      return sum(1 for f in findings if f.get("service") == "ECR")
                  if "dynamodb" in name_lower:
                      return sum(1 for f in findings if f.get("service") == "DynamoDB")
                  if "opensearch" in name_lower:
                      return sum(1 for f in findings if f.get("service") == "OpenSearch")
                  if "global accelerator" in name_lower:
                      return sum(1 for f in findings if "global accelerator" in f.get("category", "").lower() or "global accelerator" in f.get("service", "").lower())
                  if "rds" in name_lower:
                      return sum(1 for f in findings if f.get("service", "").startswith("RDS") or "RDS" in f.get("category", ""))
                  if "s3" in name_lower:
                      return sum(1 for f in findings if f.get("service", "").startswith("S3") or "s3" in f.get("category", "").lower())
                  if "large cloudwatch" in name_lower:
                      return sum(1 for f in findings if f.get("category") == "Large CloudWatch Log Group Review")
                  if "cloudwatch log retention" in name_lower:
                      return sum(1 for f in findings if f.get("category") == "CloudWatch Log Group Without Retention")
                  if "elb" in name_lower:
                      return sum(1 for f in findings if f.get("service") == "ELBv2")
                  if "lambda" in name_lower:
                      return sum(1 for f in findings if f.get("service") == "Lambda")
                  if "snapshot" in name_lower:
                      return sum(1 for f in findings if "Snapshot" in f.get("category", ""))
                  if "ami" in name_lower:
                      return sum(1 for f in findings if "AMI" in f.get("category", ""))
                  if "nat gateway" in name_lower:
                      return sum(1 for f in findings if "NAT Gateway" in f.get("category", ""))
                  if "gp2" in name_lower:
                      return sum(1 for f in findings if f.get("category") == "EBS gp2 Volume Conversion Review")
                  if "ec2 modernization" in name_lower or "rightsizing" in name_lower:
                      return sum(1 for f in findings if f.get("category") in ["EC2 Previous Generation Instance Review", "EC2 Low Utilization Rightsizing Candidate", "EC2 Bursty Low-Average Instance Review", "EC2 Pricing Estimate Unavailable", "EC2 Modernization and Rightsizing Inventory Summary"])
                  if "low utilization ec2" in name_lower:
                      return sum(1 for f in findings if f.get("category") == "Low Utilization EC2 Instance Review")
                  if "low activity ebs" in name_lower:
                      return sum(1 for f in findings if f.get("category") == "Low Activity EBS Volume Review")
                  if "stopped ec2" in name_lower:
                      return sum(1 for f in findings if f.get("category") == "Stopped EC2 Instance With Attached EBS")
                  if "unattached ebs" in name_lower:
                      return sum(1 for f in findings if f.get("category") == "Unattached EBS Volume")
                  if "elastic ip" in name_lower:
                      return sum(1 for f in findings if f.get("category") == "Unused Elastic IP")
                  return 0

              checks_rows = "".join(
                  f"<tr><td>{escape(check['name'])}</td>"
                  f"<td>{'Completed with warning' if check_warning_count(check['services']) else 'Completed'}</td>"
                  f"<td>{check_finding_count(check['name'])}</td>"
                  f"<td>{check_warning_count(check['services'])}</td></tr>"
                  for check in expected_checks
              )

              def classify_warning(warning):
                  error_text = str(warning.get("error", "")).lower()
                  if "explicit deny" in error_text or "service control policy" in error_text:
                      return "SCP ExplicitDeny"
                  if "accessdenied" in error_text or "not authorized" in error_text or "unauthorized" in error_text:
                      return "IAM AccessDenied"
                  if "could not connect" in error_text or "connect timeout" in error_text or "read timeout" in error_text:
                      return "Endpoint/Timeout"
                  if "not found" in error_text or "unknownendpoint" in error_text:
                      return "Regional API/Endpoint"
                  return "Other"

              warning_summary = {}
              for w in warnings:
                  key = (
                      w.get("region", "unknown"),
                      w.get("service", "unknown"),
                      w.get("operation", "unknown"),
                      classify_warning(w)
                  )
                  warning_summary[key] = warning_summary.get(key, 0) + 1

              warning_summary_rows = "".join(
                  f"<tr><td>{escape(region)}</td><td>{escape(service)}</td><td>{escape(operation)}</td><td>{escape(issue_type)}</td><td>{count}</td></tr>"
                  for (region, service, operation, issue_type), count in sorted(warning_summary.items(), key=lambda item: (item[0][0], item[0][1], item[0][2], item[0][3]))
              )

              warning_rows = "".join(
                  "<tr>"
                  f"<td>{escape(w.get('region',''))}</td>"
                  f"<td>{escape(w.get('service',''))}</td>"
                  f"<td>{escape(w.get('operation',''))}</td>"
                  f"<td>{escape(w.get('resource',''))}</td>"
                  f"<td>{escape(w.get('error',''))}</td>"
                  f"</tr>"
                  for w in warnings
              )
              dependency_notes = {
                  "Unused Elastic IP": "Check DNS records, vendor allowlists, firewall rules, documentation, and prior resource attachments before releasing the address.",
                  "Unattached EBS Volume": "Confirm snapshots, backup policy, application owner approval, and whether the volume was intentionally detached before deletion.",
                  "Stopped EC2 Instance With Attached EBS": "Confirm whether the stopped instance is intentionally retained. Termination behavior can delete root volumes depending on DeleteOnTermination, while data volumes may remain billable.",
                  "Empty Target Group": "Check ALB/NLB listener rules and deployment pipelines before deleting. A target group can be referenced even when it has no registered targets.",
                  "Load Balancer With No Target Groups": "Check Route 53 records, certificates, listeners, security groups, WAF associations, and application ownership before deleting the load balancer.",
                  "Load Balancer With No Healthy Targets": "Treat this as either stale infrastructure or a possible outage. Check target health, listener rules, DNS, certificates, security groups, and application ownership before cleanup.",
                  "Load Balancer With Empty Target Groups Review": "The load balancer is the billable resource. Empty target groups are supporting evidence, but confirm DNS, listeners, certificates, security groups, WAF, and owners before deletion.",
                  "Idle NAT Gateway Review": "Check private subnet route tables, patching paths, package repositories, license servers, vendor calls, and VPC endpoint alternatives before deleting a NAT Gateway.",
                  "Old AMI Review": "Deregistering an AMI does not necessarily remove all backing snapshots. Confirm rollback, golden-image, compliance, and DR requirements before deregistration and snapshot cleanup.",
                  "Old Unassociated EBS Snapshot Review": "Confirm that the snapshot is not needed for backup, compliance, manual rollback, legal hold, or disaster recovery before deletion.",
                  "CloudWatch Log Group Without Retention": "Retention policies permanently expire older events after the retention window. Confirm security, audit, and troubleshooting requirements before applying retention.",
                  "Large CloudWatch Log Group Review": "Review retention, ingestion volume, debug verbosity, subscription filters, export strategy, and audit requirements before changing or deleting logs.",
                  "S3 Bucket Without Lifecycle Review": "Lifecycle rules can expire objects, noncurrent versions, and incomplete multipart uploads. Confirm application behavior, compliance, object-lock, and retention expectations before enabling rules.",
                  "S3 Versioning Without Lifecycle Review": "Noncurrent version expiration can permanently remove older object versions. Confirm rollback, legal hold, compliance, and application recovery requirements before applying lifecycle rules.",
                  "EBS gp2 Volume Conversion Review": "gp3 is often cheaper and more flexible, but confirm IOPS, throughput, latency sensitivity, operating-system support, and change-management requirements before modifying the volume.",
                  "EC2 Previous Generation Instance Review": "Previous-generation EC2 instances may have modernization opportunities, but review AMI compatibility, ENA/NVMe drivers, licensing, memory, EBS/network bandwidth, Savings Plans/RIs, and application behavior before changing instance families.",
                  "EC2 Low Utilization Rightsizing Candidate": "CPU-based rightsizing is only a candidate signal. Validate memory, network, EBS throughput, batch schedules, licensing, and business context before choosing a smaller instance type.",
                  "EC2 Bursty Low-Average Instance Review": "Average CPU can be low while peak CPU is high. Treat this as a caution against downsizing from CPU average alone; validate peak usage, memory, network, EBS throughput, login behavior, batch jobs, and business schedule.",
                  "EC2 Pricing Estimate Unavailable": "The report could not obtain a reliable AWS Price List API estimate for one or more EC2 candidates. Review manually before making cost assumptions.",
                  "EC2 Modernization and Rightsizing Inventory Summary": "Informational summary confirming the EC2 modernization/rightsizing scanner ran in a Region but did not emit actionable candidates under the current thresholds.",
                  "Lambda No Invocations Review": "Inactive functions can still be custom resources, break-glass tools, infrequent jobs, or deployment hooks. Check triggers, aliases, EventBridge rules, and pipelines before removal.",
                  "Global Accelerator Review": "Check listeners, endpoint groups, DNS/client references, routing requirements, and global failover needs before disabling or deleting an accelerator.",
                  "WorkSpaces Applications/AppStream Review": "Check active users, fleet/image-builder state, schedules, assignments, and business-hour requirements before stopping or deleting AppStream-related resources.",
                  "AWS Backup Old Recovery Point Review": "Confirm retention policy, legal hold, compliance, audit, ransomware recovery, and restore requirements before deleting recovery points.",
                  "AWS Backup Recovery Point Without Delete Lifecycle Review": "Recovery points without delete lifecycle may be retained indefinitely. Confirm backup plan/rule intent before lifecycle changes or deletion.",
                  "AWS Backup Copied Recovery Point Review": "Cross-Region or cross-account backup copies can duplicate cost but may be required for DR or compliance. If BackupSizeInBytes/size_bytes is 0, the report cannot confidently estimate storage cost from that API record; validate copied backup cost using AWS Backup vault metrics, Cost Explorer, CUR, or billing data before assuming there is no cost.",
                  "AWS Backup Copied Recovery Point Summary": "Summary row for copied AWS Backup recovery points. Use this to review copy policy, lifecycle, and DR/compliance intent without expanding thousands of normal copied recovery point rows. A $0.0 estimate with total_size_bytes=0 means cost estimate unavailable from the recovery point API, not necessarily no cost.",
                  "AWS Backup Large Vault Review": "Large backup vault recovery-point counts may be normal for heavily protected environments. Review backup plans, lifecycle rules, and restore requirements before cleanup.",
                  "ECR Repository Without Lifecycle Policy Review": "ECR lifecycle policies can delete images used for rollback or deployments by digest. Confirm deployment, rollback, and release-retention requirements before cleanup.",
                  "ECR Untagged Image Review": "Untagged images are common cleanup candidates, but they can still be referenced by digest. Confirm deployment tooling and rollback needs before lifecycle deletion.",
                  "ECR Old Image Review": "Old container images may support rollback, audit, incident recovery, or release traceability. Confirm retention expectations before expiring images.",
                  "ECR Large Repository Review": "Large ECR repositories may indicate missing lifecycle policies, excessive build churn, or oversized layers. Confirm owner and retention policy before cleanup.",
                  "DynamoDB Large Table Review": "Large DynamoDB tables may require TTL, archival, export, or item retention review. Confirm application ownership before deleting or archiving data.",
                  "DynamoDB Low Activity Table Review": "Low activity does not prove unused. Confirm infrequent jobs, audit lookups, DR usage, and application dependencies before cleanup.",
                  "DynamoDB Provisioned Capacity Review": "Provisioned capacity may be appropriate for predictable workloads. Review consumed capacity, auto scaling, and on-demand tradeoffs before changing billing mode or capacity.",
                  "OpenSearch Domain Inventory Summary": "Inventory-only row for visibility. Use it to confirm OpenSearch domains are being seen by the report.",
                  "OpenSearch Large Storage Review": "OpenSearch storage can grow from old indexes, shard strategy, logs, or retention gaps. Confirm index retention and snapshot requirements before cleanup.",
                  "OpenSearch Instance Count Review": "OpenSearch instance count reductions can impact performance and availability. Validate CPU, JVM, storage, shard distribution, and workload before resizing.",
                  "OpenSearch Advanced Capacity Review": "Zone awareness, standby, dedicated masters, and warm nodes may be necessary for production. Review environment criticality before changing capacity settings.",
                  "Clumio Air-Gap RDS Snapshot Retention Review": "This appears to be Clumio air-gap related and older than the Clumio air-gap review threshold. Continuous backup/PITR only confirms recent restore coverage; it does not prove long-term air-gap snapshots are safe to delete. Review Clumio retention, legal hold, ransomware recovery, compliance, archive lock, and backup catalog ownership before deleting. Prefer Clumio policy cleanup or documented backup-owner approval.",
                  "RDS Clumio Manual Snapshot Review": "This appears to be a Clumio-created RDS manual snapshot. Confirm Clumio ownership, retention policy, backup catalog state, and cleanup method before deleting directly from AWS.",
                  "RDS Snapshot From AWS Backup Restore Test + Clumio Tag Inheritance Review": "Likely AWS Backup Restore Testing + Clumio tag inheritance backup loop. Confirm whether awsbackup-restore-test-* RDS instances inherit Clumio protection tags and should be excluded from Clumio backup policies."
              }
              present_categories = set(summary.get("by_category", {}).keys())
              dependency_rows = "".join(
                  f"<tr><td>{escape(category)}</td><td>{escape(note)}</td></tr>"
                  for category, note in dependency_notes.items()
                  if category in present_categories
              )
              config_json = escape(json.dumps(config, indent=2, default=json_safe))

              return f"""<!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>AWS Cost Optimization Report</title>
            <style>
              :root {{
                --primary-bg: #0a0a0a;
                --secondary-bg: #1a1a1a;
                --tertiary-bg: #2a2a2a;
                --accent-color: #00d4aa;
                --accent-secondary: #00b4ff;
                --text-primary: #ffffff;
                --text-secondary: #b0b0b0;
                --text-muted: #6a6a6a;
                --border-color: #333333;
                --error-color: #ff4757;
                --warning-color: #ffa502;
                --success-color: #2ed573;
              }}
              * {{ box-sizing: border-box; }}
              html {{ scroll-behavior: smooth; }}
              body {{
                margin: 0;
                background: var(--primary-bg);
                color: var(--text-primary);
                font-family: Inter, Segoe UI, Arial, sans-serif;
              }}
              header {{
                padding: 32px;
                background: linear-gradient(135deg, #111 0%, #1b2630 100%);
                border-bottom: 1px solid var(--border-color);
              }}
              .logo {{ font-size: 32px; font-weight: 700; }}
              .subtitle {{ color: var(--text-secondary); margin-top: 8px; }}
              .container {{ padding: 24px; }}
              .grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }}
              .card {{
                background: var(--secondary-bg);
                border: 1px solid var(--border-color);
                border-radius: 12px;
                padding: 18px;
                box-shadow: 0 8px 32px rgba(0,0,0,0.3);
              }}
              .metric {{ font-size: 28px; font-weight: 700; color: var(--accent-color); }}
              .label {{ color: var(--text-secondary); font-size: 13px; margin-top: 4px; }}
              h2 {{ margin-top: 32px; color: var(--accent-secondary); }}
              table {{ width: 100%; border-collapse: collapse; margin-top: 12px; background: var(--secondary-bg); }}
              th, td {{ border: 1px solid var(--border-color); padding: 10px; vertical-align: top; font-size: 13px; }}
              th {{ background: var(--tertiary-bg); text-align: left; color: var(--accent-color); }}
              code, pre {{ font-family: 'SF Mono', Consolas, Monaco, monospace; }}
              pre {{ background: #050505; border: 1px solid var(--border-color); padding: 16px; border-radius: 8px; overflow: auto; }}
              .notice {{ border-left: 4px solid var(--warning-color); padding: 12px 16px; background: rgba(255,165,2,0.08); color: var(--text-secondary); }}
              a {{ color: var(--accent-color); text-decoration: none; }}
              a:hover {{ text-decoration: underline; }}
              .category-section {{ scroll-margin-top: 18px; }}
              .category-section td {{
                background: rgba(0, 212, 170, 0.20);
                color: var(--accent-color);
                font-weight: 700;
                letter-spacing: 0.08em;
                border-top: 2px solid var(--accent-color);
              }}
              .top-link {{ float: right; font-size: 11px; letter-spacing: normal; font-weight: 400; }}
              .findings-table {{ table-layout: fixed; width: 100%; }}
              .findings-table th, .findings-table td {{ font-size: 12px; }}
              .findings-table th:nth-child(1), .findings-table td:nth-child(1) {{ width: 76px; white-space: nowrap; }}
              .findings-table th:nth-child(2), .findings-table td:nth-child(2) {{ width: 150px; }}
              .findings-table th:nth-child(3), .findings-table td:nth-child(3) {{ width: 118px; }}
              .findings-table th:nth-child(4), .findings-table td:nth-child(4) {{ width: 80px; white-space: nowrap; }}
              .findings-table th:nth-child(5), .findings-table td:nth-child(5) {{ width: 70px; white-space: nowrap; }}
              .findings-table th:nth-child(6), .findings-table td:nth-child(6) {{ width: 118px; white-space: nowrap; }}
              .findings-table th:nth-child(7), .findings-table td:nth-child(7) {{ width: 105px; white-space: nowrap; }}
              .findings-table th:nth-child(8), .findings-table td:nth-child(8) {{ width: 230px; }}
              .findings-table th:nth-child(9), .findings-table td:nth-child(9) {{ width: 112px; white-space: nowrap; text-align: right; }}
              .findings-table th:nth-child(10), .findings-table td:nth-child(10) {{ width: 30%; white-space: pre-line; }}
              .findings-table th:nth-child(11), .findings-table td:nth-child(11) {{ width: 26%; white-space: pre-line; }}
              .findings-table td {{ overflow-wrap: anywhere; word-break: normal; }}
              .findings-table code {{ white-space: normal; overflow-wrap: anywhere; word-break: break-word; }}
              .impact-chart {{ margin-top: 16px; background: var(--secondary-bg); border: 1px solid var(--border-color); border-radius: 12px; padding: 18px; }}
              .bar-row {{ display: grid; grid-template-columns: minmax(220px, 340px) 1fr 100px; gap: 12px; align-items: center; margin: 10px 0; }}
              .bar-label {{ color: var(--text-primary); font-size: 13px; }}
              .bar-track {{ background: #050505; border: 1px solid var(--border-color); height: 18px; border-radius: 999px; overflow: hidden; }}
              .bar-fill {{ height: 100%; background: linear-gradient(90deg, var(--accent-color), var(--accent-secondary)); }}
              .bar-value {{ text-align: right; color: var(--accent-color); font-family: 'SF Mono', Consolas, Monaco, monospace; font-size: 13px; }}
              .chart-note {{ color: var(--text-secondary); font-size: 13px; margin-top: 8px; }}
              .cli-details {{ margin-top: 10px; border-top: 1px solid var(--border-color); padding-top: 8px; }}
              .cli-details summary {{ color: var(--accent-color); cursor: pointer; font-weight: 600; }}
              .inline-details {{ margin-top: 8px; border-top: 1px solid var(--border-color); padding-top: 8px; }}
              .inline-details summary {{ color: var(--accent-color); cursor: pointer; font-weight: 700; }}
              .inline-details-block {{ margin: 8px 0 0 0; padding: 10px; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; }}
              .cli-label {{ color: var(--accent-secondary); font-size: 12px; margin-top: 8px; font-weight: 700; }}
              .cli-block {{ margin: 6px 0; padding: 10px; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; }}
              .cli-notes {{ color: var(--text-secondary); font-size: 12px; margin-top: 4px; }}
              .size-warning {{ color: var(--warning-color); font-weight: 800; }}
              .size-critical {{ color: var(--error-color); font-weight: 900; text-shadow: 0 0 8px rgba(255,71,87,0.35); }}
              .size-normal {{ color: var(--text-primary); }}
              .status-success {{ color: var(--success-color); font-weight: 400 !important; text-shadow: none !important; }}
              .status-error {{ color: var(--error-color); font-weight: 400 !important; text-shadow: none !important; }}
              .status-warning {{ color: var(--warning-color); font-weight: 400 !important; text-shadow: none !important; }}
              .collapsed-finding-row {{ display: none; }}
              .show-more-row td {{ background: rgba(0,180,255,0.08); text-align: center; }}
              .show-more-btn {{ background: var(--tertiary-bg); border: 1px solid var(--accent-color); color: var(--accent-color); border-radius: 8px; padding: 8px 12px; cursor: pointer; font-weight: 700; }}
              .show-more-btn:hover {{ background: rgba(0,212,170,0.12); }}
              .footer {{ color: var(--text-muted); padding: 32px; text-align: center; }}
            </style>
          </head>
          <body>
            <header>
              <div class="logo">AWS Cost Optimization Report</div>
              <div class="subtitle">Report-only cost review findings for account {escape(summary['account_id'])}</div>
            </header>
            <main class="container">
              <div class="notice">
                This report is non-destructive. It does not delete, resize, stop, release, or modify AWS resources. Direct savings estimates are separated from gross review/exposure estimates. Gross figures may overlap across backups, snapshots, and third-party protection layers and are not guaranteed savings.
              </div>

              <div class="grid" style="margin-top: 20px;">
                <div class="card"><div class="metric">{summary['total_findings']}</div><div class="label">Total Findings</div></div>
                <div class="card"><div class="metric">${summary['direct_monthly_savings_usd_known_items']}</div><div class="label">Direct Monthly Savings Estimate</div></div>
                <div class="card"><div class="metric">${summary['gross_monthly_review_exposure_usd_known_items']}</div><div class="label">Gross Monthly Review Potential</div></div>
                <div class="card"><div class="metric">{summary['warnings_count']}</div><div class="label">Collection Warnings</div></div>
              </div>

              <h2>Executive Summary</h2>
              <table>
                <tr><th>Field</th><th>Value</th></tr>
                <tr><td>Project</td><td>{escape(summary['project'])}</td></tr>
                <tr><td>Template Version</td><td>{escape(summary.get('template_version', 'unknown'))}</td></tr>
                <tr><td>Toolkit Version</td><td>{escape(summary.get('toolkit_version', 'unknown'))}</td></tr>
                <tr><td>Template Build</td><td>{escape(summary.get('template_build', 'unknown'))}</td></tr>
                <tr><td>Account</td><td>{escape(summary['account_id'])}</td></tr>
                <tr><td>Account Alias</td><td>{escape(summary.get('account_alias') or 'unknown')}</td></tr>
                <tr><td>Environment</td><td>{escape(summary['environment'])}</td></tr>
                <tr><td>Generated</td><td>{escape(summary['generated_at'])}</td></tr>
                <tr><td>Mode</td><td>{escape(summary['mode'])}</td></tr>
                <tr><td>Direct Annual Savings Estimate</td><td>${summary['direct_annual_savings_usd_known_items']}</td></tr>
                <tr><td>Gross Annual Review Potential</td><td>${summary['gross_annual_review_exposure_usd_known_items']}</td></tr>
                <tr><td>Estimate Semantics</td><td>Direct savings includes categories that are more likely to become actionable cost reductions after owner review. Gross review potential includes broader cost exposure and can overlap across findings and categories, especially snapshots, backups, restore-test copies, and third-party backup layers. Do not treat gross review potential as guaranteed or additive realized savings.</td></tr>
                <tr><td>Regions Scanned</td><td>{escape(', '.join(summary['regions_scanned']))}</td></tr>
              </table>

              <h2>Config Summary</h2>
              <table>
                <tr><th>Setting</th><th>Value</th></tr>
                {config_summary_rows}
              </table>

              <h2 id="findings-summary">Findings by Category</h2>
              <table><tr><th>Category</th><th>Count</th><th>Estimate Type</th><th>Gross Monthly Review Impact</th></tr>{category_rows}</table>

              <h2>Gross Monthly Review Potential by Category</h2>
              <div class="impact-chart">
                {chart_rows if chart_rows else '<div class="chart-note">No numeric monthly review estimates are available in this report.</div>'}
                <div class="chart-note">This visualization only uses numeric estimates generated by this report. These are gross review/exposure values, not guaranteed savings. Direct savings categories are separated in the headline cards. Categories can overlap, especially snapshot, backup, restore-test, and third-party backup findings. Categories with no estimate are excluded from the chart and should still be reviewed manually.</div>
              </div>

              <h2>Backup Retention Interpretation</h2>
              <div class="notice">
                Backup, RDS PITR, restore-test, manual snapshot, AWS Backup copyjob, and third-party air-gapped backup findings are intentionally classified as gross review exposure. Production systems can require multiple protection layers. Continuous backup confirms recent PITR coverage only; it does not prove older manual, copied, or Clumio air-gap snapshots are safe to delete. Use these findings to ask whether each backup layer has a defined restore purpose, retention period, owner, and lifecycle policy. Do not delete older recovery points only because they appear in this report; validate Clumio/AWS Backup ownership, restore, compliance, legal hold, ransomware recovery, and business requirements first.
              </div>

              <h2>Check Coverage Summary</h2>
              <div class="notice">
                This section helps detect regressions and permission gaps. A missing category in Findings by Category does not always mean the check disappeared; it may mean the check found nothing, was disabled, or encountered a warning.
              </div>
              <table>
                <tr><th>Check</th><th>Status</th><th>Enabled</th><th>Findings</th><th>Warnings</th><th>Related Categories</th></tr>
                {coverage_rows}
              </table>

              <h2>Checks Executed</h2>
              <table>
                <tr><th>Check</th><th>Status</th><th>Findings</th><th>Warnings</th></tr>
                {checks_rows}
              </table>
              <div class="chart-note">A check can show Completed with 0 findings when the account has no matching resources or no issues for that category. Warnings usually indicate permission, SCP, or regional API limitations.</div>

              <h2>Detailed Findings</h2>
              <table class="findings-table">
                <tr>
                  <th>ID</th><th>Category</th><th>Estimate Type</th><th>Confidence</th><th>Severity</th><th>Region</th><th>Service</th>
                  <th>Resource</th><th>Monthly Review Estimate</th><th>Evidence</th><th>Recommendation</th>
                </tr>
                {finding_rows if finding_rows else '<tr><td colspan="11">No findings discovered.</td></tr>'}
              </table>

              <h2>Warnings Summary</h2>
              <table>
                <tr><th>Region</th><th>Service</th><th>Operation</th><th>Issue Type</th><th>Count</th></tr>
                {warning_summary_rows if warning_summary_rows else '<tr><td colspan="5">No collection warnings.</td></tr>'}
              </table>
              <div class="chart-note">Use this summary to quickly identify SCP, IAM, endpoint, or regional API issues. The full warning details are listed below.</div>

              <h2>Collection Warnings</h2>
              <table>
                <tr><th>Region</th><th>Service</th><th>Operation</th><th>Resource</th><th>Error</th></tr>
                {warning_rows if warning_rows else '<tr><td colspan="5">No collection warnings.</td></tr>'}
              </table>

              <h2>Remediation Dependency Guidance</h2>
              <div class="notice">
                These notes are generated only for finding categories present in this report. They are intended to help avoid DependencyViolation errors, outages, or accidental data loss during cleanup planning.
              </div>
              <table>
                <tr><th>Finding Category</th><th>Dependency / Cleanup Notes</th></tr>
                {dependency_rows if dependency_rows else '<tr><td colspan="2">No dependency guidance is available for the categories in this report.</td></tr>'}
              </table>

              <h2>Runtime Diagnostics</h2>
              <details class="cli-details">
                <summary>Show runtime timing summary</summary>
                <table>
                  <tr><th>Check</th><th>Status</th><th>Duration Seconds</th><th>Error</th></tr>
                  {timing_rows if timing_rows else '<tr><td colspan="4">No timing data captured for this run.</td></tr>'}
                </table>
              </details>

              <h2>Effective Config</h2>
              <pre>{config_json}</pre>
            </main>
            <div class="footer">Generated by AWS Cost Reduction Toolkit - V1</div>
            <script>
              (function () {{
                function colorizeStatusCells() {{
                  const cells = document.querySelectorAll('td');
                  cells.forEach(function (cell) {{
                    const text = (cell.textContent || '').trim().toUpperCase();
                    if (!text) return;
                    if (text === 'SUCCESS' || text === 'COMPLETED' || text === 'RAN_WITH_FINDINGS' || text === 'NO_FINDINGS_OBSERVED') {{
                      cell.classList.add('status-success');
                    }} else if (text.includes('ERROR') || text.includes('FAILED') || text.includes('ACCESS_DENIED') || text.includes('WARNING_OR_ACCESS_DENIED')) {{
                      cell.classList.add('status-error');
                    }} else if (text.includes('WARNING') || text.includes('DISABLED_BY_CONFIG') || text === 'COMPLETED WITH WARNING') {{
                      cell.classList.add('status-warning');
                    }}
                  }});
                }}

                function makeShowMoreRow(colSpan, hiddenRows, label) {{
                  const showRow = document.createElement('tr');
                  showRow.className = 'show-more-row';
                  const td = document.createElement('td');
                  td.colSpan = colSpan;
                  const button = document.createElement('button');
                  button.type = 'button';
                  button.className = 'show-more-btn';
                  button.textContent = 'Show ' + hiddenRows.length + ' additional ' + label;
                  button.addEventListener('click', function () {{
                    const isCollapsed = hiddenRows[0] && hiddenRows[0].classList.contains('collapsed-finding-row');
                    hiddenRows.forEach(function (row) {{
                      row.classList.toggle('collapsed-finding-row', !isCollapsed);
                    }});
                    button.textContent = isCollapsed ? 'Hide additional ' + label : 'Show ' + hiddenRows.length + ' additional ' + label;
                  }});
                  td.appendChild(button);
                  showRow.appendChild(td);
                  return showRow;
                }}

                function collapseDetailedFindingGroups(limit) {{
                  const table = document.querySelector('.findings-table');
                  if (!table) return;
                  const rows = Array.from(table.rows);
                  let currentHidden = [];
                  let countInGroup = 0;

                  function flushGroup() {{
                    if (currentHidden.length === 0) return;
                    const showRow = makeShowMoreRow(11, currentHidden.slice(), 'finding(s)');
                    currentHidden[0].parentNode.insertBefore(showRow, currentHidden[0]);
                    currentHidden = [];
                  }}

                  rows.forEach(function (row) {{
                    if (row.classList.contains('category-section')) {{
                      flushGroup();
                      countInGroup = 0;
                      return;
                    }}
                    if (row.querySelector('th')) return;
                    if (!row.closest('.findings-table')) return;
                    countInGroup += 1;
                    if (countInGroup > limit) {{
                      row.classList.add('collapsed-finding-row');
                      currentHidden.push(row);
                    }}
                  }});
                  flushGroup();
                }}

                function collapseLongTables(limit) {{
                  const allTables = Array.from(document.querySelectorAll('table'));
                  allTables.forEach(function (table) {{
                    if (table.classList.contains('findings-table')) return;
                    const rows = Array.from(table.rows);
                    const dataRows = rows.filter(function (row) {{ return !row.querySelector('th'); }});
                    if (dataRows.length <= limit) return;
                    const hiddenRows = dataRows.slice(limit);
                    hiddenRows.forEach(function (row) {{ row.classList.add('collapsed-finding-row'); }});
                    const colCount = rows[0] ? Math.max(rows[0].children.length, 1) : 1;
                    const showRow = makeShowMoreRow(colCount, hiddenRows.slice(), 'row(s)');
                    hiddenRows[0].parentNode.insertBefore(showRow, hiddenRows[0]);
                  }});
                }}

                function collapseResourceDiscoveryCoverageGapSummary() {{
                  const header = document.getElementById('cat-resource-discovery-coverage-gap-summary');
                  if (!header) return;
                  const row = header.nextElementSibling;
                  if (!row) return;
                  const cells = row.querySelectorAll('td');
                  if (cells.length < 9) return;
                  const evidenceCell = cells[8];
                  if (!evidenceCell || evidenceCell.querySelector('details')) return;

                  const originalHtml = evidenceCell.innerHTML;
                  const details = document.createElement('details');
                  details.className = 'inline-details';
                  const summary = document.createElement('summary');
                  summary.textContent = 'Show resource discovery coverage details';
                  const block = document.createElement('div');
                  block.className = 'inline-details-block';
                  block.innerHTML = originalHtml;
                  details.appendChild(summary);
                  details.appendChild(block);
                  evidenceCell.innerHTML = '';
                  evidenceCell.appendChild(details);
                }}

                colorizeStatusCells();
                collapseDetailedFindingGroups(25);
                collapseLongTables(25);
                collapseResourceDiscoveryCoverageGapSummary();
              }})();
            </script>
          </body>
          </html>"""

          def put_object_text(key, body, content_type):
              s3.put_object(
                  Bucket=REPORT_BUCKET,
                  Key=key,
                  Body=body.encode("utf-8"),
                  ContentType=content_type
              )

          def put_report(account_id, timestamp_slug, run_slug, name, body, content_type):
              # Preserve the original timestamp path for backward compatibility.
              key = f"{REPORT_PREFIX}/{account_id}/{timestamp_slug}/{name}"
              put_object_text(key, body, content_type)

              # Preserve the stable latest path for automation and bookmarks.
              latest_key = f"{REPORT_PREFIX}/{account_id}/latest/{name}"
              put_object_text(latest_key, body, content_type)

              # Add v1.5.0 history path for browseable historical reports.
              run_key = f"{REPORT_PREFIX}/{account_id}/runs/{run_slug}/{name}"
              put_object_text(run_key, body, content_type)

              return key, latest_key, run_key

          def s3_uri(key):
              return f"s3://{REPORT_BUCKET}/{key}"

          def relative_history_href(account_id, key):
              prefix = f"{REPORT_PREFIX}/{account_id}/"
              if key.startswith(prefix):
                  return key[len(prefix):]
              return key

          def load_report_runs(account_id):
              manifest_key = f"{REPORT_PREFIX}/{account_id}/report-runs.json"
              try:
                  obj = s3.get_object(Bucket=REPORT_BUCKET, Key=manifest_key)
                  data = json.loads(obj["Body"].read().decode("utf-8"))
                  if isinstance(data, list):
                      return data
                  if isinstance(data, dict) and isinstance(data.get("runs"), list):
                      return data.get("runs", [])
              except ClientError as e:
                  if e.response.get("Error", {}).get("Code") not in ("NoSuchKey", "404"):
                      print(f"REPORT_HISTORY_WARNING: unable to load existing report-runs.json: {e}")
              except Exception as e:
                  print(f"REPORT_HISTORY_WARNING: unable to parse existing report-runs.json: {e}")
              return []

          def make_report_index_html(account_id, account_alias, runs):
              rows = []
              for run in runs[:100]:
                  def link(label, uri_key):
                      if not uri_key:
                          return ""
                      return f'<a href="{escape(relative_history_href(account_id, uri_key))}">{escape(label)}</a>'
                  links = " | ".join([
                      link("HTML", run.get("HtmlReportKey")),
                      link("Findings CSV", run.get("FindingsCsvKey")),
                      link("JSON", run.get("JsonReportKey")),
                      link("Remediation", run.get("RemediationPlanKey")),
                      link("Backup/Snapshot CSV", run.get("BackupSnapshotReviewCsvKey")),
                  ])
                  rows.append(
                      "<tr>"
                      f"<td>{escape(str(run.get('GeneratedAt', '')))}</td>"
                      f"<td>{escape(str(run.get('TemplateVersion', '')))}</td>"
                      f"<td>{escape(str(run.get('ToolkitVersion', '')))}</td>"
                      f"<td>{escape(str(run.get('TemplateBuild', '')))}</td>"
                      f"<td>{escape(str(run.get('TotalFindings', '')))}</td>"
                      f"<td>{escape(str(run.get('DirectMonthlySavingsEstimate', '')))}</td>"
                      f"<td>{escape(str(run.get('GrossMonthlyReviewPotential', '')))}</td>"
                      f"<td>{escape(str(run.get('CollectionWarnings', '')))}</td>"
                      f"<td>{links}</td>"
                      "</tr>"
                  )

              if not rows:
                  rows.append('<tr><td colspan="9">No report runs have been recorded yet.</td></tr>')

              return f"""<!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>AWS Cost Optimization Report History - {escape(account_id)}</title>
            <style>
              body {{ margin: 0; background: #0a0a0a; color: #fff; font-family: Inter, Segoe UI, Arial, sans-serif; }}
              header {{ padding: 28px; background: linear-gradient(135deg, #111 0%, #1b2630 100%); border-bottom: 1px solid #333; }}
              main {{ padding: 24px; }}
              h1 {{ margin: 0; font-size: 28px; }}
              .subtitle {{ color: #b0b0b0; margin-top: 8px; }}
              table {{ width: 100%; border-collapse: collapse; margin-top: 18px; background: #1a1a1a; }}
              th, td {{ border: 1px solid #333; padding: 10px; vertical-align: top; font-size: 13px; }}
              th {{ background: #2a2a2a; color: #00d4aa; text-align: left; }}
              a {{ color: #00d4aa; text-decoration: none; }}
              a:hover {{ text-decoration: underline; }}
              .notice {{ border-left: 4px solid #ffa502; padding: 12px 16px; background: rgba(255,165,2,0.08); color: #b0b0b0; margin-top: 18px; }}
            </style>
          </head>
          <body>
            <header>
              <h1>AWS Cost Optimization Report History</h1>
              <div class="subtitle">Account {escape(account_id)} / {escape(account_alias or '')}</div>
            </header>
            <main>
              <div class="notice">This index is report-only. It links to historical report artifacts generated by the toolkit.</div>
              <table>
                <tr><th>Generated At</th><th>Template Version</th><th>Toolkit</th><th>Build</th><th>Total Findings</th><th>Direct Monthly</th><th>Gross Monthly Review</th><th>Warnings</th><th>Artifacts</th></tr>
                {''.join(rows)}
              </table>
            </main>
          </body>
          </html>"""

          def update_report_history(account_id, account_alias, environment, summary, artifact_keys):
              generated_at = summary.get("generated_at") or utc_now().isoformat()
              manifest_key = f"{REPORT_PREFIX}/{account_id}/report-runs.json"
              index_key = f"{REPORT_PREFIX}/{account_id}/index.html"

              entry = {
                  "AccountId": account_id,
                  "AccountAlias": account_alias,
                  "Environment": environment,
                  "GeneratedAt": generated_at,
                  "TemplateVersion": summary.get("template_version", TEMPLATE_VERSION),
                  "ToolkitVersion": summary.get("toolkit_version", TOOLKIT_VERSION),
                  "TemplateBuild": summary.get("template_build", TEMPLATE_BUILD),
                  "TotalFindings": summary.get("total_findings"),
                  "DirectMonthlySavingsEstimate": summary.get("direct_monthly_savings_estimate"),
                  "GrossMonthlyReviewPotential": summary.get("gross_monthly_review_potential"),
                  "CollectionWarnings": summary.get("warnings_count"),
                  "HtmlReportKey": artifact_keys.get("html"),
                  "JsonReportKey": artifact_keys.get("json"),
                  "FindingsCsvKey": artifact_keys.get("csv"),
                  "RemediationPlanKey": artifact_keys.get("remediation_plan"),
                  "BackupSnapshotReviewCsvKey": artifact_keys.get("backup_snapshot_review_csv"),
                  "HtmlReportS3Uri": s3_uri(artifact_keys.get("html")),
                  "JsonReportS3Uri": s3_uri(artifact_keys.get("json")),
                  "FindingsCsvS3Uri": s3_uri(artifact_keys.get("csv")),
                  "RemediationPlanS3Uri": s3_uri(artifact_keys.get("remediation_plan")),
                  "BackupSnapshotReviewCsvS3Uri": s3_uri(artifact_keys.get("backup_snapshot_review_csv")),
              }

              runs = load_report_runs(account_id)
              runs = [r for r in runs if r.get("GeneratedAt") != generated_at]
              runs.insert(0, entry)
              runs = runs[:100]

              manifest_body = json.dumps({"runs": runs}, indent=2, default=json_safe)
              put_object_text(manifest_key, manifest_body, "application/json")

              index_body = make_report_index_html(account_id, account_alias, runs)
              put_object_text(index_key, index_body, "text/html")

              return manifest_key, index_key

          def send_cfn_response(event, context, status, data=None, reason=None, physical_resource_id=None):
              response_url = event.get("ResponseURL")
              if not response_url:
                  return
              body = {
                  "Status": status,
                  "Reason": reason or f"See CloudWatch Log Stream: {context.log_stream_name}",
                  "PhysicalResourceId": physical_resource_id or f"{PROJECT_NAME}-initial-report",
                  "StackId": event.get("StackId"),
                  "RequestId": event.get("RequestId"),
                  "LogicalResourceId": event.get("LogicalResourceId"),
                  "NoEcho": False,
                  "Data": data or {}
              }
              encoded = json.dumps(body, default=json_safe).encode("utf-8")
              request = urllib.request.Request(
                  response_url,
                  data=encoded,
                  method="PUT",
                  headers={"content-type": "", "content-length": str(len(encoded))}
              )
              urllib.request.urlopen(request, timeout=10)

          def timed_check(timings, check_name, callable_fn):
              started = utc_now()
              status = "SUCCESS"
              error = ""
              try:
                  return callable_fn()
              except Exception as e:
                  status = "ERROR"
                  error = str(e)
                  raise
              finally:
                  ended = utc_now()
                  timings.append({
                      "check": check_name,
                      "status": status,
                      "duration_seconds": round((ended - started).total_seconds(), 3),
                      "error": error
                  })

          def run_report():
              account_id = get_account_id()
              started = utc_now()
              timestamp_slug = started.strftime("%Y%m%d-%H%M%S")
              run_slug = started.strftime("%Y-%m-%dT%H-%M-%SZ")
              findings = []
              warnings = []
              timings = []
              remediation_plan = []
              discovery_counts = {}

              config, config_created = load_or_create_config(account_id)
              regions = get_enabled_regions(config)
              account_alias = get_account_alias(warnings)

              print(f"Starting cost optimization scan for account={account_id}, regions={regions}")

              # S3 is account-global for bucket listing. Run it once before the regional loop
              # so it cannot be skipped because of regional scan logic.
              if is_check_enabled(config, "s3_lifecycle_review", True) or is_check_enabled(config, "s3_review", True):
                  try:
                      print("S3_DIAGNOSTIC: Starting global S3 review before regional scans")
                      before_s3_findings = len(findings)
                      s3_started = utc_now()
                      scan_s3_global(config, findings, warnings)
                      after_s3_findings = len(findings)
                      timings.append({"check": "S3 Lifecycle / Versioning / Large Bucket Review", "status": "SUCCESS", "duration_seconds": round((utc_now() - s3_started).total_seconds(), 3), "error": ""})
                      print(f"S3_DIAGNOSTIC: Finished global S3 review; findings_added={after_s3_findings - before_s3_findings}")
                  except Exception as e:
                      timings.append({"check": "S3 Lifecycle / Versioning / Large Bucket Review", "status": "ERROR", "duration_seconds": 0, "error": str(e)})
                      warnings.append({"region": "global", "service": "S3", "operation": "scan_s3_global_pre_region", "error": str(e), "trace": traceback.format_exc()})

              for region in regions:
                  print(f"Scanning region {region}")

                  if (is_check_enabled(config, "unused_eips", True) or is_check_enabled(config, "unattached_ebs", True) or is_check_enabled(config, "stopped_ec2_with_ebs", True)) and not is_service_region_excluded(config, "ec2_ebs_base_review", region):
                      try:
                          timed_check(timings, f"EC2 / EBS Base Review ({region})", lambda: scan_ec2_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "EC2", "operation": "scan_ec2_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "ebs_gp2_conversion", True) and not is_service_region_excluded(config, "ebs_gp2_conversion", region):
                      try:
                          timed_check(timings, f"EBS gp2 Volume Conversion Review ({region})", lambda: scan_ebs_gp2_conversion_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "EBS", "operation": "scan_ebs_gp2_conversion_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "ec2_modernization_rightsizing_review", True) and not is_service_region_excluded(config, "ec2_modernization_rightsizing_review", region):
                      try:
                          timed_check(timings, f"EC2 Modernization and Rightsizing Review ({region})", lambda: scan_ec2_modernization_rightsizing_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "EC2", "operation": "scan_ec2_modernization_rightsizing_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "nat_gateway_idle", True) and not is_service_region_excluded(config, "nat_gateway_idle", region):
                      try:
                          timed_check(timings, f"NAT Gateway Idle Review ({region})", lambda: scan_nat_gateways_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "VPC/NAT Gateway", "operation": "scan_nat_gateways_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "snapshots_and_amis", True) and not is_service_region_excluded(config, "snapshots_and_amis", region):
                      try:
                          timed_check(timings, f"AMI / EBS Snapshot Review ({region})", lambda: scan_snapshots_and_amis_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "EC2/EBS", "operation": "scan_snapshots_and_amis_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "lambda_no_invocations", True) and not is_service_region_excluded(config, "lambda_no_invocations", region):
                      try:
                          timed_check(timings, f"Lambda No-Invocations Review ({region})", lambda: scan_lambda_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "Lambda", "operation": "scan_lambda_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "elb_review", True) and not is_service_region_excluded(config, "elb_review", region):
                      try:
                          timed_check(timings, f"ELBv2 Load Balancer / Target Group Review ({region})", lambda: scan_elb_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "ELBv2", "operation": "scan_elb_region", "error": str(e), "trace": traceback.format_exc()})

                  if (is_check_enabled(config, "cloudwatch_log_retention", True) or is_check_enabled(config, "large_cloudwatch_logs", True)) and not is_service_region_excluded(config, "cloudwatch_logs_review", region):
                      try:
                          timed_check(timings, f"CloudWatch Logs Review ({region})", lambda: scan_logs_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "CloudWatch Logs", "operation": "scan_logs_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "rds_basic_review", True) and not is_service_region_excluded(config, "rds_basic_review", region):
                      try:
                          timed_check(timings, f"RDS Basic / Performance / Snapshot Review ({region})", lambda: scan_rds_phase1_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "RDS", "operation": "scan_rds_phase1_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "efs_review", True) and not is_service_region_excluded(config, "efs_review", region):
                      try:
                          timed_check(timings, f"EFS File System Review ({region})", lambda: scan_efs_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "EFS", "operation": "scan_efs_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "ecr_review", True) and not is_service_region_excluded(config, "ecr_review", region):
                      try:
                          timed_check(timings, f"ECR Repository Storage / Lifecycle Review ({region})", lambda: scan_ecr_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "ECR", "operation": "scan_ecr_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "dynamodb_review", True) and not is_service_region_excluded(config, "dynamodb_review", region):
                      try:
                          timed_check(timings, f"DynamoDB Table Cost Review ({region})", lambda: scan_dynamodb_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "DynamoDB", "operation": "scan_dynamodb_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "opensearch_review", True) and not is_service_region_excluded(config, "opensearch_review", region):
                      try:
                          timed_check(timings, f"OpenSearch Service Domain Cost Review ({region})", lambda: scan_opensearch_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "OpenSearch", "operation": "scan_opensearch_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "resource_discovery_summary", True) and not is_service_region_excluded(config, "resource_discovery_summary", region):
                      try:
                          timed_check(timings, f"Resource Discovery Summary ({region})", lambda: scan_resource_discovery_region(region, config, findings, warnings, discovery_counts))
                      except Exception as e:
                          warnings.append({"region": region, "service": "Resource Groups Tagging API", "operation": "scan_resource_discovery_region", "error": str(e), "trace": traceback.format_exc()})

                  if is_check_enabled(config, "aws_backup_review", True) and not is_service_region_excluded(config, "aws_backup_review", region):
                      try:
                          timed_check(timings, f"AWS Backup Recovery Point Review ({region})", lambda: scan_aws_backup_region(region, config, findings, warnings))
                      except Exception as e:
                          warnings.append({"region": region, "service": "AWS Backup", "operation": "scan_aws_backup_region", "error": str(e), "trace": traceback.format_exc()})

              if is_check_enabled(config, "resource_discovery_summary", True):
                  try:
                      add_resource_discovery_summary(findings, discovery_counts)
                  except Exception as e:
                      warnings.append({"region": "global", "service": "Resource Discovery", "operation": "add_resource_discovery_summary", "error": str(e), "trace": traceback.format_exc()})

              remediation_plan = enrich_findings_with_cli(findings)
              timings.append({
                  "check": "Total Report Runtime",
                  "status": "SUCCESS",
                  "duration_seconds": round((utc_now() - started).total_seconds(), 3),
                  "error": ""
              })
              summary = summarize(findings, warnings, config, account_id, account_alias, regions, config_created, timings)
              report = {
                  "summary": summary,
                  "findings": findings,
                  "warnings": warnings,
                  "remediation_plan": remediation_plan,
                  "config": config
              }

              json_body = json.dumps(report, indent=2, default=json_safe)
              csv_body = make_csv(findings)
              backup_snapshot_review_body = make_backup_snapshot_review_csv(findings)
              remediation_plan_body = json.dumps({"summary": summary, "remediation_plan": remediation_plan}, indent=2, default=json_safe)
              html_body = make_html(summary, findings, warnings, config)

              json_filename = f"{account_id}-cost-report.json"
              csv_filename = f"{account_id}-cost-findings.csv"
              backup_snapshot_review_filename = f"{account_id}-backup-snapshot-review.csv"
              remediation_plan_filename = f"{account_id}-remediation-plan.json"
              html_filename = f"{account_id}-cost-report.html"

              json_key, json_latest, json_run = put_report(account_id, timestamp_slug, run_slug, json_filename, json_body, "application/json")
              csv_key, csv_latest, csv_run = put_report(account_id, timestamp_slug, run_slug, csv_filename, csv_body, "text/csv")
              backup_snapshot_review_key, backup_snapshot_review_latest, backup_snapshot_review_run = put_report(account_id, timestamp_slug, run_slug, backup_snapshot_review_filename, backup_snapshot_review_body, "text/csv")
              remediation_plan_key, remediation_plan_latest, remediation_plan_run = put_report(account_id, timestamp_slug, run_slug, remediation_plan_filename, remediation_plan_body, "application/json")
              html_key, html_latest, html_run = put_report(account_id, timestamp_slug, run_slug, html_filename, html_body, "text/html")

              report_runs_key, report_index_key = update_report_history(
                  account_id,
                  account_alias,
                  summary.get("environment") or account_alias,
                  summary,
                  {
                      "html": html_run,
                      "json": json_run,
                      "csv": csv_run,
                      "backup_snapshot_review_csv": backup_snapshot_review_run,
                      "remediation_plan": remediation_plan_run
                  }
              )

              result = {
                  "status": "SUCCESS",
                  "mode": "ReportOnly",
                  "account_id": account_id,
                  "regions_scanned": regions,
                  "total_findings": len(findings),
                  "warnings_count": len(warnings),
                  "s3_bucket": REPORT_BUCKET,
                  "reports": {
                      "html": html_key,
                      "json": json_key,
                      "csv": csv_key,
                      "backup_snapshot_review_csv": backup_snapshot_review_key,
                      "remediation_plan": remediation_plan_key,
                      "latest_html": html_latest,
                      "latest_json": json_latest,
                      "latest_csv": csv_latest,
                      "latest_backup_snapshot_review_csv": backup_snapshot_review_latest,
                      "latest_remediation_plan": remediation_plan_latest,
                      "run_html": html_run,
                      "run_json": json_run,
                      "run_csv": csv_run,
                      "run_backup_snapshot_review_csv": backup_snapshot_review_run,
                      "run_remediation_plan": remediation_plan_run,
                      "report_runs_manifest": report_runs_key,
                      "report_index": report_index_key
                  },
                  "config_created": config_created,
                  "config_key": f"{CONFIG_PREFIX}/{account_id}.json"
              }
              print(json.dumps(result, indent=2, default=json_safe))
              return result

          def handler(event, context):
              print(json.dumps(event, indent=2, default=json_safe))

              # Async report path started by the CloudFormation custom resource.
              if isinstance(event, dict) and event.get("Action") == "RunReportAsync":
                  return run_report()

              # CloudFormation custom resource path. Do not run the full report synchronously here.
              # Start a separate async Lambda invocation and respond to CloudFormation quickly.
              if isinstance(event, dict) and event.get("RequestType"):
                  physical_id = event.get("PhysicalResourceId") or f"{PROJECT_NAME}-initial-report"
                  try:
                      if event.get("RequestType") == "Delete":
                          send_cfn_response(event, context, "SUCCESS", {"Message": "No action on delete."}, physical_resource_id=physical_id)
                          return {"status": "SUCCESS", "message": "No action on delete."}

                      lambda_client = boto3.client("lambda")
                      lambda_client.invoke(
                          FunctionName=context.function_name,
                          InvocationType="Event",
                          Payload=json.dumps({
                              "Action": "RunReportAsync",
                              "Source": "CloudFormationCustomResource",
                              "RunReportToken": event.get("ResourceProperties", {}).get("RunReportToken", "")
                          }).encode("utf-8")
                      )
                      send_cfn_response(
                          event,
                          context,
                          "SUCCESS",
                          {
                              "Message": "Initial report started asynchronously.",
                              "LatestHtmlReport": "Report generation is running asynchronously. Check S3 latest path and Lambda logs."
                          },
                          physical_resource_id=physical_id
                      )
                      return {"status": "SUCCESS", "message": "Initial report started asynchronously."}
                  except Exception as e:
                      reason = str(e)
                      try:
                          send_cfn_response(event, context, "FAILED", {"Error": reason}, reason=reason, physical_resource_id=physical_id)
                      finally:
                          raise

              # Normal Lambda invoke path.
              return run_report()

  InitialReportRun:
    Type: Custom::CostOptimizationInitialReport
    Condition: RunInitialReport
    DependsOn:
      - CostReportLambda
      - ReportBucketPolicy
    Properties:
      ServiceToken: !GetAtt CostReportLambda.Arn
      RunReportToken: !Ref RunReportToken

  ScheduledRule:
    Type: AWS::Events::Rule
    Condition: EnableScheduledRun
    Properties:
      Name: !Sub '${ProjectName}-schedule'
      Description: Scheduled run for AWS Cost Optimization Report Generator.
      ScheduleExpression: !Ref ScheduleExpression
      State: ENABLED
      Targets:
        - Arn: !GetAtt CostReportLambda.Arn
          Id: CostReportLambdaTarget


  ScheduledRuleInvokePermission:
    Type: AWS::Lambda::Permission
    Condition: EnableScheduledRun
    Properties:
      FunctionName: !Ref CostReportLambda
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt ScheduledRule.Arn

Outputs:
  TemplateVersionOutput:
    Description: Template version marker for deployment verification.
    Value: !Ref TemplateVersion

  ForceLambdaUpdateTokenOutput:
    Description: Lambda update token used for this deployment.
    Value: !Ref ForceLambdaUpdateToken

  ReportBucketName:
    Description: S3 bucket containing generated reports and config.
    Value: !Ref ReportBucket

  LambdaFunctionName:
    Description: Lambda function name.
    Value: !Ref CostReportLambda

  ConfigLocation:
    Description: Per-account config location.
    Value: !Sub 's3://${ReportBucket}/${ConfigPrefix}/${AWS::AccountId}.json'

  LatestHtmlReport:
    Description: Latest HTML report location.
    Value: !Sub 's3://${ReportBucket}/${ReportPrefix}/${AWS::AccountId}/latest/${AWS::AccountId}-cost-report.html'

  LatestJsonReport:
    Description: Latest JSON report location.
    Value: !Sub 's3://${ReportBucket}/${ReportPrefix}/${AWS::AccountId}/latest/${AWS::AccountId}-cost-report.json'

  LatestCsvReport:
    Description: Latest CSV findings location.
    Value: !Sub 's3://${ReportBucket}/${ReportPrefix}/${AWS::AccountId}/latest/${AWS::AccountId}-cost-findings.csv'

  LatestBackupSnapshotReviewCsv:
    Description: Latest backup and snapshot review CSV location.
    Value: !Sub 's3://${ReportBucket}/${ReportPrefix}/${AWS::AccountId}/latest/${AWS::AccountId}-backup-snapshot-review.csv'


  ReportHistoryIndex:
    Description: Per-account report history index page.
    Value: !Sub 's3://${ReportBucket}/${ReportPrefix}/${AWS::AccountId}/index.html'

  ReportRunsManifest:
    Description: Per-account report history manifest.
    Value: !Sub 's3://${ReportBucket}/${ReportPrefix}/${AWS::AccountId}/report-runs.json'
