AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  AccessLogStack:
    Description: 'Optional stack name of s3 stack to store access logs.'
    Type: String
    Default: ''
  WAFStack:
    Description: 'Optional stack name of WAF stack.'
    Type: String
    Default: ''
  DomainName:
    Description: 'Optional domain.'
    Type: String
    Default: ''
  SubDomainNameWithDot:
    Description: 'Primary name that is used to create the DNS entry with trailing dot. Leave blank for naked (or apex and bare) domain.'
    Type: String
    Default: ''
  RedirectSubDomainNameWithDot:
    Description: 'Optional sub domain name redirecting to DomainName (e.g. www.).'
    Type: String
    Default: ''
  DefaultRootObject:
    Description: 'Optional name of the index document for the website (e.g., index.html).'
    Type: String
    Default: 'index.html'
  DefaultErrorObject:
    Description: 'Optional name of the error document for the website (e.g. error.html).'
    Type: String
    Default: ''
  DefaultErrorResponseCode:
    Description: 'The HTTP status code that you want to return along with the error page (requires DefaultErrorObject).'
    Type: String
    Default: '404'
    AllowedValues: ['200', '404']
  ContentSecurityPolicy:
    Description: 'Optional content-security-policy to be served with the site. Defaults to HTTPS only.'
    Type: String
    Default: 'default-src https:; script-src https:; style-src https:'
  ExistingCertificate:
    Description: 'Optional ACM Certificate ARN.'
    Type: String
    Default: ''
  IncludeCloudFrontAlias:
    Description: 'Whether to include the domain as an alias for the CloudFront distribution. If an existing distribution already has the Alias, we need to delete it first.'
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
  LambdaBucket:
    Description: 'Bucket for deploying lambdas.'
    Type: String
    Default: ''
  DefaultCachePolicyArn:
    Description: 'Default cache policy ARN.'
    Type: String
    Default: ''
  ImageCachePolicyArn:
    Description: 'Image cache policy ARN.'
    Type: String
    Default: ''
  StaticCachePolicyArn:
    Description: 'Static cache policy ARN.'
    Type: String
    Default: ''
  ImageOriginRequestPolicyArn:
    Description: 'Image origin request policy ARN.'
    Type: String
    Default: ''
  IsNextJs:
    Description: 'Whether this is a Next.js deployment.'
    Type: String
    Default: 'false'
    AllowedValues: ['true', 'false']
  NextJsDefaultLambdaKey:
    Description: 'Next.js default lambda key.'
    Type: String
    Default: ''
  NextJsImageLambdaKey:
    Description: 'Next.js image lambda key.'
    Type: String
    Default: ''
  NextJsApiLambdaKey:
    Description: 'Next.js api lambda key.'
    Type: String
    Default: ''
  NextJsRegenerationLambdaKey:
    Description: 'Next.js regeneration lambda key.'
    Type: String
    Default: ''
  NextJsImagePath:
    Description: 'Next.js image path.'
    Type: String
    Default: ''
  NextJsDataPath:
    Description: 'Next.js data path.'
    Type: String
    Default: ''
  NextJsBasePath:
    Description: 'Next.js base path.'
    Type: String
    Default: ''
  NextJsStaticPath:
    Description: 'Next.js static path.'
    Type: String
    Default: ''
  NextJsApiPath:
    Description: 'Next.js api path.'
    Type: String
    Default: ''
  LogsRetentionInDays:
    Description: 'Specifies the number of days you want to retain log events in the specified log group.'
    Type: Number
    Default: 14
    AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]

Conditions:
  HasDomain: !Not [!Equals [!Ref DomainName, '']]
  IsNextJs: !Equals [!Ref IsNextJs, 'true']
  HasDomainOrIsNextJs: !Or [!Condition HasDomain, !Condition IsNextJs]
  NoDomain: !Not [!Condition HasDomainOrIsNextJs]
  HasRedirectDomainName: !Not [!Equals [!Ref RedirectSubDomainNameWithDot, '']]
  HasBucket: !Not [!Equals [!Ref AccessLogStack, '']]
  HasWAF: !Not [!Equals [!Ref WAFStack, '']]
  HasDefaultRootObject: !Not [!Equals [!Ref DefaultRootObject, '']]
  HasDefaultErrorObject: !Not [!Equals [!Ref DefaultErrorObject, '']]
  HasExistingCertificate: !Not [!Equals [!Ref ExistingCertificate, '']]
  ShouldIncludeCloudFrontAlias: !Equals [!Ref IncludeCloudFrontAlias, 'true']
  HasNextJsImagePath: !Not [!Equals [!Ref NextJsImagePath, '']]
  HasNextJsDataPath: !Not [!Equals [!Ref NextJsDataPath, '']]
  HasNextJsBasePath: !Not [!Equals [!Ref NextJsBasePath, '']]
  HasNextJsStaticPath: !Not [!Equals [!Ref NextJsStaticPath, '']]
  HasNextJsApiPath: !Not [!Equals [!Ref NextJsApiPath, '']]
  HasNextJsRegenerationLambda: !Not [!Equals [!Ref NextJsRegenerationLambdaKey, '']]

Resources:
  Bucket:
    Type: 'AWS::S3::Bucket'
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Properties:
      AccelerateConfiguration:
        AccelerationStatus: Enabled
      WebsiteConfiguration:
        !If [
          NoDomain,
          {
            IndexDocument: !If [HasDefaultRootObject, !Ref DefaultRootObject, !Ref 'AWS::NoValue'],
            ErrorDocument:
              !If [HasDefaultErrorObject, !Ref DefaultErrorObject, !Ref 'AWS::NoValue'],
          },
          !Ref 'AWS::NoValue',
        ]
  BucketPolicyPublic:
    Condition: NoDomain
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref Bucket
      PolicyDocument:
        Statement:
          - Action: 's3:GetObject'
            Effect: Allow
            Resource: !Sub 'arn:aws:s3:::${Bucket}/*'
            Principal: '*'
  BucketPolicy:
    Condition: HasDomainOrIsNextJs
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref Bucket
      PolicyDocument:
        Statement:
          - Action: 's3:GetObject'
            Effect: Allow
            Resource: !Sub 'arn:aws:s3:::${Bucket}/*'
            Principal:
              CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
          - Sid: AllowSSLRequestsOnly # AWS Foundational Security Best Practices v1.0.0 S3.5
            Effect: Deny
            Principal: '*'
            Action: 's3:*'
            Resource:
              - !GetAtt 'Bucket.Arn'
              - !Sub '${Bucket.Arn}/*'
            Condition:
              Bool:
                'aws:SecureTransport': false
  NextJsDefaultLambdaRole:
    Condition: IsNextJs
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
                - edgelambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: S3Policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject*
                  - s3:GetBucket*
                  - s3:List*
                  - s3:DeleteObject*
                  - s3:PutObject*
                  - s3:Abort*
                Resource: !Sub 'arn:aws:s3:::${Bucket}/*'
        - PolicyName: SQSPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sqs:SendMessage
                  - sqs:GetQueueAttributes
                  - sqs:GetQueueUrl
                Resource:
                  - !If
                    - HasNextJsRegenerationLambda
                    - !GetAtt NextJsRegenerationQueue.Arn
                    - '*'
        - PolicyName: InvokePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource:
                  - !If
                    - HasNextJsRegenerationLambda
                    - !GetAtt NextJsRegenerationLambda.Arn
                    - '*'
  NextJsDefaultLambda:
    Condition: IsNextJs
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref LambdaBucket
        S3Key: !Ref NextJsDefaultLambdaKey
      Role: !GetAtt NextJsDefaultLambdaRole.Arn
      Description: Default Lambda@Edge function for Next.js
      Handler: index.handler
      Runtime: nodejs12.x
      Timeout: 10
      MemorySize: 512
    DependsOn:
      - NextJsDefaultLambdaRole
  NextJsDefaultLambdaCurrentVersion:
    Condition: IsNextJs
    Type: AWS::Lambda::Version
    Properties:
      FunctionName: !Ref NextJsDefaultLambda
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  NextJsImageLambdaRole:
    Condition: HasNextJsImagePath
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
                - edgelambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: S3Policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                Resource: !Sub 'arn:aws:s3:::${Bucket}/*'
  NextJsImageLambda:
    Condition: HasNextJsImagePath
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref LambdaBucket
        S3Key: !Ref NextJsImageLambdaKey
      Role: !GetAtt NextJsImageLambdaRole.Arn
      Description: Image Lambda@Edge function for Next.js
      Handler: index.handler
      Runtime: nodejs12.x
      Timeout: 10
      MemorySize: 512
    DependsOn:
      - NextJsImageLambdaRole
  NextJsImageLambdaCurrentVersion:
    Condition: HasNextJsImagePath
    Type: AWS::Lambda::Version
    Properties:
      FunctionName: !Ref NextJsImageLambda
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  NextJsImageLambdaCurrentVersionEventInvokeConfig:
    Condition: HasNextJsImagePath
    Type: AWS::Lambda::EventInvokeConfig
    Properties:
      FunctionName: !Ref NextJsImageLambda
      Qualifier: !GetAtt NextJsImageLambdaCurrentVersion.Version
      MaximumRetryAttempts: 1
  NextJsApiLambdaRole:
    Condition: HasNextJsApiPath
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
                - edgelambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: S3Policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                Resource: !Sub 'arn:aws:s3:::${Bucket}/*'
  NextJsApiLambda:
    Condition: HasNextJsApiPath
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref LambdaBucket
        S3Key: !Ref NextJsApiLambdaKey
      Role: !GetAtt NextJsApiLambdaRole.Arn
      Description: API Lambda@Edge function for Next.js
      Handler: index.handler
      Runtime: nodejs12.x
      Timeout: 10
      MemorySize: 512
    DependsOn:
      - NextJsApiLambdaRole
  NextJsApiLambdaCurrentVersion:
    Condition: HasNextJsApiPath
    Type: AWS::Lambda::Version
    Properties:
      FunctionName: !Ref NextJsApiLambda
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  NextJsApiLambdaCurrentVersionEventInvokeConfig:
    Condition: HasNextJsApiPath
    Type: AWS::Lambda::EventInvokeConfig
    Properties:
      FunctionName: !Ref NextJsApiLambda
      Qualifier: !GetAtt NextJsApiLambdaCurrentVersion.Version
      MaximumRetryAttempts: 1
  NextJsRegenerationQueue:
    Condition: HasNextJsRegenerationLambda
    Type: AWS::SQS::Queue
    DeletionPolicy: Delete
    Properties:
      QueueName: !Sub '${Bucket}.fifo'
      FifoQueue: true
  NextJsRegenerationLambdaRole:
    Condition: HasNextJsRegenerationLambda
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: S3Policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject*
                  - s3:GetBucket*
                  - s3:List*
                  - s3:DeleteObject*
                  - s3:PutObject*
                  - s3:Abort*
                Resource:
                  - !Sub 'arn:aws:s3:::${Bucket}'
                  - !Sub 'arn:aws:s3:::${Bucket}/*'
        - PolicyName: SQSPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sqs:ReceiveMessage
                  - sqs:ChangeMessageVisibility
                  - sqs:GetQueueUrl
                  - sqs:DeleteMessage
                  - sqs:GetQueueAttributes
                Resource: !GetAtt NextJsRegenerationQueue.Arn
  NextJsRegenerationLambda:
    Condition: HasNextJsRegenerationLambda
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref LambdaBucket
        S3Key: !Ref NextJsRegenerationLambdaKey
      Role: !GetAtt NextJsRegenerationLambdaRole.Arn
      Description: Regeneration Lambda function for Next.js
      Handler: index.handler
      Runtime: nodejs14.x
      Timeout: 30
      MemorySize: 512
    DependsOn:
      - NextJsRegenerationLambdaRole
  NextJsRegenerationLambdaCurrentVersion:
    Condition: HasNextJsRegenerationLambda
    Type: AWS::Lambda::Version
    Properties:
      FunctionName: !Ref NextJsRegenerationLambda
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  NextJsRegenerationLambdaEventSourceMapping:
    Condition: HasNextJsRegenerationLambda
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      Enabled: true
      EventSourceArn: !GetAtt NextJsRegenerationQueue.Arn
      FunctionName: !GetAtt NextJsRegenerationLambda.Arn
  ViewerRequestRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - 'lambda.amazonaws.com'
                - 'edgelambda.amazonaws.com'
            Action: 'sts:AssumeRole'
  ViewerRequestLambdaPolicy:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'
            Resource: !GetAtt 'ViewerRequestLogGroup.Arn'
      PolicyName: lambda
      Roles:
        - !Ref ViewerRequestRole
  ViewerRequestLambdaEdgePolicy:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action: 'logs:CreateLogGroup'
            Resource: !Sub 'arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:/aws/lambda/us-east-1.${ViewerRequestFunction}:log-stream:'
          - Effect: Allow
            Action:
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'
            Resource: !Sub 'arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:/aws/lambda/us-east-1.${ViewerRequestFunction}:log-stream:*'
      PolicyName: 'lambda-edge'
      Roles:
        - !Ref ViewerRequestRole
  ViewerRequestFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: !Sub |
          const regex = /\.[A-Za-z0-9]+$/;
          const indexDocument = '${DefaultRootObject}';
          const domainName = '${DomainName}'.toLowerCase();
          const redirectSubDomainNameWithDot = '${RedirectSubDomainNameWithDot}'.toLowerCase();
          const redirectDomainName = redirectSubDomainNameWithDot ? `${!redirectSubDomainNameWithDot}${!domainName}` : '';

          exports.handler = async function(event) {
            const cf = event.Records[0].cf;

            if (cf.request.headers.host[0].value.toLowerCase() === redirectDomainName) {
              return {
                status: '301',
                statusDescription: 'Moved Permanently',
                headers: {
                  location: [{
                    key: 'Location',
                    value: `https://${!domainName}${!cf.request.uri}`,
                  }],
                }
              };
            }

            if (cf.request.uri.endsWith('/')) {
              return Object.assign({}, cf.request, {uri: `${!cf.request.uri}${!indexDocument}`});
            }

            if (cf.request.uri.endsWith(`/${!indexDocument}`)) {
              return {
                status: '302',
                statusDescription: 'Found',
                headers: {
                  location: [{
                    key: 'Location',
                    value: cf.request.uri.substr(0, cf.request.uri.length - indexDocument.length),
                  }],
                }
              };
            }

            if (!regex.test(cf.request.uri)) {
              return {
                status: '302',
                statusDescription: 'Found',
                headers: {
                  location: [{
                    key: 'Location',
                    value: `${!cf.request.uri}/`,
                  }],
                }
              };
            }

            return cf.request;
          };
      Handler: 'index.handler'
      MemorySize: 128
      Role: !GetAtt 'ViewerRequestRole.Arn'
      Runtime: 'nodejs12.x'
      Timeout: 5
  ViewerRequestVersion:
    Type: 'AWS::Lambda::Version'
    Properties:
      FunctionName: !Ref ViewerRequestFunction
  ViewerRequestLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub '/aws/lambda/${ViewerRequestFunction}'
      RetentionInDays: !Ref LogsRetentionInDays
  ViewerResponseFunction:
    Type: 'AWS::CloudFront::Function'
    Properties:
      AutoPublish: true
      Name: !Sub '${AWS::StackName}-CfFunction'
      FunctionConfig:
        Comment: CloudFront Function to add security headers to objects
        Runtime: cloudfront-js-1.0
      FunctionCode: !Sub |
        function handler(event) {
            var response = event.response;
            var headers = response.headers;

            // Set HTTP security headers
            // Since JavaScript doesn't allow for hyphens in variable names, we use the dict["key"] notation
            headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'};
            headers['content-security-policy'] = { value: "${ContentSecurityPolicy}"};
            headers['x-content-type-options'] = { value: 'nosniff'};
            headers['x-frame-options'] = {value: 'DENY'};
            headers['x-xss-protection'] = {value: '1; mode=block'};
            headers['referrer-policy'] = {value: 'same-origin'};

            // Return the response to viewers
            return response;
        }
  CloudFrontOriginAccessIdentity:
    Condition: HasDomainOrIsNextJs
    Type: 'AWS::CloudFront::CloudFrontOriginAccessIdentity'
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Sub
          - '${SubDomainNameWithDot}${DomainName}'
          - SubDomainNameWithDot: !Ref SubDomainNameWithDot
            DomainName: !Ref DomainName
  CloudFrontDistribution:
    Condition: HasDomainOrIsNextJs
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        Aliases: !If
          - ShouldIncludeCloudFrontAlias
          - !If
            - HasRedirectDomainName
            - - !Sub
                - '${SubDomainNameWithDot}${DomainName}'
                - SubDomainNameWithDot: !Ref SubDomainNameWithDot
                  DomainName: !Ref DomainName
              - !Sub
                - '${RedirectSubDomainNameWithDot}${DomainName}'
                - RedirectSubDomainNameWithDot: !Ref RedirectSubDomainNameWithDot
                  DomainName: !Ref DomainName
            - - !Sub
                - '${SubDomainNameWithDot}${DomainName}'
                - SubDomainNameWithDot: !Ref SubDomainNameWithDot
                  DomainName: !Ref DomainName
          - !Ref 'AWS::NoValue'
        Comment: Created by Ness
        CustomErrorResponses: !If
          - HasDefaultErrorObject
          - - ErrorCode: 403 # 403 from S3 indicates that the file does not exists
              ResponseCode: !Ref DefaultErrorResponseCode
              ResponsePagePath: !Sub
                - '/${DefaultErrorObject}'
                - DefaultErrorObject: !Ref DefaultErrorObject
          - []
        CacheBehaviors:
          - !If
            - HasNextJsImagePath
            - AllowedMethods:
                - GET
                - HEAD
                - OPTIONS
                - PUT
                - PATCH
                - POST
                - DELETE
              CachePolicyId: !Ref ImageCachePolicyArn
              CachedMethods:
                - GET
                - HEAD
                - OPTIONS
              Compress: true
              LambdaFunctionAssociations:
                - EventType: origin-request
                  LambdaFunctionARN: !Ref NextJsImageLambdaCurrentVersion
              OriginRequestPolicyId: !Ref ImageOriginRequestPolicyArn
              PathPattern: !Ref NextJsImagePath
              TargetOriginId: s3origin
              ViewerProtocolPolicy: redirect-to-https
            - !Ref AWS::NoValue
          - !If
            - HasNextJsDataPath
            - AllowedMethods:
                - GET
                - HEAD
                - OPTIONS
              CachePolicyId: !Ref DefaultCachePolicyArn
              CachedMethods:
                - GET
                - HEAD
                - OPTIONS
              Compress: true
              LambdaFunctionAssociations:
                - EventType: origin-request
                  IncludeBody: true
                  LambdaFunctionARN: !Ref NextJsDefaultLambdaCurrentVersion
                - EventType: origin-response
                  LambdaFunctionARN: !Ref NextJsDefaultLambdaCurrentVersion
              PathPattern: !Ref NextJsDataPath
              TargetOriginId: s3origin
              ViewerProtocolPolicy: redirect-to-https
            - !Ref AWS::NoValue
          - !If
            - HasNextJsBasePath
            - AllowedMethods:
                - GET
                - HEAD
                - OPTIONS
              CachePolicyId: !Ref StaticCachePolicyArn
              CachedMethods:
                - GET
                - HEAD
                - OPTIONS
              Compress: true
              PathPattern: !Ref NextJsBasePath
              TargetOriginId: s3origin
              ViewerProtocolPolicy: redirect-to-https
            - !Ref AWS::NoValue
          - !If
            - HasNextJsStaticPath
            - AllowedMethods:
                - GET
                - HEAD
                - OPTIONS
              CachePolicyId: !Ref StaticCachePolicyArn
              CachedMethods:
                - GET
                - HEAD
                - OPTIONS
              Compress: true
              PathPattern: !Ref NextJsStaticPath
              TargetOriginId: s3origin
              ViewerProtocolPolicy: redirect-to-https
            - !Ref AWS::NoValue
          - !If
            - HasNextJsApiPath
            - AllowedMethods:
                - GET
                - HEAD
                - OPTIONS
                - PUT
                - PATCH
                - POST
                - DELETE
              CachePolicyId: !Ref DefaultCachePolicyArn
              CachedMethods:
                - GET
                - HEAD
                - OPTIONS
              Compress: true
              LambdaFunctionAssociations:
                - EventType: origin-request
                  IncludeBody: true
                  LambdaFunctionARN: !Ref NextJsApiLambdaCurrentVersion
              PathPattern: !Ref NextJsApiPath
              TargetOriginId: s3origin
              ViewerProtocolPolicy: redirect-to-https
            - !Ref AWS::NoValue
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
          CachePolicyId: !Ref DefaultCachePolicyArn
          CachedMethods:
            - GET
            - HEAD
            - OPTIONS
          Compress: true
          LambdaFunctionAssociations: !If
            - IsNextJs
            - - EventType: origin-request
                IncludeBody: true
                LambdaFunctionARN: !Ref NextJsDefaultLambdaCurrentVersion
              - EventType: origin-response
                LambdaFunctionARN: !Ref NextJsDefaultLambdaCurrentVersion
            - - EventType: viewer-request
                LambdaFunctionARN: !Ref ViewerRequestVersion
          FunctionAssociations:
            - EventType: viewer-response
              FunctionARN: !GetAtt ViewerResponseFunction.FunctionMetadata.FunctionARN
          TargetOriginId: s3origin
          ViewerProtocolPolicy: redirect-to-https
        DefaultRootObject: !If [HasDefaultRootObject, !Ref DefaultRootObject, !Ref 'AWS::NoValue']
        Enabled: true
        HttpVersion: http2
        IPV6Enabled: true
        Logging:
          !If [
            HasBucket,
            {
              Bucket: {'Fn::ImportValue': !Sub '${AccessLogStack}-BucketDomainName'},
              Prefix: !Ref 'AWS::StackName',
            },
            !Ref 'AWS::NoValue',
          ]
        Origins:
          - DomainName: !GetAtt 'Bucket.RegionalDomainName'
            Id: s3origin
            S3OriginConfig:
              OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}'
        PriceClass: 'PriceClass_All'
        ViewerCertificate:
          !If [
            HasExistingCertificate,
            {
              AcmCertificateArn: !Ref ExistingCertificate,
              MinimumProtocolVersion: 'TLSv1.1_2016',
              SslSupportMethod: 'sni-only',
            },
            !Ref 'AWS::NoValue',
          ]
        WebACLId: !If
          - HasWAF
          - {'Fn::ImportValue': !Sub '${WAFStack}-WebACL'}
          - !Ref 'AWS::NoValue'

Outputs:
  StackName:
    Description: 'Stack name.'
    Value: !Sub '${AWS::StackName}'
  BucketName:
    Description: 'Name of the S3 bucket storing the static files.'
    Value: !Ref Bucket
    Export:
      Name: !Sub '${AWS::StackName}-BucketName'
  URL:
    Description: 'URL to static website.'
    Value: !If
      - HasDomain
      - !Sub
        - 'https://${SubDomainNameWithDot}${DomainName}'
        - SubDomainNameWithDot: !Ref SubDomainNameWithDot
          DomainName: !Ref DomainName
      - !If
        - IsNextJs
        - !Sub
          - 'https://${DomainName}'
          - DomainName: !GetAtt 'CloudFrontDistribution.DomainName'
        - !GetAtt 'Bucket.WebsiteURL'
    Export:
      Name: !Sub '${AWS::StackName}-URL'
  DistributionId:
    Condition: HasDomainOrIsNextJs
    Description: 'CloudFront distribution ID'
    Value: !Ref CloudFrontDistribution
    Export:
      Name: !Sub '${AWS::StackName}-DistributionId'
  DistributionDomainName:
    Condition: HasDomainOrIsNextJs
    Description: 'CloudFront distribution domain name'
    Value: !GetAtt 'CloudFrontDistribution.DomainName'
    Export:
      Name: !Sub '${AWS::StackName}-DistributionDomainName'
