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 * 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 { CONTRACT_API_SUB_DOMAIN } from "./const";
import { ApiDomain } from "./construct/api-domain";
import { ApiMonitoring } from "./construct/api-monitoring";
import {
  ALLOW_ANY_ACCESS_FROM_JP,
  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 ContractApiStack 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 { accountId } = new cdk.ScopedAws(this);

    const userPoolJwksEndpointUrl = ssm.StringParameter.fromStringParameterAttributes(this, "HostedZoneIdParameter", {
      parameterName: `/${stage}/contract-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;

    // Management APIのTenant発行時に利用
    const managementUserPoolId = ssm.StringParameter.fromStringParameterAttributes(
      this,
      "ManagementUserPoolIdParameter",
      {
        parameterName: `/${stage}/user-pool-id`,
        forceDynamicReference: true,
      },
    ).stringValue;

    const role = new iam.Role(this, "ContractApiRole", {
      roleName: `${stage}-contract-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"),
      ],
    });

    role.addToPolicy(
      new iam.PolicyStatement({
        actions: ["cognito-idp:CreateUserPoolClient", "cognito-idp:DeleteUserPoolClient"],
        resources: [`arn:aws:cognito-idp:${region}:${accountId}:userpool/${managementUserPoolId}`],
      }),
    );

    const lambdaArchitecture = getLambdaArchitecture();
    const uniqueVersionId = new Date().getTime();
    const lambdaFunction = new lambda.Function(this, `${lambdaArchitecture.name}Function`, {
      functionName: `${stage}-contract-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.contract_api.handler",
      runtime: lambda.Runtime.PYTHON_3_12,
      architecture: lambdaArchitecture,
      memorySize: 1024,
      timeout: cdk.Duration.minutes(15),
      vpc,
      role: role,
      snapStart: apiProps.enableSnapStart ? lambda.SnapStartConf.ON_PUBLISHED_VERSIONS : undefined,
      environment: {
        STAGE: stage,
        SERVICE: "contract_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,
        USER_POOL_ID: managementUserPoolId,
      },
      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}-contract-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}/contract-api-arn`,
      stringValue: `arn:aws:apigateway:${region}::/restapis/${restApi.restApiId}/stages/${restApi.deploymentStage.stageName}`,
    });

    // WAF
    const webAcl = new ApiWebAcl(this, "ApiWebAcl", {
      stage,
      apiName: "contract-api",
      rules: [
        BLOCK_HIGH_RATE_ACCESS,
        BLOCK_REDOC_WITHOUT_AUTH,
        BLOCK_OPENAPI_JSON_WITHOUT_AUTH,
        ALLOW_ANY_ACCESS_FROM_JP,
      ],
    });
    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: `${CONTRACT_API_SUB_DOMAIN}.${hostedZone.zoneName}`,
      hostedZone,
      restApi,
    });

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

    // Contract Event Alert
    if (apiProps.enableContractEventAlert) {
      const contractEventAlertTopicArn = ssm.StringParameter.valueForStringParameter(
        this,
        "/account/contract-event-alert-topic-arn",
      );
      const contractEventAlertTopic = sns.Topic.fromTopicArn(
        this,
        "ContractEventAlertTopic",
        contractEventAlertTopicArn,
      );
      lambdaFunction.addEnvironment("SNS_TOPIC_ARN", contractEventAlertTopic.topicArn);

      contractEventAlertTopic.grantPublish(lambdaFunction);
    }
  }
}
