AWSTemplateFormatVersion: "2010-09-09"
Description: >
  Transient.AI DAF (Declarative Agentic Framework) — AWS Marketplace Edition.
  Deploys a complete AI agent platform: VPC, RDS PostgreSQL, ECS Fargate (backend + frontend),
  ALB, Cognito User Pool, WAF, and auto-runs database migrations on first deploy.
  No manual post-deployment steps required.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Your Company"
        Parameters:
          - CompanyName
          - AdminEmail
          - AlertEmail
      - Label:
          default: "Instance Sizing"
        Parameters:
          - RdsInstanceClass
          - BackendCpu
          - BackendMemory
          - DesiredTaskCount
      - Label:
          default: "Security (optional)"
        Parameters:
          - AcmCertificateArn
          - EnableWaf
      - Label:
          default: "Images (use defaults — only change if self-hosting)"
        Parameters:
          - BackendImageUri
          - FrontendImageUri
    ParameterLabels:
      CompanyName:
        default: "Company name"
      AdminEmail:
        default: "Admin email"
      AlertEmail:
        default: "Alert email"
      RdsInstanceClass:
        default: "Database size"
      BackendCpu:
        default: "Backend CPU units"
      BackendMemory:
        default: "Backend memory (MB)"
      DesiredTaskCount:
        default: "Replica count"
      AcmCertificateArn:
        default: "ACM certificate ARN (for HTTPS)"
      EnableWaf:
        default: "Enable WAF"

# =============================================================================
# PARAMETERS
# =============================================================================
Parameters:

  CompanyName:
    Type: String
    Description: "Lowercase company name, no spaces (e.g. acme). Used as resource name prefix."
    AllowedPattern: "^[a-z][a-z0-9]{1,19}$"
    ConstraintDescription: "2-20 lowercase alphanumeric characters, starting with a letter."

  AdminEmail:
    Type: String
    Description: "Email address for the first admin user account."

  AlertEmail:
    Type: String
    Description: "Email address to receive CloudWatch alarm notifications."

  RdsInstanceClass:
    Type: String
    Default: db.t3.small
    AllowedValues:
      - db.t3.micro
      - db.t3.small
      - db.t3.medium
      - db.r6g.large
    Description: "db.t3.small = evaluation/dev; db.r6g.large = production workloads"

  BackendCpu:
    Type: Number
    Default: 512
    AllowedValues: [256, 512, 1024, 2048]
    Description: "CPU units per backend task (1024 = 1 vCPU)."

  BackendMemory:
    Type: Number
    Default: 1024
    AllowedValues: [512, 1024, 2048, 4096]
    Description: "Memory (MB) per backend task."

  DesiredTaskCount:
    Type: Number
    Default: 2
    MinValue: 1
    MaxValue: 10
    Description: "Number of running replicas per service (2 = high-availability)."

  AcmCertificateArn:
    Type: String
    Default: ""
    Description: "ACM certificate ARN for HTTPS termination. Leave blank for HTTP-only."

  EnableWaf:
    Type: String
    Default: "true"
    AllowedValues: ["true", "false"]
    Description: "Attach AWS WAF with OWASP managed rule sets to the load balancer."

  BackendImageUri:
    Type: String
    Default: "709825985650.dkr.ecr.us-east-1.amazonaws.com/transient-ai/daf-sdk-backend:1.0.0"
    Description: "Backend container image. Change only if self-hosting images in your own ECR."

  FrontendImageUri:
    Type: String
    Default: "709825985650.dkr.ecr.us-east-1.amazonaws.com/transient-ai/daf-sdk-frontend:1.0.0"
    Description: "Frontend container image. Change only if self-hosting images in your own ECR."

# =============================================================================
# CONDITIONS
# =============================================================================
Conditions:
  HasAcmCertificate: !Not [!Equals [!Ref AcmCertificateArn, ""]]
  NoHttps: !Equals [!Ref AcmCertificateArn, ""]
  EnableWafCondition: !Equals [!Ref EnableWaf, "true"]

# =============================================================================
# RESOURCES
# =============================================================================
Resources:

  # ===========================================================================
  # NETWORKING
  # ===========================================================================

  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "10.0.0.0/16"
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-vpc"

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-sn-pub-1"

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: "10.0.2.0/24"
      AvailabilityZone: !Select [1, !GetAZs ""]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-sn-pub-2"

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: "10.0.11.0/24"
      AvailabilityZone: !Select [0, !GetAZs ""]
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-sn-prv-1"

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: "10.0.12.0/24"
      AvailabilityZone: !Select [1, !GetAZs ""]
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-sn-prv-2"

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-igw"

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc
      InternetGatewayId: !Ref InternetGateway

  NatEip:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatEip.AllocationId
      SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-nat"

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet2RouteTableAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NatGateway

  PrivateSubnet1RouteTableAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnet2RouteTableAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

  # ===========================================================================
  # SECURITY GROUPS
  # ===========================================================================

  AlbSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "ALB - HTTP/HTTPS from internet"
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - { IpProtocol: tcp, FromPort: 80,  ToPort: 80,  CidrIp: "0.0.0.0/0" }
        - { IpProtocol: tcp, FromPort: 443, ToPort: 443, CidrIp: "0.0.0.0/0" }
      SecurityGroupEgress:
        - { IpProtocol: "-1", CidrIp: "0.0.0.0/0" }
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-sg-alb"

  BackendSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "Backend ECS tasks - port 8012 from ALB"
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8012
          ToPort: 8012
          SourceSecurityGroupId: !Ref AlbSecurityGroup
      SecurityGroupEgress:
        - { IpProtocol: "-1", CidrIp: "0.0.0.0/0" }
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-sg-backend"

  FrontendSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "Frontend ECS tasks - port 8080 from ALB"
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          SourceSecurityGroupId: !Ref AlbSecurityGroup
      SecurityGroupEgress:
        - { IpProtocol: "-1", CidrIp: "0.0.0.0/0" }
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-sg-frontend"

  RdsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "RDS - Postgres from backend and migration Lambda"
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          SourceSecurityGroupId: !Ref BackendSecurityGroup
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          SourceSecurityGroupId: !Ref MigrationLambdaSecurityGroup
      SecurityGroupEgress:
        - { IpProtocol: "-1", CidrIp: "0.0.0.0/0" }
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-sg-rds"

  MigrationLambdaSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "Migration Lambda - outbound to RDS and ECR Public"
      VpcId: !Ref Vpc
      SecurityGroupEgress:
        - { IpProtocol: "-1", CidrIp: "0.0.0.0/0" }
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-sg-migration"

  # ===========================================================================
  # COGNITO (created by this template - no pre-flight needed)
  # ===========================================================================

  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Sub "${CompanyName}-daf-users"
      AutoVerifiedAttributes: [email]
      UsernameAttributes: [email]
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireUppercase: true
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: false
      Schema:
        - Name: email
          Required: true
          Mutable: true

  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    DependsOn: Alb
    Properties:
      UserPoolId: !Ref CognitoUserPool
      ClientName: !Sub "${CompanyName}-daf-frontend"
      GenerateSecret: false
      ExplicitAuthFlows:
        - ALLOW_USER_PASSWORD_AUTH
        - ALLOW_USER_SRP_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH
      CallbackURLs:
        - !If
          - HasAcmCertificate
          - !Sub "https://${Alb.DNSName}/api/auth/oauth/cognito/callback"
          - "http://localhost/api/auth/oauth/cognito/callback"
      LogoutURLs:
        - !If
          - HasAcmCertificate
          - !Sub "https://${Alb.DNSName}"
          - "http://localhost"
      AllowedOAuthFlows: [code]
      AllowedOAuthScopes: [openid, email, profile]
      AllowedOAuthFlowsUserPoolClient: true
      SupportedIdentityProviders: [COGNITO]

  # ===========================================================================
  # SECRETS MANAGER (auto-generated - no CHANGE_ME placeholders)
  # ===========================================================================

  RdsPasswordSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub "${CompanyName}/daf/rds-password"
      Description: "Auto-generated RDS master password"
      GenerateSecretString:
        SecretStringTemplate: '{"username": "dafadmin"}'
        GenerateStringKey: "password"
        ExcludeCharacters: '"@/\\'
        PasswordLength: 32

  JwtSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub "${CompanyName}/daf/jwt-secret"
      Description: "Auto-generated JWT signing secret"
      GenerateSecretString:
        SecretStringTemplate: "{}"
        GenerateStringKey: "JWT_SECRET"
        ExcludeCharacters: '"@/\\'
        PasswordLength: 64

  PasswordSaltSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub "${CompanyName}/daf/password-salt"
      Description: "Auto-generated password hash salt"
      GenerateSecretString:
        SecretStringTemplate: "{}"
        GenerateStringKey: "PASSWORD_SALT"
        ExcludeCharacters: '"@/\\'
        PasswordLength: 64

  AdminPasswordSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub "${CompanyName}/daf/admin-password"
      Description: "Auto-generated initial admin password - retrieve from Secrets Manager after deploy"
      GenerateSecretString:
        SecretStringTemplate: !Sub '{"email": "${AdminEmail}"}'
        GenerateStringKey: "ADMIN_PASSWORD"
        ExcludeCharacters: '"@/\\'
        PasswordLength: 24

  # ===========================================================================
  # RDS POSTGRESQL
  # ===========================================================================

  RdsSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupName: !Sub "${CompanyName}-daf-rds-sngrp"
      DBSubnetGroupDescription: !Sub "DAF ${CompanyName} RDS subnet group"
      SubnetIds:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2

  RdsInstance:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Snapshot
    UpdateReplacePolicy: Snapshot
    Properties:
      DBInstanceIdentifier: !Sub "${CompanyName}-daf-rds"
      Engine: postgres
      EngineVersion: "15.18"
      DBInstanceClass: !Ref RdsInstanceClass
      AllocatedStorage: "20"
      StorageType: gp3
      StorageEncrypted: true
      DBName: daf
      MasterUsername: dafadmin
      MasterUserPassword: !Sub "{{resolve:secretsmanager:${RdsPasswordSecret}:SecretString:password}}"
      DBSubnetGroupName: !Ref RdsSubnetGroup
      VPCSecurityGroups:
        - !Ref RdsSecurityGroup
      MultiAZ: false
      PubliclyAccessible: false
      DeletionProtection: false
      BackupRetentionPeriod: 7
      PreferredBackupWindow: "03:00-04:00"
      PreferredMaintenanceWindow: "Mon:04:00-Mon:05:00"
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-rds"

  # ===========================================================================
  # APPLICATION LOAD BALANCER
  # ===========================================================================

  Alb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    DependsOn: InternetGatewayAttachment
    Properties:
      Name: !Sub "${CompanyName}-daf-alb"
      Scheme: internet-facing
      Type: application
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      SecurityGroups:
        - !Ref AlbSecurityGroup
      Tags:
        - Key: Name
          Value: !Sub "${CompanyName}-daf-alb"

  BackendTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub "${CompanyName}-daf-tg-be"
      VpcId: !Ref Vpc
      Protocol: HTTP
      Port: 8012
      TargetType: ip
      HealthCheckPath: "/health"
      HealthCheckIntervalSeconds: 30
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 3

  FrontendTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub "${CompanyName}-daf-tg-fe"
      VpcId: !Ref Vpc
      Protocol: HTTP
      Port: 8080
      TargetType: ip
      HealthCheckPath: "/health"
      HealthCheckIntervalSeconds: 30
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 3

  HttpListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref Alb
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - !If
          - HasAcmCertificate
          - Type: redirect
            RedirectConfig:
              Protocol: HTTPS
              Port: "443"
              StatusCode: HTTP_301
          - Type: forward
            TargetGroupArn: !Ref FrontendTargetGroup

  HttpApiRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Condition: NoHttps
    Properties:
      ListenerArn: !Ref HttpListener
      Priority: 10
      Conditions:
        - Field: path-pattern
          Values: ["/api/*"]
      Actions:
        - Type: forward
          TargetGroupArn: !Ref BackendTargetGroup

  HttpsListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Condition: HasAcmCertificate
    Properties:
      LoadBalancerArn: !Ref Alb
      Port: 443
      Protocol: HTTPS
      SslPolicy: "ELBSecurityPolicy-TLS13-1-2-2021-06"
      Certificates:
        - CertificateArn: !Ref AcmCertificateArn
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref FrontendTargetGroup

  HttpsApiRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Condition: HasAcmCertificate
    Properties:
      ListenerArn: !Ref HttpsListener
      Priority: 10
      Conditions:
        - Field: path-pattern
          Values: ["/api/*"]
      Actions:
        - Type: forward
          TargetGroupArn: !Ref BackendTargetGroup

  # ===========================================================================
  # WAF
  # ===========================================================================

  WafWebAcl:
    Type: AWS::WAFv2::WebACL
    Condition: EnableWafCondition
    Properties:
      Name: !Sub "${CompanyName}-daf-waf"
      Scope: REGIONAL
      DefaultAction:
        Allow: {}
      VisibilityConfig:
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: !Sub "${CompanyName}DafWaf"
      Rules:
        - Name: AWSManagedRulesCommonRuleSet
          Priority: 1
          OverrideAction: { None: {} }
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesCommonRuleSet
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: CommonRuleSet
        - Name: AWSManagedRulesKnownBadInputsRuleSet
          Priority: 2
          OverrideAction: { None: {} }
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesKnownBadInputsRuleSet
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: KnownBadInputs
        - Name: AWSManagedRulesSQLiRuleSet
          Priority: 3
          OverrideAction: { None: {} }
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesSQLiRuleSet
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: SQLiRuleSet

  WafAssociation:
    Type: AWS::WAFv2::WebACLAssociation
    Condition: EnableWafCondition
    Properties:
      ResourceArn: !Ref Alb
      WebACLArn: !GetAtt WafWebAcl.Arn

  # ===========================================================================
  # IAM ROLES
  # ===========================================================================

  EcsExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${CompanyName}-daf-role-ecs-exec"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: { Service: ecs-tasks.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Policies:
        - PolicyName: read-daf-secrets
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: [secretsmanager:GetSecretValue, secretsmanager:DescribeSecret]
                Resource:
                  - !Ref RdsPasswordSecret
                  - !Ref JwtSecret
                  - !Ref PasswordSaltSecret
                  - !Ref AdminPasswordSecret
        - PolicyName: ecr-public-auth
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: [ecr-public:GetAuthorizationToken, sts:GetServiceBearerToken]
                Resource: "*"

  EcsTaskRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${CompanyName}-daf-role-ecs-task"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: { Service: ecs-tasks.amazonaws.com }
            Action: sts:AssumeRole
      Policies:
        - PolicyName: bedrock-invoke
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: [bedrock:InvokeModel, bedrock:InvokeModelWithResponseStream]
                Resource: "*"

  # ===========================================================================
  # CLOUDWATCH LOG GROUPS
  # ===========================================================================

  BackendLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/ecs/${CompanyName}-daf-backend"
      RetentionInDays: 90

  FrontendLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/ecs/${CompanyName}-daf-frontend"
      RetentionInDays: 30

  # ===========================================================================
  # ECS CLUSTER + TASK DEFINITIONS
  # ===========================================================================

  EcsCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub "${CompanyName}-daf-ecs"
      ClusterSettings:
        - Name: containerInsights
          Value: enabled

  BackendTaskDef:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub "${CompanyName}-daf-backend"
      RequiresCompatibilities: [FARGATE]
      NetworkMode: awsvpc
      Cpu: !Ref BackendCpu
      Memory: !Ref BackendMemory
      ExecutionRoleArn: !GetAtt EcsExecutionRole.Arn
      TaskRoleArn: !GetAtt EcsTaskRole.Arn
      ContainerDefinitions:
        - Name: daf-backend
          Image: !Ref BackendImageUri
          Essential: true
          PortMappings:
            - ContainerPort: 8012
              Protocol: tcp
          Environment:
            - { Name: SERVICE_NAME,         Value: "daf-sdk-backend" }
            - { Name: PYTHONPATH,           Value: "/app" }
            - { Name: PORT,                 Value: "8012" }
            - { Name: HOST,                 Value: "0.0.0.0" }
            - { Name: ENVIRONMENT,          Value: "production" }
            - { Name: PYTHONUNBUFFERED,     Value: "1" }
            - { Name: POSTGRES_HOST,        Value: !GetAtt RdsInstance.Endpoint.Address }
            - { Name: POSTGRES_PORT,        Value: "5432" }
            - { Name: POSTGRES_DB,          Value: "daf" }
            - { Name: POSTGRES_SSL_MODE,    Value: "require" }
            - { Name: POSTGRES_SSL_VERIFY,  Value: "false" }
            - { Name: UPLOAD_DIR,           Value: "/tmp/uploads" }
            - { Name: AUTH_MODE,            Value: "cognito" }
            - { Name: AUTH_DISABLED,        Value: "false" }
            - { Name: REGISTRATION_ENABLED, Value: "false" }
            - { Name: COGNITO_USER_POOL_ID, Value: !Ref CognitoUserPool }
            - { Name: COGNITO_CLIENT_ID,    Value: !Ref CognitoUserPoolClient }
            - { Name: COGNITO_REGION,       Value: !Ref "AWS::Region" }
            - { Name: ADMIN_EMAIL,          Value: !Ref AdminEmail }
            - { Name: CORS_ORIGINS,         Value: !If [HasAcmCertificate, !Sub "https://${Alb.DNSName}", !Sub "http://${Alb.DNSName}"] }
            - { Name: MAX_FILE_SIZE_MB,     Value: "10" }
            - { Name: AWS_DEFAULT_REGION,   Value: !Ref "AWS::Region" }
          Secrets:
            - { Name: POSTGRES_PASSWORD, ValueFrom: !Sub "${RdsPasswordSecret}:password::" }
            - { Name: POSTGRES_USER,     ValueFrom: !Sub "${RdsPasswordSecret}:username::" }
            - { Name: JWT_SECRET,        ValueFrom: !Sub "${JwtSecret}:JWT_SECRET::" }
            - { Name: PASSWORD_SALT,     ValueFrom: !Sub "${PasswordSaltSecret}:PASSWORD_SALT::" }
            - { Name: ADMIN_PASSWORD,    ValueFrom: !Sub "${AdminPasswordSecret}:ADMIN_PASSWORD::" }
          HealthCheck:
            Command: ["CMD-SHELL", "curl -sf http://localhost:8012/health || exit 1"]
            Interval: 30
            Timeout: 10
            StartPeriod: 60
            Retries: 3
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref BackendLogGroup
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: ecs

  FrontendTaskDef:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub "${CompanyName}-daf-frontend"
      RequiresCompatibilities: [FARGATE]
      NetworkMode: awsvpc
      Cpu: 256
      Memory: 512
      ExecutionRoleArn: !GetAtt EcsExecutionRole.Arn
      TaskRoleArn: !GetAtt EcsTaskRole.Arn
      ContainerDefinitions:
        - Name: daf-frontend
          Image: !Ref FrontendImageUri
          Essential: true
          PortMappings:
            - ContainerPort: 8080
              Protocol: tcp
          Environment:
            - { Name: VITE_USE_COGNITO_NATIVE,   Value: "true" }
            - { Name: VITE_COGNITO_USER_POOL_ID, Value: !Ref CognitoUserPool }
            - { Name: VITE_COGNITO_CLIENT_ID,    Value: !Ref CognitoUserPoolClient }
            - { Name: VITE_COGNITO_REGION,       Value: !Ref "AWS::Region" }
            - { Name: VITE_API_BASE_URL,         Value: !If [HasAcmCertificate, !Sub "https://${Alb.DNSName}", !Sub "http://${Alb.DNSName}"] }
          HealthCheck:
            Command: ["CMD-SHELL", "wget -q --spider http://localhost:8080/health || exit 1"]
            Interval: 30
            Timeout: 10
            StartPeriod: 15
            Retries: 3
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref FrontendLogGroup
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: ecs

  # ===========================================================================
  # MIGRATION LAMBDA CUSTOM RESOURCE
  # Runs `alembic upgrade head` via ECS task override before services start.
  # CloudFormation waits for SUCCESS before creating BackendService/FrontendService.
  # ===========================================================================

  MigrationLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${CompanyName}-daf-role-migration"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: { Service: lambda.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
      Policies:
        - PolicyName: ecs-run-task
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: [ecs:RunTask, ecs:DescribeTasks]
                Resource: "*"
              - Effect: Allow
                Action: iam:PassRole
                Resource:
                  - !GetAtt EcsExecutionRole.Arn
                  - !GetAtt EcsTaskRole.Arn

  MigrationFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${CompanyName}-daf-migration"
      Runtime: python3.12
      Handler: index.handler
      Timeout: 900
      Role: !GetAtt MigrationLambdaRole.Arn
      VpcConfig:
        SubnetIds: [!Ref PrivateSubnet1]
        SecurityGroupIds: [!Ref MigrationLambdaSecurityGroup]
      Environment:
        Variables:
          CLUSTER_ARN: !GetAtt EcsCluster.Arn
          TASK_DEF_ARN: !Ref BackendTaskDef
          SUBNET_ID: !Ref PrivateSubnet1
          SECURITY_GROUP_ID: !Ref BackendSecurityGroup
      Code:
        ZipFile: |
          import boto3, json, time, urllib.request, os

          def cfn_respond(url, stack_id, req_id, logical_id, status, physical_id, reason="", data={}):
              body = json.dumps({
                  "Status": status,
                  "Reason": reason,
                  "PhysicalResourceId": physical_id,
                  "StackId": stack_id,
                  "RequestId": req_id,
                  "LogicalResourceId": logical_id,
                  "Data": data,
              }).encode()
              req = urllib.request.Request(
                  url, data=body,
                  headers={"Content-Type": "", "Content-Length": str(len(body))},
                  method="PUT",
              )
              urllib.request.urlopen(req, timeout=10)

          def handler(event, context):
              print(json.dumps(event))
              url      = event["ResponseURL"]
              stack_id = event["StackId"]
              req_id   = event["RequestId"]
              logical  = event["LogicalResourceId"]
              phys     = event.get("PhysicalResourceId", "daf-migration")

              if event["RequestType"] == "Delete":
                  cfn_respond(url, stack_id, req_id, logical, "SUCCESS", phys)
                  return

              try:
                  ecs = boto3.client("ecs")
                  resp = ecs.run_task(
                      cluster=os.environ["CLUSTER_ARN"],
                      taskDefinition=os.environ["TASK_DEF_ARN"],
                      launchType="FARGATE",
                      networkConfiguration={"awsvpcConfiguration": {
                          "subnets": [os.environ["SUBNET_ID"]],
                          "securityGroups": [os.environ["SECURITY_GROUP_ID"]],
                          "assignPublicIp": "DISABLED",
                      }},
                      overrides={"containerOverrides": [{
                          "name": "daf-backend",
                          "command": ["python", "-m", "alembic", "upgrade", "head"],
                      }]},
                  )
                  if resp.get("failures"):
                      raise Exception(f"RunTask failures: {resp['failures']}")

                  task_arn = resp["tasks"][0]["taskArn"]
                  print(f"Migration task started: {task_arn}")

                  for i in range(180):
                      time.sleep(5)
                      desc = ecs.describe_tasks(
                          cluster=os.environ["CLUSTER_ARN"],
                          tasks=[task_arn],
                      )
                      task = desc["tasks"][0]
                      last_status = task["lastStatus"]
                      print(f"[{i*5}s] Task status: {last_status}")
                      if last_status == "STOPPED":
                          code = task["containers"][0].get("exitCode")
                          reason = task["containers"][0].get("reason", "")
                          if code == 0:
                              cfn_respond(url, stack_id, req_id, logical, "SUCCESS", phys,
                                          data={"TaskArn": task_arn, "Message": "Migrations complete"})
                          else:
                              cfn_respond(url, stack_id, req_id, logical, "FAILED", phys,
                                          reason=f"Migration exited {code}: {reason}")
                          return

                  cfn_respond(url, stack_id, req_id, logical, "FAILED", phys,
                              reason="Migration timed out after 15 minutes")

              except Exception as e:
                  print(f"ERROR: {e}")
                  cfn_respond(url, stack_id, req_id, logical, "FAILED", phys, reason=str(e))

  MigrationRunner:
    Type: AWS::CloudFormation::CustomResource
    DependsOn:
      - RdsInstance
      - RdsPasswordSecret
      - JwtSecret
      - PasswordSaltSecret
      - AdminPasswordSecret
    Properties:
      ServiceToken: !GetAtt MigrationFunction.Arn

  # ===========================================================================
  # ECS SERVICES (depend on MigrationRunner - start only after migrations pass)
  # ===========================================================================

  BackendService:
    Type: AWS::ECS::Service
    DependsOn:
      - HttpListener
      - MigrationRunner
    Properties:
      ServiceName: !Sub "${CompanyName}-daf-svc-backend"
      Cluster: !Ref EcsCluster
      TaskDefinition: !Ref BackendTaskDef
      DesiredCount: !Ref DesiredTaskCount
      LaunchType: FARGATE
      HealthCheckGracePeriodSeconds: 120
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          Subnets:
            - !Ref PrivateSubnet1
            - !Ref PrivateSubnet2
          SecurityGroups:
            - !Ref BackendSecurityGroup
      LoadBalancers:
        - ContainerName: daf-backend
          ContainerPort: 8012
          TargetGroupArn: !Ref BackendTargetGroup
      DeploymentConfiguration:
        MinimumHealthyPercent: 50
        MaximumPercent: 200

  FrontendService:
    Type: AWS::ECS::Service
    DependsOn:
      - HttpListener
      - MigrationRunner
    Properties:
      ServiceName: !Sub "${CompanyName}-daf-svc-frontend"
      Cluster: !Ref EcsCluster
      TaskDefinition: !Ref FrontendTaskDef
      DesiredCount: !Ref DesiredTaskCount
      LaunchType: FARGATE
      HealthCheckGracePeriodSeconds: 60
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          Subnets:
            - !Ref PrivateSubnet1
            - !Ref PrivateSubnet2
          SecurityGroups:
            - !Ref FrontendSecurityGroup
      LoadBalancers:
        - ContainerName: daf-frontend
          ContainerPort: 8080
          TargetGroupArn: !Ref FrontendTargetGroup

  # ===========================================================================
  # MONITORING
  # ===========================================================================

  AlertsTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub "${CompanyName}-daf-alerts"

  AlertsSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref AlertsTopic
      Protocol: email
      Endpoint: !Ref AlertEmail

  AlarmBackendCpu:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${CompanyName}-daf-backend-cpu-high"
      AlarmDescription: "ECS backend CPU > 80% for 5 min"
      Namespace: AWS/ECS
      MetricName: CPUUtilization
      Dimensions:
        - Name: ClusterName
          Value: !Ref EcsCluster
        - Name: ServiceName
          Value: !GetAtt BackendService.Name
      Statistic: Average
      Period: 300
      EvaluationPeriods: 2
      Threshold: 80
      ComparisonOperator: GreaterThanThreshold
      AlarmActions: [!Ref AlertsTopic]
      OKActions: [!Ref AlertsTopic]

  AlarmRdsCpu:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${CompanyName}-daf-rds-cpu-high"
      AlarmDescription: "RDS CPU > 80% for 10 min"
      Namespace: AWS/RDS
      MetricName: CPUUtilization
      Dimensions:
        - Name: DBInstanceIdentifier
          Value: !Ref RdsInstance
      Statistic: Average
      Period: 300
      EvaluationPeriods: 2
      Threshold: 80
      ComparisonOperator: GreaterThanThreshold
      AlarmActions: [!Ref AlertsTopic]
      OKActions: [!Ref AlertsTopic]

# =============================================================================
# OUTPUTS
# =============================================================================
Outputs:

  ApplicationUrl:
    Description: "DAF application URL - open this in your browser to get started"
    Value: !If
      - HasAcmCertificate
      - !Sub "https://${Alb.DNSName}"
      - !Sub "http://${Alb.DNSName}"

  AlbDnsName:
    Description: "ALB DNS name - use for CNAME if setting up a custom domain"
    Value: !GetAtt Alb.DNSName

  CognitoUserPoolId:
    Description: "Auto-created Cognito User Pool ID"
    Value: !Ref CognitoUserPool

  CognitoClientId:
    Description: "Auto-created Cognito App Client ID"
    Value: !Ref CognitoUserPoolClient

  AdminPasswordSecretArn:
    Description: "Retrieve your initial admin password from this Secrets Manager secret"
    Value: !Ref AdminPasswordSecret

  RdsEndpoint:
    Description: "RDS endpoint (internal - not publicly accessible)"
    Value: !GetAtt RdsInstance.Endpoint.Address

  EcsClusterName:
    Description: "ECS cluster name"
    Value: !Ref EcsCluster
