AWSTemplateFormatVersion: '2010-09-09'
Description: >
  Jungle Cleaner — read-only cross-account access.
  Deploys a read-only IAM role, phones home to confirm the connection,
  then self-deletes. Only the IAM role remains in your account.

Parameters:
  ExternalId:
    Type: String
    Description: Unique token provided by Jungle Cleaner.

  JungleCleanerAccountId:
    Type: String
    Default: '154959837717'
    Description: The AWS account ID that Jungle Cleaner uses to assume this role.

  ApiUrl:
    Type: String
    Default: 'https://junglecleaner.com/api/connect'
    Description: Jungle Cleaner endpoint that receives the phone-home callback.

Resources:
  # ── Read-only IAM role ─────────────────────────────────────────────────────
  JungleCleanerRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      RoleName: JungleCleanerReadOnly
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${JungleCleanerAccountId}:root'
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Ref ExternalId
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/ReadOnlyAccess
      Policies:
        - PolicyName: ExtraReadOnly
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ce:*
                  - cost-optimization-hub:GetRecommendation
                  - cost-optimization-hub:ListRecommendations
                  - cost-optimization-hub:ListRecommendationSummaries
                  - compute-optimizer:GetRecommendationSummaries
                  - compute-optimizer:GetEC2InstanceRecommendations
                  - compute-optimizer:GetLambdaFunctionRecommendations
                  - waf:ListWebACLs
                  - waf:ListResourcesForWebACL
                  - waf-regional:ListWebACLs
                  - waf-regional:ListResourcesForWebACL
                  - quicksight:ListUsers
                Resource: '*'
        - PolicyName: EnableCostServices
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: OptInComputeOptimizer
                Effect: Allow
                Action: compute-optimizer:UpdateEnrollmentStatus
                Resource: '*'
              - Sid: OptInCostOptimizationHub
                Effect: Allow
                Action:
                  - cost-optimization-hub:UpdateEnrollmentStatus
                Resource: '*'
              - Sid: EnableCOHOrgAccess
                Effect: Allow
                Action:
                  - organizations:EnableAWSServiceAccess
                Resource: '*'
                Condition:
                  StringLike:
                    organizations:ServicePrincipal:
                      - cost-optimization-hub.bcm.amazonaws.com
              - Sid: CreateCOHServiceLinkedRole
                Effect: Allow
                Action: iam:CreateServiceLinkedRole
                Resource: arn:aws:iam::*:role/aws-service-role/cost-optimization-hub.bcm.amazonaws.com/*
                Condition:
                  StringLike:
                    iam:AWSServiceName: cost-optimization-hub.bcm.amazonaws.com
              - Sid: CreateComputeOptimizerServiceLinkedRole
                Effect: Allow
                Action: iam:CreateServiceLinkedRole
                Resource: arn:aws:iam::*:role/aws-service-role/compute-optimizer.amazonaws.com/*
              - Sid: PatchServiceLinkedRoles
                Effect: Allow
                Action: iam:PutRolePolicy
                Resource:
                  - arn:aws:iam::*:role/aws-service-role/cost-optimization-hub.bcm.amazonaws.com/*
      Tags:
        - Key: ManagedBy
          Value: JungleCleaner

  # ── CloudFormation self-deletion execution role ────────────────────────────
  # CloudFormation assumes this role to delete the stack's resources, so it
  # never uses the Lambda's STS session token (which expires when the function
  # ends, causing PhoneHomeLambdaRole DELETE_FAILED).
  CFCleanupRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: CFCleanupPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                  - lambda:DeleteFunction
                Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AWS::StackName}-PhoneHomeLambda*'
              - Effect: Allow
                Action:
                  - iam:DetachRolePolicy
                  - iam:DeleteRolePolicy
                  - iam:DeleteRole
                Resource:
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}-PhoneHomeLambdaRole*'
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}-CFCleanupRole*'

  # ── Phone-home Lambda execution role ───────────────────────────────────────
  PhoneHomeLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: PhoneHomePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: '*'
              - Effect: Allow
                Action: cloudformation:DeleteStack
                Resource: !Ref AWS::StackId
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                  - lambda:DeleteFunction
                Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AWS::StackName}-PhoneHomeLambda*'
              - Effect: Allow
                Action: iam:PassRole
                Resource: !GetAtt CFCleanupRole.Arn

  # ── Phone-home Lambda ──────────────────────────────────────────────────────
  PhoneHomeLambda:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: nodejs24.x
      Handler: index.handler
      Timeout: 30
      Role: !GetAtt PhoneHomeLambdaRole.Arn
      Environment:
        Variables:
          STACK_ID: !Ref AWS::StackId
          REGION: !Ref AWS::Region
          CF_CLEANUP_ROLE_ARN: !GetAtt CFCleanupRole.Arn
      Code:
        ZipFile: |
          const { CloudFormationClient, DeleteStackCommand } = require('@aws-sdk/client-cloudformation');
          const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');

          exports.handler = async (event, context) => {
            console.log('Event:', JSON.stringify(event));

            // Phase 2: async self-invocation to delete the stack after CF has
            // processed our SUCCESS signal.
            if (event._selfDelete) {
              const cf = new CloudFormationClient({ region: process.env.REGION });
              await cf.send(new DeleteStackCommand({
                StackName: process.env.STACK_ID,
                RoleARN: process.env.CF_CLEANUP_ROLE_ARN,
              }));
              return;
            }

            if (event.RequestType === 'Delete') {
              await sendResponse(event, context, 'SUCCESS', {});
              return;
            }

            const { Token, RoleArn, ApiUrl } = event.ResourceProperties;
            const accountId = context.invokedFunctionArn.split(':')[4];

            try {
              const res = await fetch(ApiUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ token: Token, roleArn: RoleArn, accountId }),
              });
              console.log('API response:', res.status, await res.text());

              // Signal SUCCESS to CloudFormation first, then self-invoke to delete
              // the stack asynchronously so there's no race condition.
              await sendResponse(event, context, 'SUCCESS', {});

              const lambda = new LambdaClient({ region: process.env.REGION });
              await lambda.send(new InvokeCommand({
                FunctionName: context.functionName,
                InvocationType: 'Event',
                Payload: JSON.stringify({ _selfDelete: true }),
              }));
            } catch (err) {
              console.error('Error:', err);
              await sendResponse(event, context, 'FAILED', { Error: String(err) });
            }
          };

          async function sendResponse(event, context, status, data) {
            const body = JSON.stringify({
              Status: status,
              Reason: 'See CloudWatch Logs',
              PhysicalResourceId: context.logStreamName,
              StackId: event.StackId,
              RequestId: event.RequestId,
              LogicalResourceId: event.LogicalResourceId,
              Data: data,
            });
            await fetch(event.ResponseURL, {
              method: 'PUT',
              headers: { 'Content-Type': '' },
              body,
            });
          }

  # ── Custom resource — triggers Lambda on stack create ─────────────────────
  PhoneHomeResource:
    Type: AWS::CloudFormation::CustomResource
    DependsOn: JungleCleanerRole
    Properties:
      ServiceToken: !GetAtt PhoneHomeLambda.Arn
      Token: !Ref ExternalId
      RoleArn: !GetAtt JungleCleanerRole.Arn
      ApiUrl: !Ref ApiUrl

Outputs:
  RoleArn:
    Description: Read-only IAM role ARN (retained after stack self-deletes).
    Value: !GetAtt JungleCleanerRole.Arn
