AWSTemplateFormatVersion: '2010-09-09'
Description: 'Improved CloudWatch Alarms integration with WhatsApp notifications via AWS End User Messaging'

Parameters:
  WhatsAppPhoneNumberId:
    Type: String
    Description: 'AWS End User Messaging WhatsApp Phone Number ID'
    MinLength: 1
  
  AlertRecipient:
    Type: String
    Description: 'WhatsApp number to receive alerts (with country code, e.g., +1234567890)'
    MinLength: 10
    AllowedPattern: '^\+[1-9]\d{6,14}$'
    ConstraintDescription: 'Must be a valid international phone number starting with +'
  
  AlarmPrefix:
    Type: String
    Description: 'Prefix for alarm names'
    Default: 'CW-WhatsApp'
  
  Environment:
    Type: String
    Description: 'Environment name'
    Default: 'dev'
    AllowedValues: [dev, staging, prod]
  
  LogRetentionDays:
    Type: Number
    Description: 'CloudWatch Logs retention period in days'
    Default: 14
    AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]

Resources:
  # CloudWatch Log Group for Lambda
  WhatsAppAlarmLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${AlarmPrefix}-whatsapp-notifier-${Environment}'
      RetentionInDays: !Ref LogRetentionDays

  # SNS Topic for CloudWatch Alarms
  AlarmNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub '${AlarmPrefix}-notifications-${Environment}'
      DisplayName: 'CloudWatch Alarms WhatsApp Notifications'

  # Lambda Execution Role
  WhatsAppAlarmLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AlarmPrefix}-lambda-role-${Environment}'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: WhatsAppMessagingPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - social-messaging:SendWhatsAppMessage
                  - social-messaging:GetWhatsAppMessageMedia
                Resource: '*'
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'

  # Lambda Function for WhatsApp Notifications
  WhatsAppAlarmFunction:
    Type: AWS::Lambda::Function
    DependsOn: WhatsAppAlarmLogGroup
    Properties:
      FunctionName: !Sub '${AlarmPrefix}-whatsapp-notifier-${Environment}'
      Runtime: python3.9
      Handler: index.lambda_handler
      Role: !GetAtt WhatsAppAlarmLambdaRole.Arn
      Timeout: 60
      MemorySize: 512
      Environment:
        Variables:
          WHATSAPP_PHONE_NUMBER_ID: !Ref WhatsAppPhoneNumberId
          ALERT_RECIPIENT: !Ref AlertRecipient
          ENVIRONMENT: !Ref Environment
      Code:
        ZipFile: |
          import json
          import boto3
          import os
          import logging
          from datetime import datetime
          from botocore.exceptions import ClientError, BotoCoreError

          # Configure logging
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def lambda_handler(event, context):
              """
              Process CloudWatch Alarm notifications and send WhatsApp messages
              """
              logger.debug(f"Lambda function started. Event: {json.dumps(event, default=str)}")
              
              # Validate environment variables
              required_env_vars = ['WHATSAPP_PHONE_NUMBER_ID', 'ALERT_RECIPIENT']
              missing_vars = [var for var in required_env_vars if not os.environ.get(var)]
              
              if missing_vars:
                  error_msg = f"Missing required environment variables: {missing_vars}"
                  logger.error(error_msg)
                  return {
                      'statusCode': 500,
                      'body': json.dumps({'error': error_msg})
                  }
              
              try:
                  processed_count = 0
                  errors = []
                  
                  # Process SNS messages
                  for record in event.get('Records', []):
                      try:
                          logger.info(f"Processing record: {json.dumps(record, default=str)}")
                          
                          if record.get('EventSource') == 'aws:sns':
                              process_alarm_notification(record)
                              processed_count += 1
                          else:
                              logger.warning(f"Unexpected event source: {record.get('EventSource')}")
                              
                      except Exception as e:
                          error_msg = f"Error processing record: {str(e)}"
                          logger.error(error_msg, exc_info=True)
                          errors.append(error_msg)
                  
                  # Return response
                  if errors:
                      return {
                          'statusCode': 207,  # Multi-status
                          'body': json.dumps({
                              'message': f'Processed {processed_count} records with {len(errors)} errors',
                              'errors': errors
                          })
                      }
                  else:
                      return {
                          'statusCode': 200,
                          'body': json.dumps({
                              'message': f'Successfully processed {processed_count} notifications'
                          })
                      }
                      
              except Exception as e:
                  logger.error(f"Unexpected error in lambda_handler: {str(e)}", exc_info=True)
                  return {
                      'statusCode': 500,
                      'body': json.dumps({'error': f'Unexpected error: {str(e)}'})
                  }

          def process_alarm_notification(record):
              """Process individual alarm notification"""
              try:
                  # Validate SNS record structure
                  if 'Sns' not in record:
                      raise ValueError("Invalid SNS record: missing 'Sns' key")
                  
                  sns_data = record['Sns']
                  if 'Message' not in sns_data:
                      raise ValueError("Invalid SNS record: missing 'Message' key")
                  
                  # Parse SNS message
                  try:
                      sns_message = json.loads(sns_data['Message'])
                      logger.info(f"Parsed SNS message: {json.dumps(sns_message, default=str)}")
                  except json.JSONDecodeError as e:
                      raise ValueError(f"Failed to parse SNS message as JSON: {str(e)}")
                  
                  # Extract alarm details with defaults
                  alarm_details = {
                      'alarm_name': sns_message.get('AlarmName', 'Unknown Alarm'),
                      'alarm_description': sns_message.get('AlarmDescription', 'No description available'),
                      'new_state': sns_message.get('NewStateValue', 'UNKNOWN'),
                      'old_state': sns_message.get('OldStateValue', 'UNKNOWN'),
                      'reason': sns_message.get('NewStateReason', 'No reason provided'),
                      'timestamp': sns_message.get('StateChangeTime', ''),
                      'region': sns_message.get('Region', os.environ.get('AWS_REGION', 'Unknown')),
                      'account_id': sns_message.get('AWSAccountId', 'Unknown')
                  }
                  
                  logger.info(f"Extracted alarm details: {alarm_details}")
                  
                  # Format WhatsApp message
                  message = format_alarm_message(**alarm_details)
                  logger.info(f"Formatted message: {message}")
                  
                  # Send WhatsApp message
                  send_whatsapp_message(message)
                  
              except Exception as e:
                  logger.error(f"Error processing alarm notification: {str(e)}", exc_info=True)
                  raise

          def format_alarm_message(alarm_name, alarm_description, new_state, old_state, reason, timestamp, region, account_id):
              """Format alarm information for WhatsApp"""
              
              # Choose emoji based on alarm state
              state_emoji = {
                  'ALARM': '🚨',
                  'OK': '✅',
                  'INSUFFICIENT_DATA': '⚠️'
              }.get(new_state, '📊')
              
              # Format timestamp
              formatted_time = format_timestamp(timestamp)
              
              # Truncate long descriptions and reasons for mobile readability
              max_description_length = 200
              max_reason_length = 300
              
              if len(alarm_description) > max_description_length:
                  alarm_description = alarm_description[:max_description_length] + "..."
              
              if len(reason) > max_reason_length:
                  reason = reason[:max_reason_length] + "..."
              
              # Build message
              template_name = 'cw_alarm_notification'
              template_message = {
                      "name": template_name,
                      "language": {
                          "code": "en"
                      },
                      "components": [
                          {
                              "type": "header",
                              "parameters": [{
                                  "type": "text",
                                  "parameter_name": "emoji",
                                  "text": state_emoji
                                  }
                              ]
                          },
                          {
                              "type": "body",
                              "parameters": [
                                  {
                                      "type": "text",
                                      "parameter_name": "alarm_name",
                                      "text": alarm_name
                                  },
                                  {
                                      "type": "text",
                                      "parameter_name": "status",
                                      "text": old_state + " → " + new_state
                                  },
                                  {
                                      "type": "text",
                                      "parameter_name": "region_account",
                                      "text": region + " - " + account_id
                                  },
                                  {
                                      "type": "text",
                                      "parameter_name": "time",
                                      "text": formatted_time
                                  },
                                  {
                                      "type": "text",
                                      "parameter_name": "description",
                                      "text": alarm_description + reason
                                  }
                              ]
                          }

                      ]
                  }
              
              return template_message

          def format_timestamp(timestamp):
              """Format timestamp for display"""
              if not timestamp:
                  return "Unknown time"
              
              try:
                  # Handle different timestamp formats
                  if timestamp.endswith('Z'):
                      dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
                  elif '+' in timestamp or timestamp.endswith('UTC'):
                      dt = datetime.fromisoformat(timestamp.replace('UTC', '').strip())
                  else:
                      dt = datetime.fromisoformat(timestamp)
                  
                  return dt.strftime('%Y-%m-%d %H:%M:%S UTC')
              except Exception as e:
                  logger.warning(f"Failed to parse timestamp '{timestamp}': {str(e)}")
                  return timestamp

          def send_whatsapp_message(message):
              """Send WhatsApp message via AWS End User Messaging"""
              try:
                  # Initialize client with explicit region if needed
                  region = os.environ.get('AWS_REGION', 'us-east-1')
                  client = boto3.client('socialmessaging', region_name=region)
                  
                  # Get environment variables
                  phone_number_id = os.environ.get('WHATSAPP_PHONE_NUMBER_ID')
                  recipient = os.environ.get('ALERT_RECIPIENT')
                  
                  logger.info(f"Sending WhatsApp message to {recipient} via phone number ID: {phone_number_id}")
                  
                  # Prepare message object
                  message_object = {
                      "messaging_product": "whatsapp",
                      "recipient_type": "individual",
                      "to": recipient,
                      "type": "template",
                      "template": message
                  }
                  
                  logger.info(f"Message object: {json.dumps(message_object)}")
                  
                  
                  # Send message
                  response = client.send_whatsapp_message(
                      originationPhoneNumberId=phone_number_id,
                      metaApiVersion="v20.0",
                      message=bytes(json.dumps(message_object), "utf-8")
                  )
                  
                  logger.info(f"WhatsApp message sent successfully. Response: {json.dumps(response, default=str)}")
                  return response
                  
              except ClientError as e:
                  error_code = e.response.get('Error', {}).get('Code', 'Unknown')
                  error_message = e.response.get('Error', {}).get('Message', str(e))
                  logger.error(f"AWS Client Error ({error_code}): {error_message}")
                  
                  # Provide specific guidance for common errors
                  if error_code == 'AccessDenied':
                      logger.error("Check IAM permissions for socialmessaging:SendWhatsAppMessage")
                  elif error_code == 'InvalidParameter':
                      logger.error("Check WhatsApp phone number ID and recipient number format")
                  
                  raise
                  
              except Exception as e:
                  logger.error(f"Unexpected error sending WhatsApp message: {str(e)}", exc_info=True)
                  raise

  # SNS Subscription for Lambda
  AlarmNotificationSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: lambda
      TopicArn: !Ref AlarmNotificationTopic
      Endpoint: !GetAtt WhatsAppAlarmFunction.Arn

  # Lambda Permission for SNS
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref WhatsAppAlarmFunction
      Action: lambda:InvokeFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref AlarmNotificationTopic

  # Sample CloudWatch Alarm (High CPU)
  SampleHighCPUAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub '${AlarmPrefix}-Sample-High-CPU-${Environment}'
      AlarmDescription: 'Sample alarm for demonstration - monitors account-wide EC2 CPU usage'
      MetricName: CPUUtilization
      Namespace: Custom/EC2
      Statistic: Average
      Period: 60
      EvaluationPeriods: 1
      Threshold: 80
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref AlarmNotificationTopic
      OKActions:
        - !Ref AlarmNotificationTopic
      TreatMissingData: notBreaching

Outputs:
  SNSTopicArn:
    Description: 'ARN of the SNS topic for CloudWatch alarms'
    Value: !Ref AlarmNotificationTopic
    Export:
      Name: !Sub '${AWS::StackName}-SNSTopicArn'
  
  LambdaFunctionArn:
    Description: 'ARN of the WhatsApp notification Lambda function'
    Value: !GetAtt WhatsAppAlarmFunction.Arn
    Export:
      Name: !Sub '${AWS::StackName}-LambdaFunctionArn'
  
  LambdaFunctionName:
    Description: 'Name of the WhatsApp notification Lambda function'
    Value: !Ref WhatsAppAlarmFunction
    Export:
      Name: !Sub '${AWS::StackName}-LambdaFunctionName'
  
  LogGroupName:
    Description: 'CloudWatch Log Group name for Lambda function'
    Value: !Ref WhatsAppAlarmLogGroup
