#  --- Important: Use Non-Production Data Only ---
# This solution is designed for demonstration and testing purposes. Always use non-production, 
# synthetic, or publicly available data when testing:
# 
# - Do not upload sensitive or confidential data
# - Use sample data or create mock data for testing
# - Ensure compliance with your organization's data governance policies
# - Consider using anonymized or redacted data
# 
# For production deployments, implement additional security controls, data classification, 
# and access management appropriate for your sensitive data.

AWSTemplateFormatVersion: "2010-09-09"
Description: "AWS VPN Router with Transit Gateway - IPv6 Enabled Infrastructure"

Parameters:
  NotificationEmail:
    Type: String
    Description: "Email address to receive VPN tunnel state notifications"
    AllowedPattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
    ConstraintDescription: "Must be a valid email address"

# Constants as mappings
Mappings:
  NamingConventions:
    AwsVpc:
      Name: "aws-vpc"
    OnPremVpc:
      Name: "onprem-vpc"
    SshKey:
      Name: "instance-ssh-key"
    LambdaFunctions:
      GetIpv6Address: "get-ipv6-address-function"
      VpnPskValidator: "vpn-psk-validator-function"
      GetVpnTunnelDetails: "get-vpn-tunnel-details-function"
  VpcCidrs:
    Aws:
      Cidr: "10.0.0.0/16"
    OnPrem:
      Cidr: "192.168.0.0/16"
  Tags:
    Environment: 
      Value: "vpn-lab"
  InstanceTypes:
    VpnRouterInstanceType:
      Type: "m6g.medium"
    OtherInstanceType:
      Type: "t3.micro"
  ASNs:
    TransitGateway:
      Value: 64512
    CustomerGateway:
      Value: 65000
  VpnPskConfig:
    ExcludeCharacters:
      Value: "!@#$%^&*(){}[]|\"';:,/<>?`~+-=\\/ "
  LogRetention:
    CloudWatch:
      Days: 7
  LambdaConstants:
    VpnPskValidator:
      ReservedExecutions: 5
      Timeout: 30
    GetIpv6Address:
      ReservedExecutions: 5
      Timeout: 50
    GetVpnTunnelDetails:
      ReservedExecutions: 5
      Timeout: 300

Resources:
  # =============================================================================
  # SSH Key Pair (Created First - Referenced by Multiple Resources)
  # =============================================================================
  InstanceSshKeyPair:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: !Sub 
        - "${AWS::StackName}-${KeyName}"
        - KeyName: !FindInMap [NamingConventions, SshKey, Name]
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${KeyName}"
            - KeyName: !FindInMap [NamingConventions, SshKey, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # =============================================================================
  # AWS VPC (Cloud Side) - Main VPC Resources
  # =============================================================================
  AwsVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [VpcCidrs, Aws, Cidr]
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # --- AWS VPC IPv6 CIDR Block ---
  AwsVpcIpv6CidrBlock:
    Type: AWS::EC2::VPCCidrBlock
    Properties:
      AmazonProvidedIpv6CidrBlock: true
      VpcId: !Ref AwsVpc
  
  # --- AWS VPC Private Subnets (IPv6 Only) ---
  AwsVpcPrivateSubnetAz1:
    Type: AWS::EC2::Subnet
    DependsOn: 
      AwsVpcIpv6CidrBlock
    Properties:
      VpcId: !Ref AwsVpc
      Ipv6Native: true
      Ipv6CidrBlock:
        !Select [0, !Cidr [!Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [0, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-ipv6-subnet-1a"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcPrivateSubnetAz2:
    Type: AWS::EC2::Subnet
    DependsOn: 
      AwsVpcIpv6CidrBlock
    Properties:
      VpcId: !Ref AwsVpc
      Ipv6Native: true
      Ipv6CidrBlock:
        !Select [1, !Cidr [!Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [1, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-ipv6-subnet-1b"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # --- AWS VPC Transit Gateway Subnets (IPv4 + IPv6) ---
  AwsVpcTgwSubnetAz1:
    Type: AWS::EC2::Subnet
    DependsOn: 
      AwsVpcIpv6CidrBlock
    Properties:
      VpcId: !Ref AwsVpc
      CidrBlock: !Select [0, !Cidr [!GetAtt AwsVpc.CidrBlock, 4, 4]]
      Ipv6CidrBlock:
        !Select [2, !Cidr [!Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: true
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/tgw-dualstack-subnet-1a"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcTgwSubnetAz2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref AwsVpc
      CidrBlock: !Select [1, !Cidr [!GetAtt AwsVpc.CidrBlock, 4, 4]]
      Ipv6CidrBlock:
        !Select [3, !Cidr [!Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [1, !GetAZs ""]
      MapPublicIpOnLaunch: true
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/tgw-dualstack-subnet-1b"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  # --- AWS VPC Private Dual-Stack Subnets for Jump Instance ---
  AwsVpcPrivateDualStackSubnetAz1:
    Type: AWS::EC2::Subnet
    DependsOn: 
      AwsVpcIpv6CidrBlock
    Properties:
      VpcId: !Ref AwsVpc
      CidrBlock: !Select [2, !Cidr [!GetAtt AwsVpc.CidrBlock, 4, 8]]
      Ipv6CidrBlock:
        !Select [4, !Cidr [!Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [0, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-dualstack-subnet-1a"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcPrivateDualStackSubnetAz2:
    Type: AWS::EC2::Subnet
    DependsOn: 
      AwsVpcIpv6CidrBlock
    Properties:
      VpcId: !Ref AwsVpc
      CidrBlock: !Select [3, !Cidr [!GetAtt AwsVpc.CidrBlock, 4, 8]]
      Ipv6CidrBlock:
        !Select [5, !Cidr [!Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [1, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-dualstack-subnet-1b"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # --- AWS VPC Route Tables ---
  AwsVpcPrivateRouteTableAz1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref AwsVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-ipv6-rt-1a"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcPrivateRouteTableAz2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref AwsVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-ipv6-rt-1b"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcTgwRouteTableAz1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref AwsVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/tgw-dualstack-rt-1a"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcTgwRouteTableAz2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref AwsVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/tgw-dualstack-rt-1b"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  AwsVpcPrivateDualStackRouteTableAz1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref AwsVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-dualstack-rt-1a"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcPrivateDualStackRouteTableAz2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref AwsVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-dualstack-rt-1b"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # --- AWS VPC Subnet Route Table Associations ---
  AwsVpcPrivateSubnetRouteTableAssociationAz1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AwsVpcPrivateSubnetAz1
      RouteTableId: !Ref AwsVpcPrivateRouteTableAz1

  AwsVpcPrivateSubnetRouteTableAssociationAz2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AwsVpcPrivateSubnetAz2
      RouteTableId: !Ref AwsVpcPrivateRouteTableAz2

  AwsVpcTgwSubnetRouteTableAssociationAz1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AwsVpcTgwSubnetAz1
      RouteTableId: !Ref AwsVpcTgwRouteTableAz1

  AwsVpcTgwSubnetRouteTableAssociationAz2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AwsVpcTgwSubnetAz2
      RouteTableId: !Ref AwsVpcTgwRouteTableAz2

  AwsVpcPrivateDualStackSubnetRouteTableAssociationAz1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AwsVpcPrivateDualStackSubnetAz1
      RouteTableId: !Ref AwsVpcPrivateDualStackRouteTableAz1

  AwsVpcPrivateDualStackSubnetRouteTableAssociationAz2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref AwsVpcPrivateDualStackSubnetAz2
      RouteTableId: !Ref AwsVpcPrivateDualStackRouteTableAz2
  
  
  AwsVpcPrivateToTgwRouteIpv4Az1:
    Type: AWS::EC2::Route
    DependsOn:
      - TransitGatewayAwsVpcAttachment
    Properties:
      RouteTableId: !Ref AwsVpcPrivateRouteTableAz1
      DestinationCidrBlock: 0.0.0.0/0
      TransitGatewayId: !Ref TransitGateway

  AwsVpcPrivateToTgwRouteIpv6Az1:
    Type: AWS::EC2::Route
    DependsOn:
      - TransitGatewayAwsVpcAttachment
    Properties:
      RouteTableId: !Ref AwsVpcPrivateRouteTableAz1
      DestinationIpv6CidrBlock: ::/0
      TransitGatewayId: !Ref TransitGateway

  AwsVpcPrivateToTgwRouteIpv4Az2:
    Type: AWS::EC2::Route
    DependsOn:
      - TransitGatewayAwsVpcAttachment
    Properties:
      RouteTableId: !Ref AwsVpcPrivateRouteTableAz2
      DestinationCidrBlock: 0.0.0.0/0
      TransitGatewayId: !Ref TransitGateway

  AwsVpcPrivateToTgwRouteIpv6Az2:
    Type: AWS::EC2::Route
    DependsOn:
      - TransitGatewayAwsVpcAttachment
    Properties:
      RouteTableId: !Ref AwsVpcPrivateRouteTableAz2
      DestinationIpv6CidrBlock: ::/0
      TransitGatewayId: !Ref TransitGateway

  # Private Dual-Stack Subnets (Jump Instance) - No TGW routes needed
  # Jump instance uses SSM via VPC endpoints, then SSH to test instance

  # =============================================================================
  # AWS VPC Jump Instance and IAM Resources (Created Before Instance)
  # =============================================================================
  AwsVpcJumpInstanceRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub 
        - "${AWS::StackName}-${VpcName}-jump-instance-role-policy"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
      Description: "Managed policy for AWS VPC Jump Instance Role"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - ssm:GetParameter
            Resource: !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/ec2/keypair/${InstanceSshKeyPair.KeyPairId}"
          - Effect: Allow
            Action:
              - ec2:DescribeInstances
            Resource: "*"
            Condition:
              StringEquals:
                aws:RequestedRegion: !Ref "AWS::Region"

  AwsVpcJumpInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 
        - "${AWS::StackName}-${VpcName}-jump-instance-role"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - !Ref AwsVpcJumpInstanceRolePolicy
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/jump-instance-role"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  AwsVpcJumpInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub 
        - "${AWS::StackName}-${VpcName}-jump-instance-profile"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
      Roles:
        - !Ref AwsVpcJumpInstanceRole

  AwsVpcJumpInstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub 
        - "${AWS::StackName}-${VpcName}/jump-instance-sg"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
      GroupDescription: Security group for AWS VPC Jump instance - SSM access only
      VpcId: !Ref AwsVpc
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
          Description: "All IPv4 outbound traffic"
        - IpProtocol: -1
          CidrIpv6: ::/0
          Description: "All IPv6 outbound traffic"
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/jump-instance-sg"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  
  AwsVpcJumpInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-ebs}}"
      InstanceType: !FindInMap [InstanceTypes, OtherInstanceType, Type]
      EbsOptimized: true
      KeyName: !Ref InstanceSshKeyPair
      IamInstanceProfile: !Ref AwsVpcJumpInstanceProfile
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp3
            VolumeSize: 8
            Encrypted: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/jump-instance"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref AwsVpcPrivateDualStackSubnetAz1
          GroupSet:
            - !Ref AwsVpcJumpInstanceSecurityGroup
          AssociatePublicIpAddress: false # Using SSM Session Manager
      UserData:
        Fn::Base64:
          Fn::Sub: |
            #!/bin/bash
            # Redirect all output to both console and log file
            exec > >(tee /var/log/user-data.log)
            exec 2>&1
            
            echo "Starting AWS VPC Jump Instance user-data script at $(date)"
            
            yum update -y

            # Setup for ec2-user
            mkdir -p /home/ec2-user/.ssh
            chown ec2-user:ec2-user /home/ec2-user/.ssh
            chmod 700 /home/ec2-user/.ssh

            # Setup for ssm-user (SSM Session Manager default user)
            # Create ssm-user if it doesn't exist
            if ! id -u ssm-user >/dev/null 2>&1; then
              useradd -m -s /bin/bash ssm-user
              echo "Created ssm-user"
            fi
            
            # Ensure home directory exists and has correct permissions
            mkdir -p /home/ssm-user
            chown ssm-user:ssm-user /home/ssm-user
            chmod 755 /home/ssm-user
            
            # Setup SSH directory
            mkdir -p /home/ssm-user/.ssh
            chown ssm-user:ssm-user /home/ssm-user/.ssh
            chmod 700 /home/ssm-user/.ssh
          
            # Get private key from Parameter Store
            echo "Retrieving private key from Parameter Store..."
            aws ssm get-parameter \
              --name "/ec2/keypair/${InstanceSshKeyPair.KeyPairId}" \
              --with-decryption \
              --region ${AWS::Region} \
              --query 'Parameter.Value' \
              --output text > /tmp/vpn-lab-key.pem

            if [ $? -eq 0 ]; then
              echo "Successfully retrieved private key"
              
              # Set secure permissions on temp file first
              chmod 600 /tmp/vpn-lab-key.pem
              
              # Copy to ec2-user directory
              cp /tmp/vpn-lab-key.pem /home/ec2-user/.ssh/vpn-lab-key.pem
              chown ec2-user:ec2-user /home/ec2-user/.ssh/vpn-lab-key.pem
              chmod 600 /home/ec2-user/.ssh/vpn-lab-key.pem
              
              # Copy to ssm-user directory
              cp /tmp/vpn-lab-key.pem /home/ssm-user/.ssh/vpn-lab-key.pem
              chown ssm-user:ssm-user /home/ssm-user/.ssh/vpn-lab-key.pem
              chmod 600 /home/ssm-user/.ssh/vpn-lab-key.pem
              
              # Verify permissions
              echo "Verifying SSH key permissions:"
              ls -la /home/ssm-user/.ssh/vpn-lab-key.pem
              ls -la /home/ec2-user/.ssh/vpn-lab-key.pem
              
              # Clean up temp file
              rm /tmp/vpn-lab-key.pem
            else
              echo "Failed to retrieve private key"
            fi

            # Create connection script for ec2-user
            cat > /home/ec2-user/connect-to-aws-test-instance.sh << EOF
            #!/bin/bash
            echo "Connecting to AWS VPC EC2 instance at IPv6: ${GetAwsTestInstanceIpv6Address.Ipv6Address}"
            ssh -i ~/.ssh/vpn-lab-key.pem -o StrictHostKeyChecking=no ec2-user@${GetAwsTestInstanceIpv6Address.Ipv6Address}
            EOF

            chmod +x /home/ec2-user/connect-to-aws-test-instance.sh
            chown ec2-user:ec2-user /home/ec2-user/connect-to-aws-test-instance.sh

            # Create connection script for ssm-user
            cat > /home/ssm-user/connect-to-aws-test-instance.sh << 'EOF'
            #!/bin/bash
            echo "Connecting to AWS VPC EC2 instance at IPv6: ${GetAwsTestInstanceIpv6Address.Ipv6Address}"
            
            # Check if SSH key exists and has correct permissions
            if [ ! -f ~/.ssh/vpn-lab-key.pem ]; then
              echo "Error: SSH key not found at ~/.ssh/vpn-lab-key.pem"
              exit 1
            fi
            
            # Check key permissions
            key_perms=$(stat -c "%a" ~/.ssh/vpn-lab-key.pem)
            if [ "$key_perms" != "600" ]; then
              echo "Warning: SSH key has permissions $key_perms, should be 600"
              chmod 600 ~/.ssh/vpn-lab-key.pem
            fi
            
            ssh -i ~/.ssh/vpn-lab-key.pem -o StrictHostKeyChecking=no ec2-user@${GetAwsTestInstanceIpv6Address.Ipv6Address}
            EOF

            # Set proper ownership and permissions for the script
            chown ssm-user:ssm-user /home/ssm-user/connect-to-aws-test-instance.sh
            chmod +x /home/ssm-user/connect-to-aws-test-instance.sh
            
            # Also add ssm-user to sudoers for convenience (optional)
            echo "ssm-user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/ssm-user
            
            # Final verification
            echo "Final verification of ssm-user setup:"
            ls -la /home/ssm-user/
            ls -la /home/ssm-user/.ssh/
            
            echo "AWS VPC Jump Instance user-data script completed at $(date)"
  # =============================================================================
  # AWS VPC SSM Endpoints (For Private Subnet SSM Access)
  # =============================================================================
  AwsVpcSsmEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AwsVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref AwsVpcPrivateDualStackSubnetAz1
        - !Ref AwsVpcPrivateDualStackSubnetAz2
      SecurityGroupIds:
        - !Ref AwsVpcEndpointSecurityGroup
      PrivateDnsEnabled: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/ssm-endpoint"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcSsmMessagesEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AwsVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref AwsVpcPrivateDualStackSubnetAz1
        - !Ref AwsVpcPrivateDualStackSubnetAz2
      SecurityGroupIds:
        - !Ref AwsVpcEndpointSecurityGroup
      PrivateDnsEnabled: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/ssmmessages-endpoint"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcEc2MessagesEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AwsVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref AwsVpcPrivateDualStackSubnetAz1
        - !Ref AwsVpcPrivateDualStackSubnetAz2
      SecurityGroupIds:
        - !Ref AwsVpcEndpointSecurityGroup
      PrivateDnsEnabled: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/ec2messages-endpoint"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # Additional VPC Endpoints for Lambda Functions
  AwsVpcEc2Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AwsVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref AwsVpcPrivateDualStackSubnetAz1
        - !Ref AwsVpcPrivateDualStackSubnetAz2
      SecurityGroupIds:
        - !Ref AwsVpcEndpointSecurityGroup
      PrivateDnsEnabled: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/ec2-endpoint"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcSecretsManagerEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AwsVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.secretsmanager"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref AwsVpcPrivateDualStackSubnetAz1
        - !Ref AwsVpcPrivateDualStackSubnetAz2
      SecurityGroupIds:
        - !Ref AwsVpcEndpointSecurityGroup
      PrivateDnsEnabled: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/secretsmanager-endpoint"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcSqsEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AwsVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.sqs"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref AwsVpcPrivateDualStackSubnetAz1
        - !Ref AwsVpcPrivateDualStackSubnetAz2
      SecurityGroupIds:
        - !Ref AwsVpcEndpointSecurityGroup
      PrivateDnsEnabled: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/sqs-endpoint"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # S3 VPC Endpoint for CloudFormation custom resource responses
  AwsVpcS3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref AwsVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
      VpcEndpointType: Gateway
      RouteTableIds:
        - !Ref AwsVpcPrivateDualStackRouteTableAz1
        - !Ref AwsVpcPrivateDualStackRouteTableAz2
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/s3-endpoint"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub 
        - "${AWS::StackName}-${VpcName}/endpoint-sg"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
      GroupDescription: Security group for AWS VPC Endpoints
      VpcId: !Ref AwsVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !GetAtt AwsVpc.CidrBlock
          Description: "HTTPS from VPC"
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
          Description: "All outbound traffic"
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/endpoint-sg"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  # =============================================================================
  # AWS VPC EC2 Instance and IAM resources
  # =============================================================================
  AwsVpcTestEc2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub 
        - "${AWS::StackName}-${VpcName}/test-inst-sg"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
      GroupDescription: Security group for EC2 instance in AWS VPC private subnet
      VpcId: !Ref AwsVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          SourceSecurityGroupId: !Ref AwsVpcJumpInstanceSecurityGroup
          Description: "SSH access from AWS VPC Jump Instance"
        - IpProtocol: 58 # ICMPv6
          CidrIpv6: !Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks]
          Description: "ICMPv6 from On-Premises VPC (via VPN)"
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
          Description: "All IPv4 outbound traffic"
        - IpProtocol: -1
          CidrIpv6: ::/0
          Description: "All IPv6 outbound traffic"
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/test-inst-sg"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  AwsVpcTestEc2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-ebs}}"
      InstanceType: !FindInMap [InstanceTypes, OtherInstanceType, Type]
      EbsOptimized: true
      KeyName: !Ref InstanceSshKeyPair
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp3
            VolumeSize: 8
            Encrypted: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/test-instance"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref AwsVpcPrivateSubnetAz1
          GroupSet:
            - !Ref AwsVpcTestEc2SecurityGroup
      UserData:
        Fn::Base64: |
          #!/bin/bash
          yum update -y
  # =============================================================================
  # On-Premises VPC (Simulated) - Main VPC Resources
  # =============================================================================
  OnPremVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [VpcCidrs, OnPrem, Cidr]
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcIpv6CidrBlock:
    Type: AWS::EC2::VPCCidrBlock
    Properties:
      AmazonProvidedIpv6CidrBlock: true
      VpcId: !Ref OnPremVpc

  # --- On-Premises VPC Subnets ---
  OnPremVpcPrivateSubnetIpv6Az1:
    Type: AWS::EC2::Subnet
    DependsOn: 
      OnPremVpcIpv6CidrBlock
    Properties:
      EnableDns64: true
      VpcId: !Ref OnPremVpc
      Ipv6Native: true
      Ipv6CidrBlock: !Select [4, !Cidr [!Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [0, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-ipv6-subnet-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPrivateSubnetIpv6Az2:
    Type: AWS::EC2::Subnet
    DependsOn: 
      OnPremVpcIpv6CidrBlock
    Properties:
      EnableDns64: true
      VpcId: !Ref OnPremVpc
      Ipv6Native: true
      Ipv6CidrBlock:
        !Select [5, !Cidr [!Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [1, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-ipv6-subnet-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  OnPremVpcPublicSubnetIpv6Az1:
    Type: AWS::EC2::Subnet
    DependsOn: 
      OnPremVpcIpv6CidrBlock
    Properties:
      EnableDns64: true
      VpcId: !Ref OnPremVpc
      Ipv6Native: true
      Ipv6CidrBlock:
        !Select [0, !Cidr [!Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [0, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/public-ipv6-subnet-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPublicSubnetIpv6Az2:
    Type: AWS::EC2::Subnet
    DependsOn: 
      OnPremVpcIpv6CidrBlock
    Properties:
      EnableDns64: true
      VpcId: !Ref OnPremVpc
      Ipv6Native: true
      Ipv6CidrBlock: !Select [1,!Cidr [!Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [1, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/public-ipv6-subnet-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # --- On-Premises VPC Private Dual-Stack Subnets for Jump Instance ---
  OnPremVpcPrivateDualStackSubnetAz1:
    Type: AWS::EC2::Subnet
    DependsOn: 
      OnPremVpcIpv6CidrBlock
    Properties:
      EnableDns64: true
      VpcId: !Ref OnPremVpc
      CidrBlock: !Select [0, !Cidr [!GetAtt OnPremVpc.CidrBlock, 4, 8]]
      Ipv6CidrBlock: !Select [2, !Cidr [!Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [0, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-dualstack-subnet-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPrivateDualStackSubnetAz2:
    Type: AWS::EC2::Subnet
    DependsOn: 
      OnPremVpcIpv6CidrBlock
    Properties:
      EnableDns64: true
      VpcId: !Ref OnPremVpc
      CidrBlock: !Select [1, !Cidr [!GetAtt OnPremVpc.CidrBlock, 4, 8]]
      Ipv6CidrBlock: !Select [3, !Cidr [!Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks], 6, 64]]
      AvailabilityZone: !Select [1, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-dualstack-subnet-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  
  # --- On-Premises VPC Public Dual-Stack Subnets for NAT64 Gateways ---
  OnPremVpcPublicDualStackNatSubnetAz1:
    Type: AWS::EC2::Subnet
    DependsOn: 
      OnPremVpcIpv6CidrBlock
    Properties:
      EnableDns64: true
      VpcId: !Ref OnPremVpc
      CidrBlock: !Select [2, !Cidr [!GetAtt OnPremVpc.CidrBlock, 4, 8]]
      Ipv6CidrBlock: !Select [6, !Cidr [!Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks], 8, 64]]
      AvailabilityZone: !Select [0, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/public-dualstack-nat-subnet-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPublicDualStackNatSubnetAz2:
    Type: AWS::EC2::Subnet
    DependsOn: 
      OnPremVpcIpv6CidrBlock
    Properties:
      EnableDns64: true
      VpcId: !Ref OnPremVpc
      CidrBlock: !Select [3, !Cidr [!GetAtt OnPremVpc.CidrBlock, 4, 8]]
      Ipv6CidrBlock: !Select [7, !Cidr [!Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks], 8, 64]]
      AvailabilityZone: !Select [1, !GetAZs ""]
      AssignIpv6AddressOnCreation: true
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/public-dualstack-nat-subnet-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # --- Routes for IPv6-only public subnets (where VPN router lives) ---
  OnPremVpcPublicIpv6ToNat64RouteAz1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPublicIpv6RouteTableAz1
      DestinationIpv6CidrBlock: 64:ff9b::/96
      NatGatewayId: !Ref OnPremVpcNat64GatewayAz1

  OnPremVpcPublicIpv6ToNat64RouteAz2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPublicIpv6RouteTableAz2
      DestinationIpv6CidrBlock: 64:ff9b::/96
      NatGatewayId: !Ref OnPremVpcNat64GatewayAz2

  # --- Routes for IPv6-only private subnets (where test instances live) ---
  OnPremVpcPrivateIpv6ToNat64RouteAz1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPrivateIpv6RouteTableAz1
      DestinationIpv6CidrBlock: 64:ff9b::/96
      NatGatewayId: !Ref OnPremVpcNat64GatewayAz1

  OnPremVpcPrivateIpv6ToNat64RouteAz2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPrivateIpv6RouteTableAz2
      DestinationIpv6CidrBlock: 64:ff9b::/96
      NatGatewayId: !Ref OnPremVpcNat64GatewayAz2


  # =============================================================================
  # On-Premises VPC Internet Gateway and NAT Gateway Resources
  # =============================================================================
  OnPremVpcInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/igw"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcInternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref OnPremVpc
      InternetGatewayId: !Ref OnPremVpcInternetGateway

  # --- On-Premises VPC Route Tables ---
  OnPremVpcPublicIpv6RouteTableAz1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref OnPremVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/public-ipv6-rt-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPublicIpv6RouteTableAz2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref OnPremVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/public-ipv6-rt-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  OnPremVpcPrivateDualStackRouteTableAz1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref OnPremVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-dualstack-rt-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPrivateDualStackRouteTableAz2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref OnPremVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-dualstack-rt-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPublicDualStackNatRouteTableAz1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref OnPremVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/public-dualstack-nat-rt-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPublicDualStackNatRouteTableAz2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref OnPremVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/public-dualstack-nat-rt-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPrivateIpv6RouteTableAz1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref OnPremVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-ipv6-rt-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcPrivateIpv6RouteTableAz2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref OnPremVpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/private-ipv6-rt-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  # --- On-Premises VPC Routes ---
  OnPremVpcPublicIpv6RouteToIgwAz1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPublicIpv6RouteTableAz1
      DestinationIpv6CidrBlock: ::/0
      GatewayId: !Ref OnPremVpcInternetGateway

  OnPremVpcPublicIpv6RouteToIgwAz2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPublicIpv6RouteTableAz2
      DestinationIpv6CidrBlock: ::/0
      GatewayId: !Ref OnPremVpcInternetGateway

  # --- On-Premises VPC Public Dual-Stack Routes to IGW ---
  OnPremVpcPublicDualStackRouteToIgwIpv4Az1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPublicDualStackNatRouteTableAz1
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref OnPremVpcInternetGateway

  OnPremVpcPublicDualStackRouteToIgwIpv6Az1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPublicDualStackNatRouteTableAz1
      DestinationIpv6CidrBlock: ::/0
      GatewayId: !Ref OnPremVpcInternetGateway

  OnPremVpcPublicDualStackRouteToIgwIpv4Az2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPublicDualStackNatRouteTableAz2
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref OnPremVpcInternetGateway

  OnPremVpcPublicDualStackRouteToIgwIpv6Az2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPublicDualStackNatRouteTableAz2
      DestinationIpv6CidrBlock: ::/0
      GatewayId: !Ref OnPremVpcInternetGateway
  # --- On-Premises VPC Subnet Route Table Associations ---
  OnPremVpcPublicIpv6SubnetRouteTableAssociationAz1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref OnPremVpcPublicSubnetIpv6Az1
      RouteTableId: !Ref OnPremVpcPublicIpv6RouteTableAz1

  OnPremVpcPublicIpv6SubnetRouteTableAssociationAz2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref OnPremVpcPublicSubnetIpv6Az2
      RouteTableId: !Ref OnPremVpcPublicIpv6RouteTableAz2

  OnPremVpcPublicDualStackNatSubnetRouteTableAssociationAz1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref OnPremVpcPublicDualStackNatSubnetAz1
      RouteTableId: !Ref OnPremVpcPublicDualStackNatRouteTableAz1

  OnPremVpcPublicDualStackNatSubnetRouteTableAssociationAz2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref OnPremVpcPublicDualStackNatSubnetAz2
      RouteTableId: !Ref OnPremVpcPublicDualStackNatRouteTableAz2

  OnPremVpcPrivateIpv6SubnetRouteTableAssociationAz1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref OnPremVpcPrivateSubnetIpv6Az1
      RouteTableId: !Ref OnPremVpcPrivateIpv6RouteTableAz1

  OnPremVpcPrivateIpv6SubnetRouteTableAssociationAz2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref OnPremVpcPrivateSubnetIpv6Az2
      RouteTableId: !Ref OnPremVpcPrivateIpv6RouteTableAz2

  OnPremVpcPrivateDualStackSubnetRouteTableAssociationAz1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref OnPremVpcPrivateDualStackSubnetAz1
      RouteTableId: !Ref OnPremVpcPrivateDualStackRouteTableAz1

  OnPremVpcPrivateDualStackSubnetRouteTableAssociationAz2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref OnPremVpcPrivateDualStackSubnetAz2
      RouteTableId: !Ref OnPremVpcPrivateDualStackRouteTableAz2

  # =============================================================================
  # On-Premises VPC SSM Endpoints (For Private Subnet SSM Access)
  # =============================================================================
  OnPremVpcEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub 
        - "${AWS::StackName}-${VpcName}/endpoint-sg"
        - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
      GroupDescription: Security group for OnPrem VPC Endpoints
      VpcId: !Ref OnPremVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !GetAtt OnPremVpc.CidrBlock
          Description: "HTTPS from VPC"
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
          Description: "All outbound traffic"
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/endpoint-sg"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcSsmEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref OnPremVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref OnPremVpcPrivateDualStackSubnetAz1
        - !Ref OnPremVpcPrivateDualStackSubnetAz2
      SecurityGroupIds:
        - !Ref OnPremVpcEndpointSecurityGroup
      PrivateDnsEnabled: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/ssm-endpoint"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcSsmMessagesEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref OnPremVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref OnPremVpcPrivateDualStackSubnetAz1
        - !Ref OnPremVpcPrivateDualStackSubnetAz2
      SecurityGroupIds:
        - !Ref OnPremVpcEndpointSecurityGroup
      PrivateDnsEnabled: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/ssmmessages-endpoint"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcEc2MessagesEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref OnPremVpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref OnPremVpcPrivateDualStackSubnetAz1
        - !Ref OnPremVpcPrivateDualStackSubnetAz2
      SecurityGroupIds:
        - !Ref OnPremVpcEndpointSecurityGroup
      PrivateDnsEnabled: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/ec2messages-endpoint"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # =============================================================================
  #   # OnPrem VPC Test Instance (IPv6-only)
  # =============================================================================
  OnPremVpcTestEc2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub 
        - "${AWS::StackName}-${VpcName}/test-inst-sg"
        - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
      GroupDescription: Security group for OnPrem EC2 instance in private IPv6-only subnet
      VpcId: !Ref OnPremVpc
      SecurityGroupIngress:

        - IpProtocol: 58 # ICMPv6
          CidrIpv6: !Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks]
          Description: "ICMPv6 from AWS VPC IPv6 (via VPN)"
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
          Description: "All IPv4 outbound traffic"
        - IpProtocol: -1
          CidrIpv6: ::/0
          Description: "All IPv6 outbound traffic"
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/test-inst-sg"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcTestInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-ebs}}"
      InstanceType: !FindInMap [InstanceTypes, OtherInstanceType, Type]
      EbsOptimized: true
      KeyName: !Ref InstanceSshKeyPair
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp3
            VolumeSize: 8
            Encrypted: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/test-instance"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref OnPremVpcPrivateSubnetIpv6Az1
          GroupSet:
            - !Ref OnPremVpcTestEc2SecurityGroup
      UserData:
        Fn::Base64: |
          #!/bin/bash
          yum update -y
  # Note that this security group will have to be updated after deploying the S2S VPN, once tunnel addresses are known
  OnPremVpcVpnRouterSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub 
        - "${AWS::StackName}-${VpcName}/vpn-router-sg"
        - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
      GroupDescription: Security group for VPN router instance - allows SSH from Jump Instance and VPN traffic
      VpcId: !Ref OnPremVpc
      SecurityGroupIngress:

        - IpProtocol: -1
          SourceSecurityGroupId: !Ref OnPremVpcTestEc2SecurityGroup
          Description: "All traffic from OnPrem Test EC2 Instance"
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
          Description: "All outbound traffic (IPv4)"
        - IpProtocol: -1
          CidrIpv6: ::/0
          Description: "All outbound traffic (IPv6)"
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/vpn-router-sg"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # EC2 VPN router Log Group - Created explicitly to control retention
  VpnRouterLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/ec2/${AWS::StackName}/vpn-router"
      RetentionInDays: !FindInMap [LogRetention, CloudWatch, Days]
      KmsKeyId: !GetAtt EncryptionKey.Arn
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-vpn-router"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]


  # =============================================================================
  # NAT64 Gateways for IPv6-only subnets to reach IPv4 services
  # =============================================================================
  # --- Elastic IPs for NAT64 Gateways ---
  OnPremVpcNat64EipAz1:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/nat64-eip-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcNat64EipAz2:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/nat64-eip-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # --- NAT64 Gateways ---
  OnPremVpcNat64GatewayAz1:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt OnPremVpcNat64EipAz1.AllocationId
      SubnetId: !Ref OnPremVpcPublicDualStackNatSubnetAz1
      ConnectivityType: public
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/nat64-gateway-1a"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcNat64GatewayAz2:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt OnPremVpcNat64EipAz2.AllocationId
      SubnetId: !Ref OnPremVpcPublicDualStackNatSubnetAz2
      ConnectivityType: public
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/nat64-gateway-1b"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
  
  # =============================================================================
  # Lambda Dead Letter Queue and VPC Resources
  # =============================================================================
  LambdaDeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub "${AWS::StackName}-lambda-dlq"
      MessageRetentionPeriod: 1209600  # 14 days
      KmsMasterKeyId: !Ref EncryptionKey
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-lambda-dlq"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  LambdaSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${AWS::StackName}-lambda-sg"
      GroupDescription: Security group for Lambda functions
      VpcId: !Ref AwsVpc
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
          Description: "HTTPS outbound for AWS API calls"
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIpv6: ::/0
          Description: "HTTPS outbound for AWS API calls (IPv6)"
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-lambda-sg"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # =============================================================================
  # Lambda Functions for VPN Tunnel Details Retrieval
  # =============================================================================
  GetVpnTunnelDetailsFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub 
        - "/aws/lambda/${AWS::StackName}-${FunctionName}"
        - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, GetVpnTunnelDetails]
      RetentionInDays: !FindInMap [LogRetention, CloudWatch, Days]
      KmsKeyId: !GetAtt EncryptionKey.Arn
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${FunctionName}-logs"
            - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, GetVpnTunnelDetails]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  GetVpnTunnelDetailsFunctionRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${AWS::StackName}-get-vpn-details-function-role-policy"
      Description: "Managed policy for GetVpnTunnelDetailsFunction Role"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - ec2:DescribeVpnConnections
            Resource: "*"
            Condition:
              StringEquals:
                aws:RequestedRegion: !Ref "AWS::Region"
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-*:*"
          - Effect: Allow
            Action:
              - sqs:SendMessage
            Resource: !GetAtt LambdaDeadLetterQueue.Arn
          - Effect: Allow
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey
              - kms:DescribeKey
            Resource: !GetAtt EncryptionKey.Arn
            Condition:
              StringEquals:
                kms:ViaService: !Sub "sqs.${AWS::Region}.amazonaws.com"

  GetVpnTunnelDetailsFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-get-vpn-details-function-role"
      Description: "IAM role for Lambda functions to retrieve IPv6 addresses and VPN connection details"
      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
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
        - !Ref GetVpnTunnelDetailsFunctionRolePolicy
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}/get-vpn-details-function-role"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # =============================================================================
  # Lambda Functions for IPv6 Address Retrieval
  # =============================================================================
  GetIpv6AddressFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub 
        - "/aws/lambda/${AWS::StackName}-${FunctionName}"
        - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, GetIpv6Address]
      RetentionInDays: !FindInMap [LogRetention, CloudWatch, Days]
      KmsKeyId: !GetAtt EncryptionKey.Arn
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${FunctionName}-logs"
            - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, GetIpv6Address]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  GetIpv6AddressFunctionRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${AWS::StackName}-get-ipv6-address-function-role-policy"
      Description: "Managed policy for GetIpv6AddressFunction Role"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - ec2:DescribeInstances
            Resource: "*"
            Condition:
              StringEquals:
                aws:RequestedRegion: !Ref "AWS::Region"
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-*:*"
          - Effect: Allow
            Action:
              - sqs:SendMessage
            Resource: !GetAtt LambdaDeadLetterQueue.Arn
          - Effect: Allow
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey
              - kms:DescribeKey
            Resource: !GetAtt EncryptionKey.Arn
            Condition:
              StringEquals:
                kms:ViaService: !Sub "sqs.${AWS::Region}.amazonaws.com"

  GetIpv6AddressFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-get-ipv6-address-function-role"
      Description: "IAM role for Lambda functions to retrieve IPv6 addresses"
      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
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
        - !Ref GetIpv6AddressFunctionRolePolicy
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}/get-ipv6-address-function-role"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  GetIpv6AddressFunction:
    Type: AWS::Lambda::Function
    # Ensure Lambda function is deleted before its log group to prevent
    # log group recreation with "Never Expire" policy during stack deletion
    DependsOn: GetIpv6AddressFunctionLogGroup
    Properties:
      FunctionName: !Sub 
        - "${AWS::StackName}-${FunctionName}"
        - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, GetIpv6Address]
      Description: "Custom resource Lambda function to retrieve IPv6 addresses from EC2 instances for CloudFormation outputs"
      Runtime: python3.9
      Handler: index.handler
      Role: !GetAtt GetIpv6AddressFunctionRole.Arn
      Timeout: !FindInMap [LambdaConstants, GetIpv6Address, Timeout]
      ReservedConcurrentExecutions: !FindInMap [LambdaConstants, GetIpv6Address, ReservedExecutions]
      Architectures:
        - arm64
      DeadLetterConfig:
        TargetArn: !GetAtt LambdaDeadLetterQueue.Arn
      VpcConfig:
        SecurityGroupIds:
          - !Ref LambdaSecurityGroup
        SubnetIds:
          - !Ref AwsVpcPrivateDualStackSubnetAz1
          - !Ref AwsVpcPrivateDualStackSubnetAz2
      LoggingConfig:
        LogGroup: !Ref GetIpv6AddressFunctionLogGroup
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${FunctionName}"
            - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, GetIpv6Address]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import time
          import json
          import logging
          from botocore.exceptions import ClientError
          
          # Configure structured logging
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def handler(event, context):
              # Critical: Prevent Lambda from waiting for empty event loop
              context.callbackWaitsForEmptyEventLoop = False
              
              request_id = context.aws_request_id
              logger.info(f"Received {event}")
              logger.info(f"Processing {event['RequestType']} request {request_id} for {event.get('LogicalResourceId', 'Unknown')}")
              
              # Generate consistent PhysicalResourceId
              physical_id = f"GetIpv6Address-{event.get('LogicalResourceId', 'Unknown')}-{event.get('StackId', '').split('/')[-1] if event.get('StackId') else 'Unknown'}"
              
              try:
                  # Validate input parameters
                  if 'ResourceProperties' not in event:
                      raise ValueError("Missing ResourceProperties in event")
                  
                  resource_props = event['ResourceProperties']
                  
                  if event['RequestType'] == 'Create':
                      if 'InstanceId' not in resource_props:
                          raise ValueError("Missing InstanceId in ResourceProperties")
                      
                      instance_id = resource_props['InstanceId']
                      logger.info(f"Retrieving IPv6 address for instance: {instance_id}")
                      
                      ec2 = boto3.client('ec2')
                      
                      # Retry logic for instance readiness
                      max_retries = 10
                      retry_delay = 5
                      
                      for attempt in range(max_retries):
                          try:
                              response = ec2.describe_instances(InstanceIds=[instance_id])
                              
                              if not response['Reservations']:
                                  raise ValueError(f"Instance {instance_id} not found")
                              
                              instance = response['Reservations'][0]['Instances'][0]
                              instance_state = instance.get('State', {}).get('Name', 'unknown')
                              
                              logger.info(f"Instance {instance_id} state: {instance_state}")
                              
                              if instance_state not in ['running', 'pending']:
                                  raise ValueError(f"Instance {instance_id} is in {instance_state} state")
                              
                              network_interfaces = instance.get('NetworkInterfaces', [])
                              
                              if not network_interfaces:
                                  if attempt < max_retries - 1:
                                      logger.warning(f"No network interfaces found for {instance_id}, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})")
                                      time.sleep(retry_delay)
                                      continue
                                  else:
                                      raise ValueError(f"No network interfaces found for instance {instance_id}")
                              
                              ipv6_addresses = network_interfaces[0].get('Ipv6Addresses', [])
                              
                              if not ipv6_addresses:
                                  if attempt < max_retries - 1:
                                      logger.warning(f"No IPv6 addresses found for {instance_id}, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})")
                                      time.sleep(retry_delay)
                                      continue
                                  else:
                                      raise ValueError(f"No IPv6 addresses found for instance {instance_id}")
                              
                              ipv6_address = ipv6_addresses[0]['Ipv6Address']
                              logger.info(f"Found IPv6 address: {ipv6_address}")
                              
                              response_data = {'Ipv6Address': ipv6_address}
                              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, physical_id)
                              return
                              
                          except ClientError as e:
                              error_code = e.response['Error']['Code']
                              if error_code == 'InvalidInstanceID.NotFound':
                                  if attempt < max_retries - 1:
                                      logger.warning(f"Instance {instance_id} not found, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})")
                                      time.sleep(retry_delay)
                                      continue
                                  else:
                                      raise ValueError(f"Instance {instance_id} not found after {max_retries} attempts")
                              else:
                                  raise
                      
                      # If we get here, all retries failed
                      raise ValueError(f"Failed to retrieve IPv6 address after {max_retries} attempts")
                      
                  elif event['RequestType'] == 'Update':
                      # For updates, re-fetch the IPv6 address in case it changed
                      if 'InstanceId' not in resource_props:
                          raise ValueError("Missing InstanceId in ResourceProperties")
                      
                      instance_id = resource_props['InstanceId']
                      logger.info(f"Updating IPv6 address for instance: {instance_id}")
                      
                      ec2 = boto3.client('ec2')
                      response = ec2.describe_instances(InstanceIds=[instance_id])
                      
                      if not response['Reservations']:
                          raise ValueError(f"Instance {instance_id} not found")
                      
                      instance = response['Reservations'][0]['Instances'][0]
                      network_interfaces = instance.get('NetworkInterfaces', [])
                      
                      if not network_interfaces:
                          raise ValueError(f"No network interfaces found for instance {instance_id}")
                      
                      ipv6_addresses = network_interfaces[0].get('Ipv6Addresses', [])
                      
                      if not ipv6_addresses:
                          raise ValueError(f"No IPv6 addresses found for instance {instance_id}")
                      
                      ipv6_address = ipv6_addresses[0]['Ipv6Address']
                      logger.info(f"Updated IPv6 address: {ipv6_address}")
                      
                      response_data = {'Ipv6Address': ipv6_address}
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, physical_id)
                      
                  elif event['RequestType'] == 'Delete':
                      logger.info("Processing Delete request - no action required")
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)
                      
                  else:
                      raise ValueError(f"Unknown RequestType: {event['RequestType']}")
                      
              except ClientError as e:
                  error_msg = f"AWS API error: {e.response['Error']['Code']} - {e.response['Error']['Message']}"
                  logger.error(error_msg)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg}, physical_id)
                  
              except Exception as e:
                  error_msg = f"Unexpected error: {str(e)}"
                  logger.error(error_msg)
                  import traceback
                  logger.error(f"Traceback: {traceback.format_exc()}")
                  cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg}, physical_id)
  
  GetAwsTestInstanceIpv6Address:
    Type: Custom::GetIPv6
    DependsOn: 
      - AwsVpcS3Endpoint
      - AwsVpcEc2Endpoint
      - AwsVpcPrivateDualStackSubnetRouteTableAssociationAz1
      - AwsVpcPrivateDualStackSubnetRouteTableAssociationAz2
    Properties:
      ServiceToken: !GetAtt GetIpv6AddressFunction.Arn
      InstanceId: !Ref AwsVpcTestEc2Instance

  GetOnPremTestInstanceIpv6Address:
    Type: Custom::GetIPv6
    DependsOn: 
      - AwsVpcS3Endpoint
      - AwsVpcEc2Endpoint
      - AwsVpcPrivateDualStackSubnetRouteTableAssociationAz1
      - AwsVpcPrivateDualStackSubnetRouteTableAssociationAz2
    Properties:
      ServiceToken: !GetAtt GetIpv6AddressFunction.Arn
      InstanceId: !Ref OnPremVpcTestInstance

  GetOnPremVpnRouterInstanceIpv6Address:
    Type: Custom::GetIPv6
    DependsOn: 
      - AwsVpcS3Endpoint
      - AwsVpcEc2Endpoint
      - AwsVpcPrivateDualStackSubnetRouteTableAssociationAz1
      - AwsVpcPrivateDualStackSubnetRouteTableAssociationAz2
    Properties:
      ServiceToken: !GetAtt GetIpv6AddressFunction.Arn
      InstanceId: !Ref OnPremVpcVpnRouterInstance
      

  # =============================================================================
  # Transit Gateway and VPN Connection
  # =============================================================================
  TransitGateway:
    Type: AWS::EC2::TransitGateway
    Properties:
      AmazonSideAsn: !FindInMap [ASNs, TransitGateway, Value]
      AutoAcceptSharedAttachments: "enable"
      DefaultRouteTableAssociation: "enable"
      DefaultRouteTablePropagation: "enable"
      Description: !Sub "${AWS::StackName} - Transit Gateway for AWS-side VPC"
      DnsSupport: "enable"
      MulticastSupport: "disable"
      VpnEcmpSupport: "enable"
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}/transit-gateway"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  TransitGatewayAwsVpcAttachment:
    Type: AWS::EC2::TransitGatewayAttachment
    Metadata:
      cfn-lint:
        config:
          ignore_checks:
            - W3005 # DependsOn necessary to ensure attachment is provisioned after TGW is fully provisioned
    DependsOn:
      - TransitGateway
    Properties:
      Options:
        Ipv6Support: "enable"
      TransitGatewayId: !Ref TransitGateway
      VpcId: !Ref AwsVpc
      SubnetIds:
        - !Ref AwsVpcTgwSubnetAz1
        - !Ref AwsVpcTgwSubnetAz2
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}-att"
            - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  VpnPreSharedSecretTunnel1:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub "${AWS::StackName}-vpn-psk-tunnel-1"
      Description: !Sub "${AWS::StackName} - Tunnel 1 PSK"
      KmsKeyId: !Ref EncryptionKey
      GenerateSecretString:
        SecretStringTemplate: '{"psk":""}'
        GenerateStringKey: "psk"
        PasswordLength: 32
        ExcludePunctuation: false
        ExcludeCharacters: !FindInMap [VpnPskConfig, ExcludeCharacters, Value]
        ExcludeLowercase: false
        ExcludeUppercase: false
        ExcludeNumbers: false
        IncludeSpace: false
        RequireEachIncludedType: true
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-vpn-psk-tunnel-1"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  VpnPreSharedSecretTunnel2:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub "${AWS::StackName}-vpn-psk-tunnel-2"
      Description: !Sub "${AWS::StackName} - Tunnel 2 PSK"
      KmsKeyId: !Ref EncryptionKey
      GenerateSecretString:
        SecretStringTemplate: '{"psk":""}'
        GenerateStringKey: "psk"
        PasswordLength: 32
        ExcludePunctuation: false
        ExcludeCharacters: !FindInMap [VpnPskConfig, ExcludeCharacters, Value]
        ExcludeLowercase: false
        ExcludeUppercase: false
        ExcludeNumbers: false
        IncludeSpace: false
        RequireEachIncludedType: true
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-vpn-psk-tunnel-2"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  VpnConnectionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub 
        - "ipv6_s2s_vpn_logs_${UniqueId}"
        - UniqueId: !Select [2, !Split ["-", !Select [2, !Split ["/", !Ref "AWS::StackId"]]]]
      RetentionInDays: !FindInMap [LogRetention, CloudWatch, Days]
      KmsKeyId: !GetAtt EncryptionKey.Arn
      Tags:
        - Key: Name
          Value: !Sub 
            - "ipv6_s2s_vpn_logs_${UniqueId}"
            - UniqueId: !Select [2, !Split ["-", !Select [2, !Split ["/", !Ref "AWS::StackId"]]]]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # =============================================================================
  # Lambda Functions for VPN PSK Validation
  # =============================================================================
  VpnPskValidatorFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub 
        - "/aws/lambda/${AWS::StackName}-${FunctionName}"
        - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, VpnPskValidator]
      RetentionInDays: !FindInMap [LogRetention, CloudWatch, Days]
      KmsKeyId: !GetAtt EncryptionKey.Arn
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${FunctionName}-logs"
            - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, VpnPskValidator]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  VpnPskValidatorFunctionRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${AWS::StackName}-vpn-psk-validator-role-policy"
      Description: "Managed policy for VpnPskValidatorFunction Role"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - secretsmanager:GetSecretValue
              - secretsmanager:PutSecretValue
            Resource: 
              - !Ref VpnPreSharedSecretTunnel1
              - !Ref VpnPreSharedSecretTunnel2
          - Effect: Allow
            Action:
              - sqs:SendMessage
            Resource: !GetAtt LambdaDeadLetterQueue.Arn
          - Effect: Allow
            Action:
              - kms:Decrypt
              - kms:DescribeKey
              - kms:Encrypt
              - kms:GenerateDataKey
              - kms:ReEncrypt*
            Resource: !GetAtt EncryptionKey.Arn
            Condition:
              StringEquals:
                kms:ViaService: 
                  - !Sub "secretsmanager.${AWS::Region}.amazonaws.com"
                  - !Sub "sqs.${AWS::Region}.amazonaws.com"

  VpnPskValidatorFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-vpn-psk-validator-role"
      Description: "IAM role for VPN PSK validator Lambda function to read and write secrets"
      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
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
        - !Ref VpnPskValidatorFunctionRolePolicy
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}/vpn-psk-validator-role"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  VpnPskValidatorFunction:
    Type: AWS::Lambda::Function
    # Ensure Lambda function is deleted before its log group to prevent
    # log group recreation with "Never Expire" policy during stack deletion
    DependsOn: VpnPskValidatorFunctionLogGroup
    Properties:
      FunctionName: !Sub 
        - "${AWS::StackName}-${FunctionName}"
        - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, VpnPskValidator]
      Description: "Custom resource Lambda function to validate the generated PSK (Pre-shared key) to ensure it doesn't violate the required rules"
      Runtime: python3.9
      Handler: index.handler
      Role: !GetAtt VpnPskValidatorFunctionRole.Arn
      Timeout: !FindInMap [LambdaConstants, VpnPskValidator, Timeout]
      ReservedConcurrentExecutions: !FindInMap [LambdaConstants, VpnPskValidator, ReservedExecutions]
      Architectures:
        - arm64
      DeadLetterConfig:
        TargetArn: !GetAtt LambdaDeadLetterQueue.Arn
      VpcConfig:
        SecurityGroupIds:
          - !Ref LambdaSecurityGroup
        SubnetIds:
          - !Ref AwsVpcPrivateDualStackSubnetAz1
          - !Ref AwsVpcPrivateDualStackSubnetAz2
      LoggingConfig:
        LogGroup: !Ref VpnPskValidatorFunctionLogGroup
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import json
          import random
          import string
          import logging
          from botocore.exceptions import ClientError
          
          # Configure structured logging
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          
          def generate_psk(length=32):
              """Generate a PSK that complies with all validation rules"""
              # Define allowed characters (alphanumeric, periods, underscores)
              allowed_chars = string.ascii_letters + string.digits + '._'
              
              # Define allowed first characters (anything except '0')
              first_chars = string.ascii_letters + '123456789' + '._'
              
              # Generate first character
              psk = random.choice(first_chars)
              
              # Generate remaining characters
              for _ in range(length - 1):
                  psk += random.choice(allowed_chars)
                  
              return psk
          
          def validate_psk(psk):
              """Validate PSK against all rules and return violations"""
              valid_chars = set(string.ascii_letters + string.digits + '._')
              psk_length = len(psk)
              violations = []
              
              # Rule 1: PSK cannot start with '0'
              if psk.startswith('0'):
                  violations.append("PSK cannot start with '0'")
              
              # Rule 2: PSK must contain only valid characters
              invalid_chars = [c for c in psk if c not in valid_chars]
              if invalid_chars:
                  violations.append(f"PSK contains invalid characters: {list(set(invalid_chars))}")
              
              # Rule 3: PSK length must be between 8-64 characters
              if psk_length < 8:
                  violations.append(f"PSK length ({psk_length}) is too short (minimum 8 characters)")
              elif psk_length > 64:
                  violations.append(f"PSK length ({psk_length}) is too long (maximum 64 characters)")
              
              return violations
          
          def handler(event, context):
              # Critical: Prevent Lambda from waiting for empty event loop
              context.callbackWaitsForEmptyEventLoop = False
              
              request_id = context.aws_request_id
              logger.info(f"Received {event}")
              logger.info(f"Processing {event['RequestType']} request {request_id} for {event.get('LogicalResourceId', 'Unknown')}")
              
              # Generate consistent PhysicalResourceId
              physical_id = f"VpnPskValidator-{event.get('LogicalResourceId', 'Unknown')}-{event.get('StackId', '').split('/')[-1] if event.get('StackId') else 'Unknown'}"
              
              try:
                  # Validate input parameters
                  if 'ResourceProperties' not in event:
                      raise ValueError("Missing ResourceProperties in event")
                  
                  resource_props = event['ResourceProperties']
                  if 'SecretName' not in resource_props:
                      raise ValueError("Missing SecretName in ResourceProperties")
                  
                  secret_name = resource_props['SecretName']
                  
                  if event['RequestType'] == 'Create':
                      logger.info(f"Validating PSK for secret: {secret_name}")
                      
                      # Get the current PSK
                      secrets_client = boto3.client('secretsmanager')
                      try:
                          response = secrets_client.get_secret_value(SecretId=secret_name)
                          secret_data = json.loads(response['SecretString'])
                          current_psk = secret_data.get('psk', '')
                          
                          if not current_psk:
                              raise ValueError("PSK not found in secret")
                          
                          logger.info(f"Retrieved PSK from secret (length: {len(current_psk)})")
                          
                      except ClientError as e:
                          error_code = e.response['Error']['Code']
                          logger.error(f"Failed to retrieve secret {secret_name}: {error_code}")
                          raise
                      
                      # Validate PSK
                      violations = validate_psk(current_psk)
                      
                      if violations:
                          logger.warning("PSK validation failed:")
                          for violation in violations:
                              logger.warning(f"  - {violation}")
                          
                          logger.info("Regenerating PSK...")
                          
                          # Generate a new compliant PSK
                          new_psk = generate_psk(32)
                          
                          # Verify the new PSK is compliant
                          new_violations = validate_psk(new_psk)
                          if new_violations:
                              raise RuntimeError(f"Generated PSK still has violations: {new_violations}")
                          
                          # Update the secret with the new PSK
                          secret_data['psk'] = new_psk
                          secrets_client.put_secret_value(
                              SecretId=secret_name,
                              SecretString=json.dumps(secret_data)
                          )
                          logger.info("PSK regenerated successfully")
                          
                          response_data = {
                              'SecretArn': response['ARN'], 
                              'RegeneratePsk': 'true'
                          }
                      else:
                          logger.info("PSK validation passed - all rules satisfied")
                          response_data = {
                              'SecretArn': response['ARN'], 
                              'RegeneratePsk': 'false'
                          }
                      
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, physical_id)
                      
                  elif event['RequestType'] == 'Update':
                      logger.info(f"Update request for secret: {secret_name} - no action required")
                      response_data = {'SecretArn': secret_name, 'RegeneratePsk': 'false'}
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, physical_id)
                      
                  elif event['RequestType'] == 'Delete':
                      logger.info("Delete request - no action required")
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)
                      
                  else:
                      raise ValueError(f"Unknown RequestType: {event['RequestType']}")
                      
              except ClientError as e:
                  error_msg = f"AWS API error: {e.response['Error']['Code']} - {e.response['Error']['Message']}"
                  logger.error(error_msg)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg}, physical_id)
                  
              except Exception as e:
                  error_msg = f"Unexpected error: {str(e)}"
                  logger.error(error_msg)
                  import traceback
                  logger.error(f"Traceback: {traceback.format_exc()}")
                  cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg}, physical_id)


  # Custom resource to validate the PSK
  ValidateVpnPskTunnel1CustomResource:
    Type: Custom::ValidateVpnPsk
    DependsOn: 
      - AwsVpcS3Endpoint
      - AwsVpcSecretsManagerEndpoint
    Properties:
      ServiceToken: !GetAtt VpnPskValidatorFunction.Arn
      SecretName: !Ref VpnPreSharedSecretTunnel1

  ValidateVpnPskTunnel2CustomResource:
    Type: Custom::ValidateVpnPsk
    DependsOn: 
      - AwsVpcS3Endpoint
      - AwsVpcSecretsManagerEndpoint
    Properties:
      ServiceToken: !GetAtt VpnPskValidatorFunction.Arn
      SecretName: !Ref VpnPreSharedSecretTunnel2

  TransitGatewayVpnConnection:
    Type: AWS::EC2::VPNConnection
    DependsOn:
      - ValidateVpnPskTunnel1CustomResource
      - ValidateVpnPskTunnel2CustomResource
    Properties:
      OutsideIpAddressType: Ipv6
      LocalIpv6NetworkCidr: ::/0
      RemoteIpv6NetworkCidr: ::/0
      TunnelInsideIpVersion: ipv6
      Type: ipsec.1
      CustomerGatewayId: !Ref CustomerGateway
      TransitGatewayId: !Ref TransitGateway
      StaticRoutesOnly: false
      VpnTunnelOptionsSpecifications:
        - PreSharedKey: !Sub
            - "{{resolve:secretsmanager:${SecretName}:SecretString:psk}}"
            - SecretName: !Ref VpnPreSharedSecretTunnel1
          LogOptions:
            CloudwatchLogOptions:
              LogEnabled: true
              LogGroupArn: !GetAtt VpnConnectionLogGroup.Arn
              LogOutputFormat: "json"
        - PreSharedKey: !Sub
            - "{{resolve:secretsmanager:${SecretName}:SecretString:psk}}"
            - SecretName: !Ref VpnPreSharedSecretTunnel2
          LogOptions:
            CloudwatchLogOptions:
              LogEnabled: true
              LogGroupArn: !GetAtt VpnConnectionLogGroup.Arn
              LogOutputFormat: "json"
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-vpn-connection"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  CustomerGateway:
    Type: AWS::EC2::CustomerGateway
    Properties:
      Type: ipsec.1
      BgpAsn: !FindInMap [ASNs, CustomerGateway, Value]
      IpAddress: !GetAtt GetOnPremVpnRouterInstanceIpv6Address.Ipv6Address
      DeviceName: !Sub "${AWS::StackName}-Simulated-OnPrem-CGW"
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-customer-gateway"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  GetVpnTunnelDetailsFunction:
    Type: AWS::Lambda::Function
    # Ensure Lambda function is deleted before its log group to prevent
    # log group recreation with "Never Expire" policy during stack deletion
    DependsOn: GetVpnTunnelDetailsFunctionLogGroup
    Properties:
      FunctionName: !Sub 
        - "${AWS::StackName}-${FunctionName}"
        - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, GetVpnTunnelDetails]
      Description: "Custom resource Lambda function to get VPN tunnel details"
      Runtime: python3.9
      Handler: index.handler
      Role: !GetAtt GetVpnTunnelDetailsFunctionRole.Arn
      Timeout: !FindInMap [LambdaConstants, GetVpnTunnelDetails, Timeout]
      ReservedConcurrentExecutions: !FindInMap [LambdaConstants, GetVpnTunnelDetails, ReservedExecutions]
      Architectures:
        - arm64
      DeadLetterConfig:
        TargetArn: !GetAtt LambdaDeadLetterQueue.Arn
      VpcConfig:
        SecurityGroupIds:
          - !Ref LambdaSecurityGroup
        SubnetIds:
          - !Ref AwsVpcPrivateDualStackSubnetAz1
          - !Ref AwsVpcPrivateDualStackSubnetAz2
      LoggingConfig:
        LogGroup: !Ref GetVpnTunnelDetailsFunctionLogGroup
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${FunctionName}"
            - FunctionName: !FindInMap [NamingConventions, LambdaFunctions, GetVpnTunnelDetails]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import json
          import time
          import logging
          import ipaddress
          from botocore.exceptions import ClientError
          
          # Configure structured logging
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def calculate_tunnel_ips(tunnel_data):
              """Calculate AWS and Customer side IP addresses from tunnel prefixes"""
              try:
                  # Tunnel 1 IPv4 calculation
                  if tunnel_data['Tunnel1InsideIpv4Prefix']:
                      base_ip = ipaddress.IPv4Address(tunnel_data['Tunnel1InsideIpv4Prefix'].split('/')[0])
                      tunnel_data['Tunnel1InsideIpv4AwsSide'] = str(base_ip + 1)        # AWS = base + 1
                      tunnel_data['Tunnel1InsideIpv4CustomerSide'] = str(base_ip + 2)   # Customer = base + 2
                  
                  # Tunnel 1 IPv6 calculation
                  if tunnel_data['Tunnel1InsideIpv6Prefix']:
                      base_ip = ipaddress.IPv6Address(tunnel_data['Tunnel1InsideIpv6Prefix'].split('/')[0])
                      tunnel_data['Tunnel1InsideIpv6AwsSide'] = str(base_ip + 1)        # AWS = base + 1
                      tunnel_data['Tunnel1InsideIpv6CustomerSide'] = str(base_ip + 2)   # Customer = base + 2
                      
                  # Tunnel 2 IPv4 calculation
                  if tunnel_data['Tunnel2InsideIpv4Prefix']:
                      base_ip = ipaddress.IPv4Address(tunnel_data['Tunnel2InsideIpv4Prefix'].split('/')[0])
                      tunnel_data['Tunnel2InsideIpv4AwsSide'] = str(base_ip + 1)        # AWS = base + 1
                      tunnel_data['Tunnel2InsideIpv4CustomerSide'] = str(base_ip + 2)   # Customer = base + 2
                      
                  # Tunnel 2 IPv6 calculation
                  if tunnel_data['Tunnel2InsideIpv6Prefix']:
                      base_ip = ipaddress.IPv6Address(tunnel_data['Tunnel2InsideIpv6Prefix'].split('/')[0])
                      tunnel_data['Tunnel2InsideIpv6AwsSide'] = str(base_ip + 1)        # AWS = base + 1
                      tunnel_data['Tunnel2InsideIpv6CustomerSide'] = str(base_ip + 2)   # Customer = base + 2
                      
              except Exception as ip_error:
                  logger.warning(f"Error calculating tunnel IPs: {str(ip_error)}")
              
              return tunnel_data

          def handler(event, context):
              # Critical: Prevent Lambda from waiting for empty event loop
              context.callbackWaitsForEmptyEventLoop = False
              
              request_id = context.aws_request_id
              logger.info(f"Received {event}")
              logger.info(f"Processing {event['RequestType']} request {request_id} for {event.get('LogicalResourceId', 'Unknown')}")
              
              # Generate consistent PhysicalResourceId
              physical_id = f"GetVpnTunnelDetails-{event.get('LogicalResourceId', 'Unknown')}-{event.get('StackId', '').split('/')[-1] if event.get('StackId') else 'Unknown'}"
              
              try:
                  # Validate input parameters
                  if 'ResourceProperties' not in event:
                      raise ValueError("Missing ResourceProperties in event")
                  
                  resource_props = event['ResourceProperties']
                  
                  if event['RequestType'] == 'Create':
                      if 'VpnConnectionId' not in resource_props:
                          raise ValueError("Missing VpnConnectionId in ResourceProperties")
                      
                      vpn_connection_id = resource_props['VpnConnectionId']
                      logger.info(f"Retrieving VPN tunnel details for connection: {vpn_connection_id}")
                      
                      ec2 = boto3.client('ec2')
                      
                      # Retry logic for VPN connection readiness
                      max_retries = 20
                      retry_delay = 10
                      
                      for attempt in range(max_retries):
                          try:
                              response = ec2.describe_vpn_connections(VpnConnectionIds=[vpn_connection_id])
                              
                              if not response['VpnConnections']:
                                  raise ValueError(f"VPN connection {vpn_connection_id} not found")
                              
                              vpn_connection = response['VpnConnections'][0]
                              vpn_state = vpn_connection.get('State', 'unknown')
                              
                              logger.info(f"VPN Connection {vpn_connection_id} state: {vpn_state}")
                              
                              if vpn_state == 'deleted':
                                  raise ValueError(f"VPN connection {vpn_connection_id} is deleted")
                              
                              # Check if tunnel options are available
                              if ('Options' not in vpn_connection or 
                                  'TunnelOptions' not in vpn_connection['Options']):
                                  if attempt < max_retries - 1:
                                      logger.warning(f"TunnelOptions not available yet for {vpn_connection_id}, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})")
                                      time.sleep(retry_delay)
                                      continue
                                  else:
                                      raise ValueError(f"TunnelOptions not available for VPN connection {vpn_connection_id}")
                              
                              tunnel_options = vpn_connection['Options']['TunnelOptions']
                              
                              if len(tunnel_options) < 2:
                                  if attempt < max_retries - 1:
                                      logger.warning(f"Expected 2 tunnels, found {len(tunnel_options)}, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})")
                                      time.sleep(retry_delay)
                                      continue
                                  else:
                                      raise ValueError(f"Expected 2 VPN tunnels, found {len(tunnel_options)}")
                              
                              # Extract tunnel details
                              tunnel_data = {
                                  'Tunnel1OutsideIpv6': tunnel_options[0].get('OutsideIpAddress', ''),
                                  'Tunnel1InsideIpv4Prefix': tunnel_options[0].get('TunnelInsideCidr', ''),
                                  'Tunnel1InsideIpv6Prefix': tunnel_options[0].get('TunnelInsideIpv6Cidr', ''),
                                  'Tunnel1InsideIpv4CustomerSide': '',
                                  'Tunnel1InsideIpv4AwsSide': '',
                                  'Tunnel1InsideIpv6CustomerSide': '',
                                  'Tunnel1InsideIpv6AwsSide': '',
                                  'Tunnel1PreSharedKey': tunnel_options[0].get('PreSharedKey', ''),
                                  
                                  'Tunnel2OutsideIpv6': tunnel_options[1].get('OutsideIpAddress', ''),
                                  'Tunnel2InsideIpv4Prefix': tunnel_options[1].get('TunnelInsideCidr', ''),
                                  'Tunnel2InsideIpv6Prefix': tunnel_options[1].get('TunnelInsideIpv6Cidr', ''),
                                  'Tunnel2InsideIpv4CustomerSide': '',
                                  'Tunnel2InsideIpv4AwsSide': '',
                                  'Tunnel2InsideIpv6CustomerSide': '',
                                  'Tunnel2InsideIpv6AwsSide': '',
                                  'Tunnel2PreSharedKey': tunnel_options[1].get('PreSharedKey', '')
                              }
                              
                              # Calculate IP addresses
                              tunnel_data = calculate_tunnel_ips(tunnel_data)
                              
                              # Validate that we have essential data
                              if not tunnel_data['Tunnel1OutsideIpv6'] or not tunnel_data['Tunnel2OutsideIpv6']:
                                  if attempt < max_retries - 1:
                                      logger.warning(f"Missing outside IPv6 addresses, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})")
                                      time.sleep(retry_delay)
                                      continue
                                  else:
                                      raise ValueError("Missing outside IPv6 addresses for VPN tunnels")
                              
                              logger.info(f"Successfully extracted tunnel data for {vpn_connection_id}")
                              cfnresponse.send(event, context, cfnresponse.SUCCESS, tunnel_data, physical_id)
                              return
                              
                          except ClientError as e:
                              error_code = e.response['Error']['Code']
                              if error_code == 'InvalidVpnConnectionID.NotFound':
                                  if attempt < max_retries - 1:
                                      logger.warning(f"VPN connection {vpn_connection_id} not found, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})")
                                      time.sleep(retry_delay)
                                      continue
                                  else:
                                      raise ValueError(f"VPN connection {vpn_connection_id} not found after {max_retries} attempts")
                              else:
                                  raise
                      
                      # If we get here, all retries failed
                      raise ValueError(f"Failed to retrieve VPN tunnel details after {max_retries} attempts")
                      
                  elif event['RequestType'] == 'Update':
                      # For updates, re-fetch the tunnel details in case they changed
                      if 'VpnConnectionId' not in resource_props:
                          raise ValueError("Missing VpnConnectionId in ResourceProperties")
                      
                      vpn_connection_id = resource_props['VpnConnectionId']
                      logger.info(f"Updating VPN tunnel details for connection: {vpn_connection_id}")
                      
                      ec2 = boto3.client('ec2')
                      response = ec2.describe_vpn_connections(VpnConnectionIds=[vpn_connection_id])
                      
                      if not response['VpnConnections']:
                          raise ValueError(f"VPN connection {vpn_connection_id} not found")
                      
                      vpn_connection = response['VpnConnections'][0]
                      
                      if ('Options' not in vpn_connection or 
                          'TunnelOptions' not in vpn_connection['Options']):
                          raise ValueError(f"TunnelOptions not available for VPN connection {vpn_connection_id}")
                      
                      tunnel_options = vpn_connection['Options']['TunnelOptions']
                      
                      if len(tunnel_options) < 2:
                          raise ValueError(f"Expected 2 VPN tunnels, found {len(tunnel_options)}")
                      
                      # Extract tunnel details
                      tunnel_data = {
                          'Tunnel1OutsideIpv6': tunnel_options[0].get('OutsideIpAddress', ''),
                          'Tunnel1InsideIpv4Prefix': tunnel_options[0].get('TunnelInsideCidr', ''),
                          'Tunnel1InsideIpv6Prefix': tunnel_options[0].get('TunnelInsideIpv6Cidr', ''),
                          'Tunnel1InsideIpv4CustomerSide': '',
                          'Tunnel1InsideIpv4AwsSide': '',
                          'Tunnel1InsideIpv6CustomerSide': '',
                          'Tunnel1InsideIpv6AwsSide': '',
                          'Tunnel1PreSharedKey': tunnel_options[0].get('PreSharedKey', ''),
                          
                          'Tunnel2OutsideIpv6': tunnel_options[1].get('OutsideIpAddress', ''),
                          'Tunnel2InsideIpv4Prefix': tunnel_options[1].get('TunnelInsideCidr', ''),
                          'Tunnel2InsideIpv6Prefix': tunnel_options[1].get('TunnelInsideIpv6Cidr', ''),
                          'Tunnel2InsideIpv4CustomerSide': '',
                          'Tunnel2InsideIpv4AwsSide': '',
                          'Tunnel2InsideIpv6CustomerSide': '',
                          'Tunnel2InsideIpv6AwsSide': '',
                          'Tunnel2PreSharedKey': tunnel_options[1].get('PreSharedKey', '')
                      }
                      
                      # Calculate IP addresses
                      tunnel_data = calculate_tunnel_ips(tunnel_data)
                      
                      logger.info(f"Updated tunnel data for {vpn_connection_id}")
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, tunnel_data, physical_id)
                      
                  elif event['RequestType'] == 'Delete':
                      logger.info("Processing Delete request - no action required")
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)
                      
                  else:
                      raise ValueError(f"Unknown RequestType: {event['RequestType']}")
                      
              except ClientError as e:
                  error_msg = f"AWS API error: {e.response['Error']['Code']} - {e.response['Error']['Message']}"
                  logger.error(error_msg)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg}, physical_id)
                  
              except Exception as e:
                  error_msg = f"Unexpected error: {str(e)}"
                  logger.error(error_msg)
                  import traceback
                  logger.error(f"Traceback: {traceback.format_exc()}")
                  cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg}, physical_id)
  
  GetVpnTunnelDetails:
    Type: Custom::GetVpnTunnelDetails
    DependsOn: 
      - AwsVpcS3Endpoint
      - AwsVpcEc2Endpoint
    Properties:
      ServiceToken: !GetAtt GetVpnTunnelDetailsFunction.Arn
      VpnConnectionId: !Ref TransitGatewayVpnConnection

  # =============================================================================
  # KMS Key for Encryption
  # =============================================================================
  EncryptionKey:
    Type: AWS::KMS::Key
    Properties:
      Description: !Sub "${AWS::StackName} - KMS key for encrypting SNS topics, CloudWatch logs, and Secrets Manager"
      EnableKeyRotation: true
      KeyPolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action: 'kms:*'
            Resource: '*'
          - Sid: Allow CloudWatch Logs
            Effect: Allow
            Principal:
              Service: !Sub "logs.${AWS::Region}.amazonaws.com"
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: '*'
            Condition:
              ArnEquals:
                kms:EncryptionContext:aws:logs:arn: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*"
          - Sid: Allow SNS
            Effect: Allow
            Principal:
              Service: sns.amazonaws.com
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: '*'
          - Sid: Allow Secrets Manager
            Effect: Allow
            Principal:
              Service: secretsmanager.amazonaws.com
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: '*'
          - Sid: Allow CloudWatch Alarms
            Effect: Allow
            Principal:
              Service: cloudwatch.amazonaws.com
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: '*'
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-encryption-key"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  EncryptionKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${AWS::StackName}-encryption-key"
      TargetKeyId: !Ref EncryptionKey

  # =============================================================================
  # SNS Topic and CloudWatch Alarm for VPN Tunnel Monitoring
  # =============================================================================
  VpnTunnelStateNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub "${AWS::StackName}-ipv6-tunnel-state"
      DisplayName: !Sub "${AWS::StackName} IPv6 VPN Tunnel State Notifications"
      KmsMasterKeyId: !Ref EncryptionKey
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-ipv6-tunnel-state"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  VpnTunnelStateEmailSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref VpnTunnelStateNotificationTopic
      Protocol: email
      Endpoint: !Ref NotificationEmail

  VpnTunnelStateAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${AWS::StackName}-vpn-tunnel-down"
      AlarmDescription: "Alarm when VPN tunnel state is DOWN (0)"
      MetricName: TunnelState
      Namespace: AWS/VPN
      Statistic: Maximum
      Period: 60
      EvaluationPeriods: 2
      Threshold: 0.5
      ComparisonOperator: LessThanThreshold
      Dimensions:
        - Name: VpnId
          Value: !Ref TransitGatewayVpnConnection
      AlarmActions:
        - !Ref VpnTunnelStateNotificationTopic
      OKActions:
        - !Ref VpnTunnelStateNotificationTopic
      TreatMissingData: breaching
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-vpn-tunnel-down"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  # =============================================================================
  # VPN resources which are created in automated mode
  # =============================================================================
  
  OnPremVpcVpnRouterRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${AWS::StackName}-vpn-router-role-policy"
      Description: "Managed policy for OnPremVpcVpnRouter Role"
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - secretsmanager:GetSecretValue
            Resource: 
              - !Ref VpnPreSharedSecretTunnel1
              - !Ref VpnPreSharedSecretTunnel2
          - Effect: Allow
            Action:
              - cloudformation:DescribeStacks
            Resource: !Sub "arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*"
          - Effect: Allow
            Action:
              - ec2:DescribeVpnConnections
            Resource: "*"
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
              - logs:DescribeLogStreams
              - logs:DescribeLogGroups
            Resource: 
              - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/ec2/${AWS::StackName}/vpn-router"
              - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/ec2/${AWS::StackName}/vpn-router:*"
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:DescribeLogGroups
            Resource: 
              - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*"
          - Effect: Allow
            Action:
              - kms:Decrypt
              - kms:DescribeKey
            Resource: !GetAtt EncryptionKey.Arn
            Condition:
              StringEquals:
                kms:ViaService: !Sub "secretsmanager.${AWS::Region}.amazonaws.com"

  OnPremVpcVpnRouterRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-vpn-router-role"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - !Ref OnPremVpcVpnRouterRolePolicy
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-vpn-router-role"
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]

  OnPremVpcVpnRouterInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub "${AWS::StackName}-vpn-router-profile"
      Roles:
        - !Ref OnPremVpcVpnRouterRole

  OnPremVpcVpnRouterInstance:
    Type: AWS::EC2::Instance
    Metadata:
      AWS::CloudFormation::Init:
        configSets:
          default:
            - 01-install-strongswan
            - 02-config-ipsec-files
            - 03-update-configs
            - 04-start-services

        01-install-strongswan:
          commands:
            01_install_strongswan:
              command: "DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::Options::='--force-confold' strongswan strongswan-pki libcharon-extra-plugins"
            02_enable_strongswan_boot:
              command: "systemctl enable strongswan-starter"
            03_confirm_strongswan:
              command: "systemctl --no-pager status strongswan-starter"
            04_ipsec_status:
              command: "ipsec status"

        02-config-ipsec-files:
          files:
            "/tmp/configure-strongswan.sh":
              content: |

                #!/bin/bash
                echo "Installing apt install strongswan strongswan-pki libcharon-extra-plugins -y"
                apt install strongswan strongswan-pki libcharon-extra-plugins -y

                echo "sudo systemctl enable strongswan-starter"
                sudo systemctl enable strongswan-starter

                echo "sudo systemctl --no-pager status strongswan-starter"
                sudo systemctl --no-pager status strongswan-starter

                echo "ipsec status"
                ipsec status

                echo "Strongswan config file"
                cat > /etc/strongswan.conf << 'EOF'

                # strongswan.conf - strongSwan configuration file
                # Refer to the strongswan.conf(5) manpage for details
                #
                # Configuration changes should be made in the included files

                charon {
                    load_modular = yes
                    routing_table = 0
                    install_routes = yes
                    
                    # Enhanced logging configuration
                    filelog {
                        charon-log {
                            path = /var/log/strongswan-charon.log
                            time_format = %b %e %T
                            ike_name = yes
                            append = no
                            default = 2
                            flush_line = yes
                        }
                        stderr {
                            ike = 2
                            knl = 2
                            cfg = 2
                        }
                    }
                    
                    syslog {
                        identifier = charon
                        daemon {
                            default = 1
                            ike = 2
                            knl = 2
                            cfg = 2
                            net = 2
                            esp = 2
                            dmn = 2
                            mgr = 2
                        }
                    }
                    
                    plugins {
                        include strongswan.d/charon/*.conf
                    }
                }

                starter {
                    load_warning = yes
                }

                include strongswan.d/*.conf
                EOF

                chown root:root /etc/strongswan.conf
                chmod 644 /etc/strongswan.conf

                # Create strongSwan log directory
                mkdir -p /var/log/strongswan
                chown root:root /var/log/strongswan
                chmod 755 /var/log/strongswan

                # Configure rsyslog to capture strongSwan logs
                cat > /etc/rsyslog.d/10-strongswan.conf << 'EOF'
                # strongSwan logging
                :programname, isequal, "charon" /var/log/strongswan/strongswan.log
                :programname, isequal, "starter" /var/log/strongswan/strongswan.log
                :programname, isequal, "ipsec" /var/log/strongswan/strongswan.log
                & stop
                EOF

                # Restart rsyslog to apply configuration
                systemctl restart rsyslog

                echo "Strongswan configuration completed"
              mode: "000755"
              owner: "root"
              group: "root"

            "/tmp/configure-frr.sh":
              content: !Sub 
                - |
                  #!/bin/bash
                  set -e

                  echo "Installing and configuring FRR..."

                  # Install FRR
                  apt-get install -y frr frr-pythontools
                  systemctl enable frr
                  systemctl start frr

                  # Configure daemons
                  sed -i 's/zebra=no/zebra=yes/' /etc/frr/daemons
                  sed -i 's/bgpd=no/bgpd=yes/' /etc/frr/daemons

                  echo "Fetching tunnel details from CloudFormation stack..."
                  STACK_NAME=${AWS::StackName}

                  # Get tunnel inside IPv6 addresses
                  TUNNEL_1_INSIDE_IPV6_AWS_SIDE=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1InsideIpv6AwsSide`].OutputValue' \
                    --output text)

                  TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1InsideIpv6CustomerSide`].OutputValue' \
                    --output text)

                  TUNNEL_2_INSIDE_IPV6_AWS_SIDE=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2InsideIpv6AwsSide`].OutputValue' \
                    --output text)

                  TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2InsideIpv6CustomerSide`].OutputValue' \
                    --output text)

                  # Get tunnel inside IPv4 prefixes
                  TUNNEL_1_INSIDE_IPV4_PREFIX=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1InsideIpv4Prefix`].OutputValue' \
                    --output text)

                  TUNNEL_2_INSIDE_IPV4_PREFIX=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2InsideIpv4Prefix`].OutputValue' \
                    --output text)

                  # Get instance ID
                  EC2_INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $(curl -X PUT 'http://169.254.169.254/latest/api/token' -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600' -s)" -s http://169.254.169.254/latest/meta-data/instance-id)

                  # Get On-Premises VPC IPv6 PREFIX from CloudFormation stack
                  ON_PREMISES_VPC_IPV6_PREFIX=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`OnPremVpcIpv6Prefix`].OutputValue' \
                    --output text)

                  # Set BGP ASN values (standard values for AWS VPN)
                  CUSTOMER_ASN="${CustomerAsn}"
                  AWS_ASN="${AwsAsn}"

                  # Generate router ID from instance ID (simplified approach)
                  ROUTER_ID=$(echo $EC2_INSTANCE_ID | tr -d 'i-' | head -c 8 | fold -w2 | head -4 | paste -sd. | awk -F. '{printf "%d.%d.%d.%d", "0x"$1, "0x"$2, "0x"$3, "0x"$4}')

                  # Get IPv6 router address (gateway) - use a default if not found
                  ON_PREMISES_VPC_IPV6_ROUTER_ADDR=$(ip -6 route show default | awk '/via/ {print $3}' | head -1)
                  if [ -z "$ON_PREMISES_VPC_IPV6_ROUTER_ADDR" ]; then
                    ON_PREMISES_VPC_IPV6_ROUTER_ADDR="fe80::1"
                  fi

                  echo "Configuration values:"
                  echo "EC2 Instance ID: $EC2_INSTANCE_ID"
                  echo "Router ID: $ROUTER_ID"
                  echo "On-Premises VPC IPv6 Prefix: $ON_PREMISES_VPC_IPV6_PREFIX"
                  echo "Tunnel 1 Inside IPv6 AWS Side: $TUNNEL_1_INSIDE_IPV6_AWS_SIDE"
                  echo "Tunnel 1 Inside IPv6 Customer Side: $TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE"
                  echo "Tunnel 2 Inside IPv6 AWS Side: $TUNNEL_2_INSIDE_IPV6_AWS_SIDE"
                  echo "Tunnel 2 Inside IPv6 Customer Side: $TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE"

                  echo "Creating FRR configuration file with actual values..."

                  cat > /etc/frr/frr.conf << EOF
                  frr version 8.4.4
                  frr defaults traditional
                  hostname $EC2_INSTANCE_ID
                  log file /var/log/frr/zebra.log
                  log syslog informational
                  service integrated-vtysh-config
                  !
                  ipv6 forwarding
                  !
                  ipv6 route $ON_PREMISES_VPC_IPV6_PREFIX $ON_PREMISES_VPC_IPV6_ROUTER_ADDR ens5 onlink
                  !
                  router bgp $CUSTOMER_ASN
                    bgp router-id $ROUTER_ID
                      no bgp default ipv4-unicast
                      bgp bestpath as-path multipath-relax
                      bgp log-neighbor-changes
                      neighbor $TUNNEL_1_INSIDE_IPV6_AWS_SIDE remote-as $AWS_ASN
                      neighbor $TUNNEL_1_INSIDE_IPV6_AWS_SIDE update-source $TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE
                      neighbor $TUNNEL_1_INSIDE_IPV6_AWS_SIDE timers 10 30
                      neighbor $TUNNEL_2_INSIDE_IPV6_AWS_SIDE remote-as $AWS_ASN
                      neighbor $TUNNEL_2_INSIDE_IPV6_AWS_SIDE update-source $TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE
                      neighbor $TUNNEL_2_INSIDE_IPV6_AWS_SIDE timers 10 30
                    !
                    address-family ipv4 unicast
                      network $TUNNEL_1_INSIDE_IPV4_PREFIX
                      network $TUNNEL_2_INSIDE_IPV4_PREFIX
                    exit-address-family
                    !
                    address-family ipv6 unicast
                      network $ON_PREMISES_VPC_IPV6_PREFIX
                      neighbor $TUNNEL_1_INSIDE_IPV6_AWS_SIDE activate
                      neighbor $TUNNEL_1_INSIDE_IPV6_AWS_SIDE next-hop-self
                      neighbor $TUNNEL_1_INSIDE_IPV6_AWS_SIDE soft-reconfiguration inbound
                      neighbor $TUNNEL_1_INSIDE_IPV6_AWS_SIDE route-map ALLOW_ALL in
                      neighbor $TUNNEL_1_INSIDE_IPV6_AWS_SIDE route-map ALLOW_ALL out
                      neighbor $TUNNEL_2_INSIDE_IPV6_AWS_SIDE activate
                      neighbor $TUNNEL_2_INSIDE_IPV6_AWS_SIDE next-hop-self
                      neighbor $TUNNEL_2_INSIDE_IPV6_AWS_SIDE soft-reconfiguration inbound
                      neighbor $TUNNEL_2_INSIDE_IPV6_AWS_SIDE route-map ALLOW_ALL in
                      neighbor $TUNNEL_2_INSIDE_IPV6_AWS_SIDE route-map ALLOW_ALL out
                    exit-address-family
                  exit
                  !
                  route-map ALLOW_ALL permit 10
                  exit
                  !
                  EOF

                  echo "FRR configuration done"
                  chown frr:frr /etc/frr/frr.conf
                  chmod 640 /etc/frr/frr.conf

                  echo "Restart FRR to apply changes"
                  systemctl restart frr
                  systemctl status frr.service

                  echo "FRR installation and configuration completed"
                - CustomerAsn: !FindInMap [ASNs, CustomerGateway, Value]
                  AwsAsn: !FindInMap [ASNs, TransitGateway, Value]
              mode: "000755"
              owner: "root"
              group: "root"

            "/etc/ipsec-vti.sh":
              content: |
                #!/bin/bash
                echo "Script executed at $(date) with PLUTO_VERB=$PLUTO_VERB PLUTO_CONNECTION=$PLUTO_CONNECTION" >> /var/log/ipsec-vti.log
                echo "PLUTO_ME=$PLUTO_ME PLUTO_PEER=$PLUTO_PEER" >> /var/log/ipsec-vti.log
                echo "PLUTO_MARK_IN=$PLUTO_MARK_IN PLUTO_MARK_OUT=$PLUTO_MARK_OUT" >> /var/log/ipsec-vti.log
                IP=$(which ip)
                IPTABLES=$(which iptables)
                PLUTO_MARK_OUT_ARR=(${PLUTO_MARK_OUT//// })
                PLUTO_MARK_IN_ARR=(${PLUTO_MARK_IN//// })
                case "$PLUTO_CONNECTION" in
                    aws-tunnel1)
                        VTI_INTERFACE=vti1
                        VTI_LOCALADDR=TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE_IP/126
                        VTI_REMOTEADDR=TUNNEL_1_INSIDE_IPV6_AWS_SIDE_IP/126
                        ;;
                    aws-tunnel2)
                        VTI_INTERFACE=vti2
                        VTI_LOCALADDR=TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE_IP/126
                        VTI_REMOTEADDR=TUNNEL_2_INSIDE_IPV6_AWS_SIDE_IP/126
                        ;;
                    *)
                        echo "Unknown connection: $PLUTO_CONNECTION" >> /var/log/ipsec-vti.log
                        exit 1
                        ;;
                esac
                echo "Processing $PLUTO_CONNECTION with interface $VTI_INTERFACE" >> /var/log/ipsec-vti.log
                case "${PLUTO_VERB}" in
                    up-client|up-client-v6)
                        echo "Bringing up $VTI_INTERFACE" >> /var/log/ipsec-vti.log
                        # Check if interface already exists and remove it
                        if $IP link show $VTI_INTERFACE > /dev/null 2>&1; then
                            echo "Interface $VTI_INTERFACE exists, removing it first" >> /var/log/ipsec-vti.log
                            $IP link del $VTI_INTERFACE
                        fi
                        # Create VTI interface for IPv6
                        echo "Creating IPv6 VTI interface: $IP link add $VTI_INTERFACE type vti6 local $PLUTO_ME remote $PLUTO_PEER okey ${PLUTO_MARK_OUT_ARR[0]} ikey ${PLUTO_MARK_IN_ARR[0]}" >> /var/log/ipsec-vti.log
                        $IP link add $VTI_INTERFACE type vti6 local $PLUTO_ME remote $PLUTO_PEER okey ${PLUTO_MARK_OUT_ARR[0]} ikey ${PLUTO_MARK_IN_ARR[0]} 2>&1 | tee -a /var/log/ipsec-vti.log
                        if [ $? -ne 0 ]; then
                            echo "Failed to create VTI6 interface $VTI_INTERFACE" >> /var/log/ipsec-vti.log
                            exit 1
                        fi
                        # Configure interface policies (IPv6 only for vti6)
                        sysctl -w net.ipv6.conf.$VTI_INTERFACE.disable_policy=1 2>&1 | tee -a /var/log/ipsec-vti.log
                        sysctl -w net.ipv6.conf.all.forwarding=1 2>&1 | tee -a /var/log/ipsec-vti.log
                        # Add IPv6 address to interface
                        echo "Adding IPv6 address: $IP addr add $VTI_LOCALADDR remote $VTI_REMOTEADDR dev $VTI_INTERFACE" >> /var/log/ipsec-vti.log
                        $IP addr add $VTI_LOCALADDR remote $VTI_REMOTEADDR dev $VTI_INTERFACE 2>&1 | tee -a /var/log/ipsec-vti.log
                        if [ $? -ne 0 ]; then
                            echo "Failed to add IPv6 address to $VTI_INTERFACE" >> /var/log/ipsec-vti.log
                        fi
                        # Bring interface up
                        echo "Bringing up interface: $IP link set $VTI_INTERFACE up mtu 1436" >> /var/log/ipsec-vti.log
                        $IP link set $VTI_INTERFACE up mtu 1436 2>&1 | tee -a /var/log/ipsec-vti.log
                        if [ $? -ne 0 ]; then
                            echo "Failed to bring up $VTI_INTERFACE" >> /var/log/ipsec-vti.log
                        fi
                        

                        # Verify interface was created
                        if $IP link show $VTI_INTERFACE > /dev/null 2>&1; then
                            echo "SUCCESS: $VTI_INTERFACE interface created and is up" >> /var/log/ipsec-vti.log
                            $IP addr show $VTI_INTERFACE >> /var/log/ipsec-vti.log
                        else
                            echo "ERROR: $VTI_INTERFACE interface was not created" >> /var/log/ipsec-vti.log
                        fi
                        # Add iptables rules
                        $IPTABLES -t mangle -I FORWARD -o $VTI_INTERFACE -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
                        $IPTABLES -t mangle -I INPUT -p esp -s $PLUTO_PEER -d $PLUTO_ME -j MARK --set-xmark $PLUTO_MARK_IN
                        echo "Successfully configured $VTI_INTERFACE" >> /var/log/ipsec-vti.log
                        ;;
                    down-client|down-client-v6)
                        echo "Bringing down $VTI_INTERFACE" >> /var/log/ipsec-vti.log

                        # Remove VTI interface
                        $IP link del $VTI_INTERFACE
                        # Remove iptables rules
                        $IPTABLES -t mangle -D FORWARD -o $VTI_INTERFACE -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 2>/dev/null
                        $IPTABLES -t mangle -D INPUT -p esp -s $PLUTO_PEER -d $PLUTO_ME -j MARK --set-xmark $PLUTO_MARK_IN 2>/dev/null
                        echo "Successfully removed $VTI_INTERFACE" >> /var/log/ipsec-vti.log
                        ;;
                  esac
                  # Enable IPv4 and IPv6 forwarding
                  sysctl -w net.ipv4.ip_forward=1
                  sysctl -w net.ipv6.conf.all.forwarding=1
                  sysctl -w net.ipv4.conf.eth0.disable_xfrm=1
                  sysctl -w net.ipv4.conf.eth0.disable_policy=1

              mode: "000755"
              owner: "root"
              group: "root"

            "/etc/ipsec.conf":
              content: |
                config setup
                    charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2"
                    uniqueids=yes
                conn %default
                    keyexchange=ikev2
                    ike=aes128-sha1-modp1024!
                    esp=aes128-sha1-modp1024!
                    dpdaction=restart
                    dpddelay=30
                    dpdtimeout=120
                    rekey=yes
                    ikelifetime=28800s
                    lifetime=3600s
                    rekeymargin=540s
                    rekeyfuzz=100%
                    margintime=540s
                    leftid=EC2_VPN_IPV6
                    leftauth=psk
                    rightauth=psk
                    fragmentation=yes
                    mobike=no
                    keyingtries=%forever
                    closeaction=restart
                # Tunnel 1 (mark 100)
                conn aws-tunnel1
                    leftupdown=/etc/ipsec-vti.sh
                    left=EC2_VPN_IPV6
                    right=TUNNEL_1_OUTSIDE_IPV6_ADDRESS
                    leftsubnet=0.0.0.0/0,::/0
                    rightsubnet=0.0.0.0/0,::/0
                    mark=100
                    type=tunnel
                    mtu=1436
                    auto=start
                # Tunnel 2 (mark 200)
                conn aws-tunnel2
                    leftupdown=/etc/ipsec-vti.sh
                    left=EC2_VPN_IPV6
                    right=TUNNEL_2_OUTSIDE_IPV6_ADDRESS
                    leftsubnet=0.0.0.0/0,::/0
                    rightsubnet=0.0.0.0/0,::/0
                    mark=200
                    type=tunnel
                    mtu=1436
                    auto=start
              mode: "000644"
              owner: "root"
              group: "root"

            "/etc/ipsec.secrets":
              content: |
                # This file will be overwritten by update-ipsec-secrets.sh
                # Placeholder content
              mode: "000600"
              owner: "root"
              group: "root"
            "/tmp/update-ipsec-conf.sh":
              content: !Sub |
                #!/bin/bash
                apt install awscli -y
                set -e

                echo "Starting ipsec.conf update script..."

                # Get CloudFormation stack outputs for tunnel addresses
                echo "Fetching tunnel addresses from CloudFormation stack..."
                STACK_NAME=${AWS::StackName}

                # Function to wait for VPN connection to be available
                wait_for_vpn_connection() {
                  local vpn_connection_id=""
                  local max_attempts=60
                  local wait_seconds=10
                  local attempt=1
                  
                  echo "Waiting for VPN connection to be available. Added CloudFormation."
                  
                  # First, get the VPN connection ID, this is neccessary to avoid a cyclical dependency
                  while ([ -z "$vpn_connection_id" ] || [ "$vpn_connection_id" = "None" ]) && [ $attempt -le $max_attempts ]; do
                    
                    echo "DEBUG: Current vpn_connection_id value: '$vpn_connection_id'"
                    echo "Attempt $attempt/$max_attempts: Getting VPN connection ID..."
                    
                    vpn_connection_id=$(aws cloudformation describe-stacks \
                      --stack-name $STACK_NAME \
                      --region ${AWS::Region} \
                      --query 'Stacks[0].Outputs[?OutputKey==`VpnConnectionId`].OutputValue' \
                      --output text)
                    
                    if [ -z "$vpn_connection_id" ] || [ "$vpn_connection_id" = "None" ]; then
                      echo "VPN connection ID not found, waiting $wait_seconds seconds..."
                      sleep $wait_seconds
                      attempt=$((attempt + 1))
                    else
                      echo "Found VPN connection ID: $vpn_connection_id"
                    fi
                  done
                  
                  if [ -z "$vpn_connection_id" ] || [ "$vpn_connection_id" = "None" ]; then
                    echo "Failed to get VPN connection ID after $max_attempts attempts, tried $attempt times"
                    return 1
                  fi
                  
                  # Now wait for the VPN connection to be available
                  attempt=1
                  local vpn_state=""
                  
                  while [ "$vpn_state" != "available" ] && [ $attempt -le $max_attempts ]; do
                    echo "Attempt $attempt/$max_attempts: Checking VPN connection state..."
                    vpn_state=$(aws ec2 describe-vpn-connections \
                      --vpn-connection-ids $vpn_connection_id \
                      --region ${AWS::Region} \
                      --query 'VpnConnections[0].State' \
                      --output text)
                    
                    if [ "$vpn_state" != "available" ]; then
                      echo "VPN connection state is $vpn_state, waiting $wait_seconds seconds..."
                      sleep $wait_seconds
                      attempt=$((attempt + 1))
                    else
                      echo "VPN connection is available"
                    fi
                  done
                  
                  if [ "$vpn_state" != "available" ]; then
                    echo "VPN connection did not become available after $max_attempts attempts"
                    return 1
                  fi
                  
                  return 0
                }

                # Wait for VPN connection to be available
                if ! wait_for_vpn_connection; then
                  echo "Failed to wait for VPN connection to be available"
                  exit 1
                fi

                # Now proceed with getting the tunnel details
                echo "VPN connection is available, getting tunnel details..."

                TUNNEL_1_OUTSIDE_IPV6=$(aws cloudformation describe-stacks \
                  --stack-name $STACK_NAME \
                  --region ${AWS::Region} \
                  --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1OutsideIpv6`].OutputValue' \
                  --output text)

                TUNNEL_2_OUTSIDE_IPV6=$(aws cloudformation describe-stacks \
                  --stack-name $STACK_NAME \
                  --region ${AWS::Region} \
                  --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2OutsideIpv6`].OutputValue' \
                  --output text)

                # Get this instance's IPv6 address using IMDSv2
                echo "Getting EC2 instance IPv6 address..."

                # Get IMDSv2 token
                TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" -s)

                if [ -z "$TOKEN" ]; then
                  echo "Failed to get IMDSv2 token, trying network interface method..."
                  EC2_VPN_IPV6=$(ip -6 addr show | grep 'inet6.*global' | head -1 | awk '{print $2}' | cut -d'/' -f1)
                else
                  # Try to get IPv6 from metadata service
                  EC2_VPN_IPV6=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/ipv6 2>/dev/null)
                  
                  if [ -z "$EC2_VPN_IPV6" ]; then
                    echo "IPv6 not available from metadata service, trying network interface..."
                    EC2_VPN_IPV6=$(ip -6 addr show | grep 'inet6.*global' | head -1 | awk '{print $2}' | cut -d'/' -f1)
                  fi
                fi

                if [ -z "$EC2_VPN_IPV6" ]; then
                  echo "No IPv6 address found, trying to get from CloudFormation outputs..."
                  EC2_VPN_IPV6=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==\`OnPremVpcVpnGatewayInstanceIpv6Address\`].OutputValue' \
                    --output text)
                fi

                # Validate we have all required values
                if [ -z "$TUNNEL_1_OUTSIDE_IPV6" ] || [ "$TUNNEL_1_OUTSIDE_IPV6" = "None" ]; then
                  echo "Failed to retrieve Tunnel 1 outside IPv6 address"
                  exit 1
                fi

                if [ -z "$TUNNEL_2_OUTSIDE_IPV6" ] || [ "$TUNNEL_2_OUTSIDE_IPV6" = "None" ]; then
                  echo "Failed to retrieve Tunnel 2 outside IPv6 address"
                  exit 1
                fi

                if [ -z "$EC2_VPN_IPV6" ]; then
                  echo "Failed to retrieve EC2 instance IPv6 addresss"
                  exit 1
                fi

                echo "Tunnel 1 Outside IPv6: $TUNNEL_1_OUTSIDE_IPV6"
                echo "Tunnel 2 Outside IPv6: $TUNNEL_2_OUTSIDE_IPV6"
                echo "EC2 Instance IPv6: $EC2_VPN_IPV6"

                # Update /etc/ipsec.conf with actual values
                echo "Updating /etc/ipsec.conf with actual tunnel addresses..."
                sed -i "s/EC2_VPN_IPV6/$EC2_VPN_IPV6/g" /etc/ipsec.conf
                sed -i "s/TUNNEL_1_OUTSIDE_IPV6_ADDRESS/$TUNNEL_1_OUTSIDE_IPV6/g" /etc/ipsec.conf
                sed -i "s/TUNNEL_2_OUTSIDE_IPV6_ADDRESS/$TUNNEL_2_OUTSIDE_IPV6/g" /etc/ipsec.conf

                echo "ipsec.conf updated successfully with actual tunnel addresses"
              mode: "000755"
              owner: "root"
              group: "root"

            "/tmp/update-frr-conf.sh":
              content: !Sub 
                - |
                  #!/bin/bash
                  set -e

                  echo "Starting frr.conf update script..."

                  # Get CloudFormation stack outputs for tunnel details
                  echo "Fetching tunnel details from CloudFormation stack..."
                  STACK_NAME=${AWS::StackName}

                  # Get tunnel inside IPv6 addresses
                  TUNNEL_1_INSIDE_IPV6_AWS_SIDE=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1InsideIpv6AwsSide`].OutputValue' \
                    --output text)

                  TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1InsideIpv6CustomerSide`].OutputValue' \
                    --output text)

                  TUNNEL_2_INSIDE_IPV6_AWS_SIDE=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2InsideIpv6AwsSide`].OutputValue' \
                    --output text)

                  TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2InsideIpv6CustomerSide`].OutputValue' \
                    --output text)

                  # Get tunnel inside IPv4 prefixes
                  TUNNEL_1_INSIDE_IPV4_PREFIX=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1InsideIpv4Prefix`].OutputValue' \
                    --output text)

                  TUNNEL_2_INSIDE_IPV4_PREFIX=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2InsideIpv4Prefix`].OutputValue' \
                    --output text)

                  # Get instance ID
                  EC2_INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $(curl -X PUT 'http://169.254.169.254/latest/api/token' -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600' -s)" -s http://169.254.169.254/latest/meta-data/instance-id)

                  # Get On-Premises VPC IPv6 CIDR from CloudFormation stack
                  ON_PREMISES_VPC_IPV6_PREFIX=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${AWS::Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`OnPremVpcIpv6Cidr`].OutputValue' \
                    --output text)

                  # Set BGP ASN values (standard values for AWS VPN)
                  CUSTOMER_ASN="${CustomerAsn}"
                  AWS_ASN="${AwsAsn}"

                  # Generate router ID from instance ID (simplified approach)
                  ROUTER_ID="1.1.1.1"  # Use a simple static router ID for BGP

                  # Get IPv6 router address (gateway)
                  ON_PREMISES_VPC_IPV6_ROUTER_ADDR=$(ip -6 route show default | awk '/via/ {print $3}' | head -1)

                  echo "Updating /etc/frr/frr.conf with actual values..."
                  echo "EC2 Instance ID: $EC2_INSTANCE_ID"
                  echo "On-Premises VPC IPv6 Prefix: $ON_PREMISES_VPC_IPV6_PREFIX"
                  echo "Router ID: $ROUTER_ID"
                  echo "Tunnel 1 Inside IPv6 AWS Side: $TUNNEL_1_INSIDE_IPV6_AWS_SIDE"
                  echo "Tunnel 1 Inside IPv6 Customer Side: $TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE"
                  echo "Tunnel 2 Inside IPv6 AWS Side: $TUNNEL_2_INSIDE_IPV6_AWS_SIDE"
                  echo "Tunnel 2 Inside IPv6 Customer Side: $TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE"

                  # Update frr.conf with actual values
                  sed -i "s/EC2_INSTANCE_ID/$EC2_INSTANCE_ID/g" /etc/frr/frr.conf
                  sed -i "s|ON_PREMISES_VPC_IPV6_PREFIX|$ON_PREMISES_VPC_IPV6_PREFIX|g" /etc/frr/frr.conf
                  sed -i "s/ON_PREMISES_VPC_IPV6_ROUTER_ADDR/$ON_PREMISES_VPC_IPV6_ROUTER_ADDR/g" /etc/frr/frr.conf
                  sed -i "s/CUSTOMER_ASN/$CUSTOMER_ASN/g" /etc/frr/frr.conf
                  sed -i "s/AWS_ASN/$AWS_ASN/g" /etc/frr/frr.conf
                  sed -i "s/ROUTER_ID/$ROUTER_ID/g" /etc/frr/frr.conf
                  # Directly set the router ID to ensure it's properly configured
                  sed -i 's/bgp router-id 0.0.0.0/bgp router-id 1.1.1.1/' /etc/frr/frr.conf
                  sed -i "s/TUNNEL_1_INSIDE_IPV6_AWS_SIDE_IP/$TUNNEL_1_INSIDE_IPV6_AWS_SIDE/g" /etc/frr/frr.conf
                  sed -i "s/TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE_IP/$TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE/g" /etc/frr/frr.conf
                  sed -i "s/TUNNEL_2_INSIDE_IPV6_AWS_SIDE_IP/$TUNNEL_2_INSIDE_IPV6_AWS_SIDE/g" /etc/frr/frr.conf
                  sed -i "s/TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE_IP/$TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE/g" /etc/frr/frr.conf
                  sed -i "s|TUNNEL_1_INSIDE_IPV4_ADDRESS_PREFIX|$TUNNEL_1_INSIDE_IPV4_PREFIX|g" /etc/frr/frr.conf
                  sed -i "s|TUNNEL_2_INSIDE_IPV4_ADDRESS_PREFIX|$TUNNEL_2_INSIDE_IPV4_PREFIX|g" /etc/frr/frr.conf
                  # Directly set the router ID to 1.1.1.1
                  sudo sed -i 's/bgp router-id 0.0.0.0/bgp router-id 1.1.1.1/' /etc/frr/frr.conf


                  echo "frr.conf updated successfully with actual values"

                  # Restart FRR to apply changes
                  echo "Restarting FRR to apply configuration changes..."
                  systemctl restart frr
                  systemctl status frr.service
                - CustomerAsn: !FindInMap [ASNs, CustomerGateway, Value]
                  AwsAsn: !FindInMap [ASNs, TransitGateway, Value]
              mode: "000755"
              owner: "root"
              group: "root"

            "/tmp/update-ipsec-vti.sh":
              content: !Sub |
                #!/bin/bash
                set -e

                echo "Starting ipsec-vti.sh update script..."

                # Get CloudFormation stack outputs for tunnel details
                echo "Fetching tunnel details from CloudFormation stack..."
                STACK_NAME=${AWS::StackName}

                # Get tunnel inside IPv6 addresses
                TUNNEL_1_INSIDE_IPV6_AWS_SIDE=$(aws cloudformation describe-stacks \
                  --stack-name $STACK_NAME \
                  --region ${AWS::Region} \
                  --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1InsideIpv6AwsSide`].OutputValue' \
                  --output text)

                TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE=$(aws cloudformation describe-stacks \
                  --stack-name $STACK_NAME \
                  --region ${AWS::Region} \
                  --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1InsideIpv6CustomerSide`].OutputValue' \
                  --output text)

                TUNNEL_2_INSIDE_IPV6_AWS_SIDE=$(aws cloudformation describe-stacks \
                  --stack-name $STACK_NAME \
                  --region ${AWS::Region} \
                  --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2InsideIpv6AwsSide`].OutputValue' \
                  --output text)

                TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE=$(aws cloudformation describe-stacks \
                  --stack-name $STACK_NAME \
                  --region ${AWS::Region} \
                  --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2InsideIpv6CustomerSide`].OutputValue' \
                  --output text)

                # Validate we have all required values
                if [ -z "$TUNNEL_1_INSIDE_IPV6_AWS_SIDE" ] || [ "$TUNNEL_1_INSIDE_IPV6_AWS_SIDE" = "None" ]; then
                  echo "Failed to retrieve Tunnel 1 inside IPv6 AWS side address"
                  exit 1
                fi

                if [ -z "$TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE" ] || [ "$TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE" = "None" ]; then
                  echo "Failed to retrieve Tunnel 1 inside IPv6 customer side address"
                  exit 1
                fi

                if [ -z "$TUNNEL_2_INSIDE_IPV6_AWS_SIDE" ] || [ "$TUNNEL_2_INSIDE_IPV6_AWS_SIDE" = "None" ]; then
                  echo "Failed to retrieve Tunnel 2 inside IPv6 AWS side address"
                  exit 1
                fi

                if [ -z "$TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE" ] || [ "$TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE" = "None" ]; then
                  echo "Failed to retrieve Tunnel 2 inside IPv6 customer side address"
                  exit 1
                fi

                echo "Tunnel 1 Inside IPv6 AWS Side: $TUNNEL_1_INSIDE_IPV6_AWS_SIDE"
                echo "Tunnel 1 Inside IPv6 Customer Side: $TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE"
                echo "Tunnel 2 Inside IPv6 AWS Side: $TUNNEL_2_INSIDE_IPV6_AWS_SIDE"
                echo "Tunnel 2 Inside IPv6 Customer Side: $TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE"

                # Update /etc/ipsec-vti.sh with actual values
                echo "Updating /etc/ipsec-vti.sh with actual tunnel inside addresses..."
                sed -i "s/TUNNEL_1_INSIDE_IPV6_AWS_SIDE_IP/$TUNNEL_1_INSIDE_IPV6_AWS_SIDE/g" /etc/ipsec-vti.sh
                sed -i "s/TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE_IP/$TUNNEL_1_INSIDE_IPV6_CUSTOMER_SIDE/g" /etc/ipsec-vti.sh
                sed -i "s/TUNNEL_2_INSIDE_IPV6_AWS_SIDE_IP/$TUNNEL_2_INSIDE_IPV6_AWS_SIDE/g" /etc/ipsec-vti.sh
                sed -i "s/TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE_IP/$TUNNEL_2_INSIDE_IPV6_CUSTOMER_SIDE/g" /etc/ipsec-vti.sh

                echo "ipsec-vti.sh updated successfully with actual tunnel inside addresses"
              mode: "000755"
              owner: "root"
              group: "root"

            "/tmp/update-ipsec-secrets.sh":
              content: !Sub
                - |
                  #!/bin/bash
                  echo "Retrieving pre-shared key from Secrets Manager..."

                  # Install jq if not present
                  if ! command -v jq &> /dev/null; then
                      echo "Installing jq..."
                      apt-get update -y
                      apt-get install -y jq
                      apt install awscli
                  fi

                  echo "install aws cli"
                  apt install awscli -y
                  
                  # Get the pre-shared key for Tunnel 1
                  echo "Fetching secret from: ${SecretArnTunnel1}"
                  PSK1=$(aws secretsmanager get-secret-value \
                    --secret-id ${SecretArnTunnel1} \
                    --region ${Region} \
                    --query 'SecretString' \
                    --output text | jq -r '.psk')

                  if [ -z "$PSK1" ] || [ "$PSK1" = "null" ]; then
                    echo "Failed to retrieve pre-shared key for tunnel 1"
                    exit 1
                  fi

                  echo "Successfully retrieved pre-shared key for tunnel 1 (length: ${!#PSK1})"

                  # Get the pre-shared key for Tunnel 1
                  echo "Fetching secret from: ${SecretArnTunnel2}"
                  PSK2=$(aws secretsmanager get-secret-value \
                    --secret-id ${SecretArnTunnel2} \
                    --region ${Region} \
                    --query 'SecretString' \
                    --output text | jq -r '.psk')

                  if [ -z "$PSK2" ] || [ "$PSK2" = "null" ]; then
                    echo "Failed to retrieve pre-shared key for tunnel 2"
                    exit 1
                  fi

                  echo "Successfully retrieved pre-shared key for tunnel 2 (length: ${!#PSK2})"

                  # Get CloudFormation stack outputs for tunnel addresses
                  echo "Fetching tunnel addresses from CloudFormation stack..."
                  STACK_NAME=${AWS::StackName}

                  TUNNEL_1_OUTSIDE_IPV6=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel1OutsideIpv6`].OutputValue' \
                    --output text)

                  TUNNEL_2_OUTSIDE_IPV6=$(aws cloudformation describe-stacks \
                    --stack-name $STACK_NAME \
                    --region ${Region} \
                    --query 'Stacks[0].Outputs[?OutputKey==`Tunnel2OutsideIpv6`].OutputValue' \
                    --output text)

                  # Get this instance's IPv6 address using IMDSv2
                  echo "Getting EC2 instance IPv6 address..."

                  # Get IMDSv2 token
                  TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" -s)

                  if [ -z "$TOKEN" ]; then
                    echo "Failed to get IMDSv2 token, trying network interface method..."
                    EC2_VPN_IPV6=$(ip -6 addr show | grep 'inet6.*global' | head -1 | awk '{print $2}' | cut -d'/' -f1)
                  else
                    # Try to get IPv6 from metadata service
                    EC2_VPN_IPV6=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/ipv6 2>/dev/null)
                    
                    if [ -z "$EC2_VPN_IPV6" ]; then
                      echo "IPv6 not available from metadata service, trying network interface..."
                      EC2_VPN_IPV6=$(ip -6 addr show | grep 'inet6.*global' | head -1 | awk '{print $2}' | cut -d'/' -f1)
                    fi
                  fi

                  if [ -z "$EC2_VPN_IPV6" ]; then
                    echo "No IPv6 address found, trying to get from CloudFormation outputs..."
                    EC2_VPN_IPV6=$(aws cloudformation describe-stacks \
                      --stack-name $STACK_NAME \
                      --region ${Region} \
                      --query 'Stacks[0].Outputs[?OutputKey==\`OnPremVpcVpnGatewayInstanceIpv6Address\`].OutputValue' \
                      --output text)
                  fi

                  if [ -z "$TUNNEL_1_OUTSIDE_IPV6" ] || [ "$TUNNEL_1_OUTSIDE_IPV6" = "None" ]; then
                    echo "Failed to retrieve Tunnel 1 outside IPv6 address"
                    exit 1
                  fi

                  if [ -z "$TUNNEL_2_OUTSIDE_IPV6" ] || [ "$TUNNEL_2_OUTSIDE_IPV6" = "None" ]; then
                    echo "Failed to retrieve Tunnel 2 outside IPv6 address"
                    exit 1
                  fi

                  if [ -z "$EC2_VPN_IPV6" ]; then
                    echo "Failed to retrieve EC2 instance IPv6 address"
                    exit 1
                  fi

                  echo "Tunnel 1 Outside IPv6: $TUNNEL_1_OUTSIDE_IPV6"
                  echo "Tunnel 2 Outside IPv6: $TUNNEL_2_OUTSIDE_IPV6"
                  echo "EC2 Instances IPv6: $EC2_VPN_IPV6"

                  # Create ipsec.secrets with actual values
                  echo "# /etc/ipsec.secrets - strongSwan IPsec secrets file" > /etc/ipsec.secrets
                  echo "# Generated automatically from AWS Secrets Manager" >> /etc/ipsec.secrets
                  echo "" >> /etc/ipsec.secrets
                  echo "# Tunnel 1" >> /etc/ipsec.secrets
                  echo "$EC2_VPN_IPV6 $TUNNEL_1_OUTSIDE_IPV6 : PSK \"$PSK1\"" >> /etc/ipsec.secrets
                  echo "" >> /etc/ipsec.secrets
                  echo "# Tunnel 2" >> /etc/ipsec.secrets
                  echo "$EC2_VPN_IPV6 $TUNNEL_2_OUTSIDE_IPV6 : PSK \"$PSK2\"" >> /etc/ipsec.secrets

                  # Set proper permissions
                  chmod 600 /etc/ipsec.secrets
                  chown root:root /etc/ipsec.secrets

                  echo "ipsec.secrets updated with actual pre-shared key"
                  echo "File permissions set to 600 (root only)"
                - SecretArnTunnel1: !Ref VpnPreSharedSecretTunnel1
                  SecretArnTunnel2: !Ref VpnPreSharedSecretTunnel2
                  Region: !Ref AWS::Region
              mode: "000755"
              owner: "root"
              group: "root"

            "/etc/cfn/cfn-hup.conf":
              content: !Sub |
                [main]
                stack=${AWS::StackName}
                region=${AWS::Region}
                interval=1
              mode: "000400"
              owner: "root"
              group: "root"
            "/etc/cfn/hooks.d/cfn-auto-reloader.conf":
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.OnPremVpcVpnRouterInstance.Metadata.AWS::CloudFormation::Init
                action=/usr/local/bin/cfn-init -v --stack ${AWS::StackName} --resource OnPremVpcVpnRouterInstance --region ${AWS::Region}
                runas=root
              mode: "000400"
              owner: "root"
              group: "root"

          commands:
            05_update_ipsec_conf:
              command: "/tmp/update-ipsec-conf.sh"
            06_update_ipsec_vti:
              command: "/tmp/update-ipsec-vti.sh"
            07_update_ipsec_secrets:
              command: "/tmp/update-ipsec-secrets.sh"
        
        03-update-configs:
          commands:
            08_configure_strongswan:
              command: "/tmp/configure-strongswan.sh"
            09_configure_frr:
              command: "/tmp/configure-frr.sh"
            10_update_frr_conf:
              command: "/tmp/update-frr-conf.sh"
          
        04-start-services:
          commands:
            11_restart_strongswan:
              command: "systemctl restart strongswan-starter"
            12_log_strongswan_status:
              command: |
                echo "=== strongSwan Status Check at $(date) ===" >> /var/log/strongswan-status.log
                systemctl status strongswan-starter >> /var/log/strongswan-status.log 2>&1
                echo "=== IPSec Status ===" >> /var/log/strongswan-status.log
                ipsec status >> /var/log/strongswan-status.log 2>&1
                echo "=== IPSec StatusAll ===" >> /var/log/strongswan-status.log
                ipsec statusall >> /var/log/strongswan-status.log 2>&1
                echo "=== End Status Check ===" >> /var/log/strongswan-status.log
            13_setup_strongswan_monitoring:
              command: |
                # Create a script to periodically log strongSwan status
                cat > /usr/local/bin/strongswan-monitor.sh << 'EOF'
                #!/bin/bash
                while true; do
                  echo "=== strongSwan Monitor Check at $(date) ===" >> /var/log/strongswan-monitor.log
                  ipsec status >> /var/log/strongswan-monitor.log 2>&1
                  echo "=== VTI Interfaces ===" >> /var/log/strongswan-monitor.log
                  ip link show type vti6 >> /var/log/strongswan-monitor.log 2>&1
                  echo "=== BGP Summary ===" >> /var/log/strongswan-monitor.log
                  vtysh -c "show bgp summary" >> /var/log/strongswan-monitor.log 2>&1
                  echo "=== End Monitor Check ===" >> /var/log/strongswan-monitor.log
                  sleep 300  # Check every 5 minutes
                done
                EOF
                chmod +x /usr/local/bin/strongswan-monitor.sh
                # Start the monitoring script in background
                nohup /usr/local/bin/strongswan-monitor.sh &

    Properties:
      ImageId: "{{resolve:ssm:/aws/service/canonical/ubuntu/server/22.04/stable/current/arm64/hvm/ebs-gp2/ami-id}}"
      KeyName: !Ref InstanceSshKeyPair
      InstanceType: !FindInMap [InstanceTypes, VpnRouterInstanceType, Type]
      EbsOptimized: true
      IamInstanceProfile: !Ref OnPremVpcVpnRouterInstanceProfile
      SourceDestCheck: false
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: gp3
            VolumeSize: 8
            Encrypted: true
      Tags:
        - Key: Name
          Value: !Sub 
            - "${AWS::StackName}-${VpcName}/vpn-router-instance"
            - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]
        - Key: Environment
          Value: !FindInMap [Tags, Environment, Value]
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref OnPremVpcPublicSubnetIpv6Az1
          GroupSet:
            - !Ref OnPremVpcVpnRouterSecurityGroup
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          # Redirect all output to both console and log file
          exec > >(tee /var/log/user-data.log)
          exec 2>&1
          
          echo "Starting user-data script at $(date)"
          
          apt-get update -y
          apt-get install -y python3-pip awscli
          
          # Install CloudWatch agent
          wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
          dpkg -i -E ./amazon-cloudwatch-agent.deb
          
          # Create CloudWatch agent configuration
          cat > /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json << 'EOF'
          {
            "logs": {
              "logs_collected": {
                "files": {
                  "collect_list": [
                    {
                      "file_path": "/var/log/user-data.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/user-data",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/cloud-init.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/cloud-init",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/cloud-init-output.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/cloud-init-output",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/cfn-init.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/cfn-init",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/cfn-init-cmd.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/cfn-init-cmd",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/ipsec-vti.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/ipsec-vti",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/syslog",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/syslog",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/auth.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/auth",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/frr/zebra.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/frr-zebra",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/frr/bgpd.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/frr-bgpd",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/frr/frr.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/frr-general",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/strongswan-charon.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/strongswan-charon",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/strongswan/strongswan.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/strongswan-syslog",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/strongswan-status.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/strongswan-status",
                      "timezone": "UTC"
                    },
                    {
                      "file_path": "/var/log/strongswan-monitor.log",
                      "log_group_name": "/aws/ec2/${AWS::StackName}/vpn-router",
                      "log_stream_name": "{instance_id}/strongswan-monitor",
                      "timezone": "UTC"
                    }
                  ]
                }
              }
            }
          }
          EOF
          
          # Start CloudWatch agent
          /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
            -a fetch-config \
            -m ec2 \
            -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json \
            -s
          
          pip3 install https://s3.dualstack.us-east-1.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz

          echo "Running cfn-init at $(date)"
          # Run cfn-init
          /usr/local/bin/cfn-init -v --stack ${AWS::StackName} --resource OnPremVpcVpnRouterInstance --configsets default --region ${AWS::Region}

          CFN_INIT_EXIT_CODE=$?
          echo "cfn-init completed with exit code: $CFN_INIT_EXIT_CODE at $(date)"

          # Start cfn-hup service for continuous updates
          /usr/local/bin/cfn-hup &

          echo "Signaling CloudFormation at $(date)"
          /usr/local/bin/cfn-signal -e $CFN_INIT_EXIT_CODE --stack ${AWS::StackName} --resource OnPremVpcVpnRouterInstance --region ${AWS::Region}
          
          echo "User-data script completed at $(date)"

  OnPremToAwsVpcRouteIpv6AutomatedAz1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPrivateIpv6RouteTableAz1
      DestinationIpv6CidrBlock: !Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks]
      InstanceId: !Ref OnPremVpcVpnRouterInstance

  OnPremToAwsVpcRouteIpv6AutomatedAz2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref OnPremVpcPrivateIpv6RouteTableAz2
      DestinationIpv6CidrBlock: !Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks]
      InstanceId: !Ref OnPremVpcVpnRouterInstance


# =============================================================================
# Outputs
# =============================================================================
Outputs:
  # VPC Information
  OnPremVpcIpv6Prefix:
    Description: "On-Premises VPC IPv6 Prefix"
    Value: !Select [0, !GetAtt OnPremVpc.Ipv6CidrBlocks]
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${VpcName}-ipv6-prefix"
        - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]

  AwsVpcIpv6Prefix:
    Description: "AWS VPC IPv6 Prefix"
    Value: !Select [0, !GetAtt AwsVpc.Ipv6CidrBlocks]
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${VpcName}-ipv6-prefix"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]

  # EC2 Instances
  AwsVpcTestInstanceId:
    Description: "AWS VPC Test Instance ID (IPv6-only)"
    Value: !Ref AwsVpcTestEc2Instance
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${VpcName}-test-instance-id"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]

  AwsVpcJumpInstanceId:
    Description: "AWS VPC Jump Instance ID (provides access to AWS VPC Test Instance)"
    Value: !Ref AwsVpcJumpInstance
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${VpcName}-jump-instance-id"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]


  AwsVpcTestInstanceIpv6Address:
    Description: "IPv6 Address of the AWS VPC Test Instance"
    Value: !GetAtt GetAwsTestInstanceIpv6Address.Ipv6Address
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${VpcName}-test-instance-ipv6-address"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]

  OnPremVpcTestInstanceId:
    Description: "OnPrem VPC Test Instance ID (IPv6-only)"
    Value: !Ref OnPremVpcTestInstance
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${VpcName}-test-instance-id"
        - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]

  OnPremVpcTestInstanceIpv6Address:
    Description: "IPv6 Address of the OnPrem VPC Test Instance"
    Value: !GetAtt GetOnPremTestInstanceIpv6Address.Ipv6Address
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${VpcName}-test-instance-ipv6-address"
        - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]

  OnPremVpcVpnRouterInstanceId:
    Description: "OnPrem VPC VPN Router Instance ID"
    Value: !Ref OnPremVpcVpnRouterInstance
    Export:
      Name: !Sub "${AWS::StackName}-vpn-router-instance-id"

  OnPremVpcVpnRouterInstanceIpv6Address:
    Description: "IPv6 Address of the OnPrem VPC VPN Router Instance"
    Value: !GetAtt GetOnPremVpnRouterInstanceIpv6Address.Ipv6Address
    Export:
      Name: !Sub "${AWS::StackName}-vpn-router-ipv6-address"

  VpnConnectionId:
    Description: "VPN Connection ID"
    Value: !Ref TransitGatewayVpnConnection
    Export:
      Name: !Sub "${AWS::StackName}-vpn-connection-id"

  VpnConnectionLogGroupName:
    Description: "VPN Connection CloudWatch Log Group Name"
    Value: !Ref VpnConnectionLogGroup
    Export:
      Name: !Sub "${AWS::StackName}-vpn-connection-log-group-name"

  # VPC Resources
  AwsVpcId:
    Description: "AWS VPC ID"
    Value: !Ref AwsVpc
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${VpcName}-id"
        - VpcName: !FindInMap [NamingConventions, AwsVpc, Name]

  OnPremVpcId:
    Description: "On-Premises VPC ID"
    Value: !Ref OnPremVpc
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${VpcName}-id"
        - VpcName: !FindInMap [NamingConventions, OnPremVpc, Name]

  # Transit Gateway Resources
  AwsTransitGatewayId:
    Description: "Transit Gateway ID"
    Value: !Ref TransitGateway
    Export:
      Name: !Sub "${AWS::StackName}-transit-gateway-id"

  # Key Pair
  InstanceKeyPairName:
    Description: "Instance Key Pair Name"
    Value: !Ref InstanceSshKeyPair
    Export:
      Name: !Sub 
        - "${AWS::StackName}-${KeyName}-name"
        - KeyName: !FindInMap [NamingConventions, SshKey, Name]

  InstanceKeyPairParameterName:
    Description: "Parameter Store location of the private key"
    Value: !Sub "/ec2/keypair/${InstanceSshKeyPair.KeyPairId}"
    Export:
      Name: !Sub "${AWS::StackName}-keypair-parameter-name"


  # VPN Tunnel Details
  Tunnel1OutsideIpv6:
    Description: "Tunnel 1 Outside IPv6 Address"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel1OutsideIpv6
    Export:
      Name: !Sub "${AWS::StackName}-tunnel1-outside-ipv6"

  Tunnel1InsideIpv4Prefix:
    Description: "Tunnel 1 Inside IPv4 Prefix"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel1InsideIpv4Prefix
    Export:
      Name: !Sub "${AWS::StackName}-tunnel1-inside-ipv4-prefix"

  Tunnel1InsideIpv6Prefix:
    Description: "Tunnel 1 Inside IPv6 Prefix"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel1InsideIpv6Prefix
    Export:
      Name: !Sub "${AWS::StackName}-tunnel1-inside-ipv6-prefix"

  Tunnel1InsideIpv6AwsSide:
    Description: "Tunnel 1 Inside IPv6 AWS Side Address"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel1InsideIpv6AwsSide
    Export:
      Name: !Sub "${AWS::StackName}-tunnel1-inside-ipv6-aws-side"

  Tunnel1InsideIpv6CustomerSide:
    Description: "Tunnel 1 Inside IPv6 Customer Side Address"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel1InsideIpv6CustomerSide
    Export:
      Name: !Sub "${AWS::StackName}-tunnel1-inside-ipv6-customer-side"

  Tunnel2OutsideIpv6:
    Description: "Tunnel 2 Outside IPv6 Address"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel2OutsideIpv6
    Export:
      Name: !Sub "${AWS::StackName}-tunnel2-outside-ipv6"

  Tunnel2InsideIpv4Prefix:
    Description: "Tunnel 2 Inside IPv4 Prefix"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel2InsideIpv4Prefix
    Export:
      Name: !Sub "${AWS::StackName}-tunnel2-inside-ipv4-prefix"

  Tunnel2InsideIpv6Prefix:
    Description: "Tunnel 2 Inside IPv6 Prefix"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel2InsideIpv6Prefix
    Export:
      Name: !Sub "${AWS::StackName}-tunnel2-inside-ipv6-prefix"

  Tunnel2InsideIpv6AwsSide:
    Description: "Tunnel 2 Inside IPv6 AWS Side Address"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel2InsideIpv6AwsSide
    Export:
      Name: !Sub "${AWS::StackName}-tunnel2-inside-ipv6-aws-side"

  Tunnel2InsideIpv6CustomerSide:
    Description: "Tunnel 2 Inside IPv6 Customer Side Address"
    Value: !GetAtt GetVpnTunnelDetails.Tunnel2InsideIpv6CustomerSide
    Export:
      Name: !Sub "${AWS::StackName}-tunnel2-inside-ipv6-customer-side"

  # SNS Topic for VPN Monitoring
  VpnTunnelStateNotificationTopicArn:
    Description: "SNS Topic ARN for VPN tunnel state notifications"
    Value: !Ref VpnTunnelStateNotificationTopic
    Export:
      Name: !Sub "${AWS::StackName}-vpn-tunnel-notification-topic"

  VpnTunnelStateAlarmName:
    Description: "CloudWatch Alarm name for VPN tunnel state monitoring"
    Value: !Ref VpnTunnelStateAlarm
    Export:
      Name: !Sub "${AWS::StackName}-vpn-tunnel-alarm"


