import * as cdk from "aws-cdk-lib";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import { EndpointType } from "aws-cdk-lib/aws-apigatewayv2";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as sns from "aws-cdk-lib/aws-sns";
import { Queue } from "aws-cdk-lib/aws-sqs";
import * as ssm from "aws-cdk-lib/aws-ssm";
import * as waf from "aws-cdk-lib/aws-wafv2";
import { Construct } from "constructs";
import { Context } from "../lib/context";

import { SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
import { ApiDomain } from "./construct/api-domain";
import { ApiMonitoring } from "./construct/api-monitoring";
import {
  ALLOW_ANY_ACCESS_FROM_JP_AND_US,
  ApiWebAcl,
  BLOCK_HIGH_RATE_ACCESS,
  BLOCK_OPENAPI_JSON_WITHOUT_AUTH,
  BLOCK_REDOC_WITHOUT_AUTH,
} from "./construct/api-web-acl";
import { getLambdaArchitecture } from "./utils";

interface StackProps extends cdk.StackProps {
  readonly stage: string;
  readonly apiProps: Context["apiProps"];
  readonly vpc: ec2.IVpc;
}

export class ClientApiStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const { stage, apiProps, vpc } = props;
    const { allowedApiCallOriginList, lambdaIntegrationTimeout } = apiProps;
    const { region } = new cdk.ScopedAws(this);

    const userPoolJwksEndpointUrl = ssm.StringParameter.fromStringParameterAttributes(this, "HostedZoneIdParameter", {
      parameterName: `/${stage}/user-pool-jwks-endpoint-url`,
      forceDynamicReference: true,
    }).stringValue;

    const dbClusterAppSecretName = ssm.StringParameter.fromStringParameterAttributes(this, "DbClusterAppSecretName", {
      parameterName: `/${stage}/db-app-secret-name`,
      forceDynamicReference: true,
    }).stringValue;

    const dbClusterHost = ssm.StringParameter.fromStringParameterAttributes(this, "DbClusterHost", {
      parameterName: `/${stage}/db-host`,
      forceDynamicReference: true,
    }).stringValue;

    const dbClusterPort = ssm.StringParameter.fromStringParameterAttributes(this, "DbClusterPort", {
      parameterName: `/${stage}/db-port`,
      forceDynamicReference: true,
    }).stringValue;

    const usageEventRequestsBucketName = ssm.StringParameter.fromStringParameterAttributes(
      this,
      "UsageEventRequestsBucketName",
      {
        parameterName: `/${stage}/usage-event-requests-bucket-name`,
        forceDynamicReference: true,
      },
    ).stringValue;

    const deadLetterQueue = new Queue(this, "UsageEventDeadLetterQueue", {
      queueName: `${stage}-usage-event-dead-letter-queue`,
      retentionPeriod: cdk.Duration.days(14),
      visibilityTimeout: cdk.Duration.seconds(70), // DLQ通知lambdaより長く
    });
    const queue = new Queue(this, "UsageEventQueue", {
      queueName: `${stage}-usage-event-queue`,
      visibilityTimeout: cdk.Duration.minutes(16),
      deadLetterQueue: {
        maxReceiveCount: 3,
        queue: deadLetterQueue,
      },
    });

    const role = new iam.Role(this, "ClientApiRole", {
      roleName: `${stage}-client-api-role`,
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
        iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole"),
        iam.ManagedPolicy.fromAwsManagedPolicyName("AWSSecretsManagerClientReadOnlyAccess"),
        iam.ManagedPolicy.fromAwsManagedPolicyName("AWSXrayFullAccess"),
        iam.ManagedPolicy.fromAwsManagedPolicyName("AWSLambda_FullAccess"), // 暫定
      ],
      inlinePolicies: {
        SqsPolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ["sqs:SendMessage"],
              resources: [queue.queueArn],
            }),
          ],
        }),
      },
    });
    const s3AccessRole = new iam.Role(this, "ClientApiS3AccessRole", {
      roleName: `${stage}-client-api-s3-access-role`,
      assumedBy: new iam.SessionTagsPrincipal(role),
      inlinePolicies: {
        S3AccessPolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ["s3:ListBucket"],
              resources: [`arn:aws:s3:::${usageEventRequestsBucketName}`],
              conditions: {
                StringLike: {
                  "s3:prefix": ["/${aws:PrincipalTag/tenant_id}/*"],
                },
              },
            }),
            new iam.PolicyStatement({
              actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
              resources: [`arn:aws:s3:::${usageEventRequestsBucketName}/\${aws:PrincipalTag/tenant_id}/*`],
            }),
          ],
        }),
      },
    });

    const lambdaArchitecture = getLambdaArchitecture();
    const uniqueVersionId = new Date().getTime();
    const usageEventConsumerFunctionName = `${stage}-client-usage-event-consumer-${lambdaArchitecture.name}`;
    const lambdaFunction = new lambda.Function(this, `${lambdaArchitecture.name}Function`, {
      functionName: `${stage}-client-api-python-${lambdaArchitecture.name}`,
      description: `created at ${uniqueVersionId}`, // デプロイの度にversionを更新するため
      code: lambda.Code.fromAsset("../../backend", {
        bundling: {
          image: lambda.Runtime.PYTHON_3_12.bundlingImage,
          command: ["bash", "-c", "pip install -r requirements.txt -t /asset-output && cp -au . /asset-output"],
          user: "root",
          bundlingFileAccess: cdk.BundlingFileAccess.VOLUME_COPY,
        },
      }),
      handler: "src.client_api.handler",
      runtime: lambda.Runtime.PYTHON_3_12,
      architecture: lambdaArchitecture,
      memorySize: 2048,
      timeout: cdk.Duration.minutes(15),
      vpc,
      role: role,
      snapStart: apiProps.enableSnapStart ? lambda.SnapStartConf.ON_PUBLISHED_VERSIONS : undefined,
      environment: {
        STAGE: stage,
        SERVICE: "client_api",
        APPLICATION_LOG_LEVEL: apiProps.applicationLogLevel,
        USER_POOL_JWKS_ENDPOINT_URL: userPoolJwksEndpointUrl,
        POSTGRES_SECRET_NAME: dbClusterAppSecretName,
        POSTGRES_DB: "postgres",
        POSTGRES_HOST: dbClusterHost,
        POSTGRES_PORT: dbClusterPort,
        S3_ROLE_ARN: s3AccessRole.roleArn,
        USAGE_EVENT_CONSUMER_FUNCTION_NAME: usageEventConsumerFunctionName,
        USAGE_EVENT_STORAGE_NAME: usageEventRequestsBucketName,
        USAGE_EVENT_QUEUE_URL: queue.queueUrl,
      },
      logGroup: new LogGroup(this, "ApiLogGroup", {
        retention: RetentionDays.ONE_YEAR + RetentionDays.SIX_MONTHS,
        removalPolicy: cdk.RemovalPolicy.DESTROY,
      }),
      tracing: apiProps.enableXray ? lambda.Tracing.ACTIVE : lambda.Tracing.DISABLED,
    });
    const lambdaVersion = new lambda.Version(this, `LambdaVersion${uniqueVersionId}`, {
      lambda: lambdaFunction,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    }); // snap startのためにversionを作成

    const accessLogGroup = new LogGroup(this, "RestApiLogGroup", {
      retention: RetentionDays.ONE_YEAR + RetentionDays.SIX_MONTHS,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    const restApi = new apigateway.LambdaRestApi(this, "RestApi", {
      restApiName: `${stage}-client-api`,
      handler: lambdaVersion,
      integrationOptions: {
        timeout: cdk.Duration.seconds(lambdaIntegrationTimeout),
      },
      deploy: true,
      endpointTypes: [EndpointType.REGIONAL],
      cloudWatchRole: true,
      cloudWatchRoleRemovalPolicy: cdk.RemovalPolicy.DESTROY,
      deployOptions: {
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
        accessLogDestination: new apigateway.LogGroupLogDestination(accessLogGroup),
        accessLogFormat: apigateway.AccessLogFormat.custom(
          JSON.stringify({
            requestId: "$context.requestId",
            ip: "$context.identity.sourceIp",
            httpMethod: "$context.httpMethod",
            path: "$context.path",
            status: "$context.status",
            responseLatency: "$context.responseLatency",
            integrationStatus: "$context.integrationStatus",
            integrationLatency: "$context.integrationLatency",
            requestTime: "$context.requestTime",
            errorMessage: "$context.error.message",
            userAgent: "$context.identity.userAgent",
            xrayTraceId: "$context.xrayTraceId",
          }),
        ),
        tracingEnabled: apiProps.enableXray,
      },
      defaultCorsPreflightOptions: {
        allowOrigins: allowedApiCallOriginList,
        allowMethods: apigateway.Cors.ALL_METHODS,
      },
    });
    restApi.addGatewayResponse(`Gwr4xx`, {
      type: apigateway.ResponseType.DEFAULT_4XX,
      responseHeaders: {
        "Access-Control-Allow-Origin": allowedApiCallOriginList.map((o) => `'${o}'`).join(","),
      },
    });
    restApi.addGatewayResponse(`Gwr5xx`, {
      type: apigateway.ResponseType.DEFAULT_5XX,
      responseHeaders: {
        "Access-Control-Allow-Origin": allowedApiCallOriginList.map((o) => `'${o}'`).join(","),
      },
    });
    new ssm.StringParameter(this, "ApiArnParameter", {
      parameterName: `/${stage}/client-api-arn`,
      stringValue: `arn:aws:apigateway:${region}::/restapis/${restApi.restApiId}/stages/${restApi.deploymentStage.stageName}`,
    });

    // WAF
    const webAcl = new ApiWebAcl(this, "ApiWebAcl", {
      stage,
      apiName: "client-api",
      rules: [
        BLOCK_HIGH_RATE_ACCESS,
        BLOCK_REDOC_WITHOUT_AUTH,
        BLOCK_OPENAPI_JSON_WITHOUT_AUTH,
        ALLOW_ANY_ACCESS_FROM_JP_AND_US,
      ],
    });
    new waf.CfnWebACLAssociation(this, "WebACLAssociation", {
      resourceArn: `arn:aws:apigateway:${region}::/restapis/${restApi.restApiId}/stages/${restApi.deploymentStage.stageName}`,
      webAclArn: webAcl.attrArn,
    });

    // Custom Domain
    const hostedZoneId = ssm.StringParameter.valueForStringParameter(this, `/${stage}/hosted-zone-id`);
    const hostedZoneName = ssm.StringParameter.valueForStringParameter(this, `/${stage}/hosted-zone-name`);

    const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, "Zone", {
      hostedZoneId,
      zoneName: hostedZoneName,
    });
    new ApiDomain(this, "ApiDomain", {
      stage,
      domainName: hostedZone.zoneName,
      hostedZone,
      restApi,
    });

    if (apiProps.enableMonitoring) {
      new ApiMonitoring(this, "ApiMonitoring", {
        stage,
        apiName: "client-api",
        accessLogGroup,
      });
    }

    const usageEventConsumerFunction = new lambda.Function(
      this,
      `ClientUsageEventConsumer${lambdaArchitecture.name}Function`,
      {
        functionName: usageEventConsumerFunctionName,
        code: lambda.Code.fromAsset("../../backend", {
          bundling: {
            image: lambda.Runtime.PYTHON_3_12.bundlingImage,
            command: ["bash", "-c", "pip install -r requirements.txt -t /asset-output && cp -au . /asset-output"],
            user: "root",
            bundlingFileAccess: cdk.BundlingFileAccess.VOLUME_COPY,
          },
        }),
        handler: "src.usage_event_consumer.handler",
        runtime: lambda.Runtime.PYTHON_3_12,
        architecture: lambdaArchitecture,
        memorySize: 1024,
        timeout: cdk.Duration.minutes(15),
        reservedConcurrentExecutions: apiProps.consumerConcurrencyLimit,
        vpc,
        role: role,
        environment: {
          STAGE: stage,
          SERVICE: "client_usage_event_consumer",
          APPLICATION_LOG_LEVEL: apiProps.applicationLogLevel,
          USER_POOL_JWKS_ENDPOINT_URL: userPoolJwksEndpointUrl,
          POSTGRES_SECRET_NAME: dbClusterAppSecretName,
          POSTGRES_DB: "postgres",
          POSTGRES_HOST: dbClusterHost,
          POSTGRES_PORT: dbClusterPort,
          S3_ROLE_ARN: s3AccessRole.roleArn,
          USAGE_EVENT_STORAGE_NAME: usageEventRequestsBucketName,
        },
        logGroup: new LogGroup(this, "UsageEventConsumerLogGroup", {
          retention: RetentionDays.ONE_YEAR + RetentionDays.SIX_MONTHS,
          removalPolicy: cdk.RemovalPolicy.DESTROY,
        }),
        tracing: apiProps.enableXray ? lambda.Tracing.ACTIVE : lambda.Tracing.DISABLED,
      },
    );

    usageEventConsumerFunction.addEventSource(
      new SqsEventSource(queue, {
        batchSize: 1,
        maxBatchingWindow: cdk.Duration.seconds(1),
        reportBatchItemFailures: true,
      }),
    );

    if (apiProps.enableDlqAlarm) {
      const stageAlertTopicArn = ssm.StringParameter.valueForStringParameter(this, "/account/stage-alert-topic-arn");
      const stageAlertTopic = sns.Topic.fromTopicArn(this, "StageAlertTopic", stageAlertTopicArn);

      const dlqHandlerFunction = new lambda.Function(this, `ClientUsageEventDLQ${lambdaArchitecture.name}Function`, {
        functionName: `${stage}-dlq-handler-${lambdaArchitecture.name}`,
        code: lambda.Code.fromAsset("lambda/dlq_handler", {
          bundling: {
            image: lambda.Runtime.PYTHON_3_12.bundlingImage,
            command: ["bash", "-c", "pip install -r requirements.txt -t /asset-output && cp -au . /asset-output"],
            user: "root",
            bundlingFileAccess: cdk.BundlingFileAccess.VOLUME_COPY,
          },
        }),
        handler: "index.handler",
        runtime: lambda.Runtime.PYTHON_3_12,
        architecture: lambdaArchitecture,
        memorySize: 128,
        timeout: cdk.Duration.minutes(1),
        vpc,
        role: role,
        environment: {
          STAGE: stage,
          APPLICATION_LOG_LEVEL: apiProps.applicationLogLevel,
          QUEUE_LAMBDA_NAME: usageEventConsumerFunction.functionName,
          SNS_TOPIC_ARN: stageAlertTopic.topicArn,
        },
        logGroup: new LogGroup(this, "UsageEventDLQLogGroup", {
          retention: RetentionDays.ONE_YEAR + RetentionDays.SIX_MONTHS,
          removalPolicy: cdk.RemovalPolicy.DESTROY,
        }),
      });
      stageAlertTopic.grantPublish(dlqHandlerFunction);
      dlqHandlerFunction.addEventSource(
        new SqsEventSource(deadLetterQueue, {
          batchSize: 10,
          maxBatchingWindow: cdk.Duration.seconds(5),
        }),
      );
    }
  }
}
