import * as cdk from "aws-cdk-lib";
import * as logs from "aws-cdk-lib/aws-logs";
import * as waf from "aws-cdk-lib/aws-wafv2";
import { Construct } from "constructs";

interface ApiWebAclProps {
  stage: string;
  apiName: string;
  rules: waf.CfnWebACL.RuleProperty[];
}

export const BLOCK_HIGH_RATE_ACCESS: waf.CfnWebACL.RuleProperty = {
  priority: 100,
  name: "BLOCK_HIGH_RATE_ACCESS",
  action: { block: {} },
  visibilityConfig: {
    cloudWatchMetricsEnabled: true,
    sampledRequestsEnabled: true,
    metricName: "BLOCK_HIGH_RATE_ACCESS",
  },
  statement: {
    rateBasedStatement: {
      aggregateKeyType: "IP",
      limit: 10000, // aggregateKeyTypeごとに5分間にどれだけ多くのリクエストを許可するかの上限値
    },
  },
} as const;

export const BLOCK_REDOC_WITHOUT_AUTH: waf.CfnWebACL.RuleProperty = {
  priority: 200,
  name: "BLOCK_REDOC_WITHOUT_AUTH",
  action: {
    block: {
      customResponse: {
        responseCode: 401,
        responseHeaders: [
          {
            name: "www-authenticate",
            value: "Basic",
          },
        ],
      },
    },
  },
  visibilityConfig: {
    cloudWatchMetricsEnabled: true,
    sampledRequestsEnabled: true,
    metricName: "BLOCK_REDOC_WITHOUT_AUTH",
  },
  statement: {
    andStatement: {
      statements: [
        {
          notStatement: {
            statement: {
              byteMatchStatement: {
                searchString: "Basic ZGV2OmRldg==", //dev:dev
                fieldToMatch: {
                  singleHeader: {
                    Name: "Authorization",
                  },
                },
                textTransformations: [
                  {
                    priority: 0,
                    type: "NONE",
                  },
                ],
                positionalConstraint: "EXACTLY",
              },
            },
          },
        },
        {
          byteMatchStatement: {
            searchString: "/redoc",
            fieldToMatch: {
              uriPath: {},
            },
            textTransformations: [
              {
                priority: 0,
                type: "NONE",
              },
            ],
            positionalConstraint: "ENDS_WITH",
          },
        },
      ],
    },
  },
};

export const BLOCK_OPENAPI_JSON_WITHOUT_AUTH: waf.CfnWebACL.RuleProperty = {
  priority: 210,
  name: "BLOCK_OPENAPI_JSON_WITHOUT_AUTH",
  action: {
    block: {
      customResponse: {
        responseCode: 401,
        responseHeaders: [
          {
            name: "www-authenticate",
            value: "Basic",
          },
        ],
      },
    },
  },
  visibilityConfig: {
    cloudWatchMetricsEnabled: true,
    sampledRequestsEnabled: true,
    metricName: "BLOCK_OPENAPI_JSON_WITHOUT_AUTH",
  },
  statement: {
    andStatement: {
      statements: [
        {
          notStatement: {
            statement: {
              byteMatchStatement: {
                searchString: "Basic ZGV2OmRldg==",
                fieldToMatch: {
                  singleHeader: {
                    Name: "Authorization",
                  },
                },
                textTransformations: [
                  {
                    priority: 0,
                    type: "NONE",
                  },
                ],
                positionalConstraint: "EXACTLY",
              },
            },
          },
        },
        {
          byteMatchStatement: {
            searchString: "/openapi.json",
            fieldToMatch: {
              uriPath: {},
            },
            textTransformations: [
              {
                priority: 0,
                type: "NONE",
              },
            ],
            positionalConstraint: "ENDS_WITH",
          },
        },
      ],
    },
  },
};

export const ALLOW_ANY_ACCESS_FROM_JP: waf.CfnWebACL.RuleProperty = {
  priority: 300,
  name: "ALLOW_ANY_ACCESS_FROM_JP",
  action: { allow: {} },
  visibilityConfig: {
    cloudWatchMetricsEnabled: true,
    sampledRequestsEnabled: true,
    metricName: "ALLOW_ANY_ACCESS_FROM_JP",
  },
  statement: {
    geoMatchStatement: {
      countryCodes: ["JP"],
    },
  },
} as const;

// GitHub Actionsの標準ランナーが米国リージョンで起動するため、米国リージョンからのアクセスも許可する
export const ALLOW_ANY_ACCESS_FROM_JP_AND_US: waf.CfnWebACL.RuleProperty = {
  priority: 310,
  name: "ALLOW_ANY_ACCESS_FROM_JP_AND_US",
  action: { allow: {} },
  visibilityConfig: {
    cloudWatchMetricsEnabled: true,
    sampledRequestsEnabled: true,
    metricName: "ALLOW_ANY_ACCESS_FROM_JP_AND_US",
  },
  statement: {
    geoMatchStatement: {
      countryCodes: ["JP", "US"],
    },
  },
} as const;

export class ApiWebAcl extends Construct {
  readonly attrArn: string;

  constructor(scope: Construct, id: string, props: ApiWebAclProps) {
    super(scope, id);
    const { stage, apiName, rules } = props;
    const webAcl = new waf.CfnWebACL(this, "ApiWebAcl", {
      name: `${stage}-${apiName}-web-acl`,
      defaultAction: { block: {} },
      rules: rules,
      scope: "REGIONAL",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: `${stage}-${apiName}-web-acl`,
        sampledRequestsEnabled: true,
      },
      associationConfig: {
        requestBody: {
          API_GATEWAY: {
            defaultSizeInspectionLimit: "KB_64",
          },
        },
      },
    });

    const wafLogGroupPrefix = "aws-waf-logs";
    const logGroup = new logs.LogGroup(this, "WebACLLogGroup", {
      logGroupName: `${wafLogGroupPrefix}-${stage}-${apiName}-logs`,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      retention: logs.RetentionDays.ONE_YEAR + logs.RetentionDays.SIX_MONTHS,
    });
    new waf.CfnLoggingConfiguration(this, "WebACLLogConfig", {
      logDestinationConfigs: [logGroup.logGroupArn],
      resourceArn: webAcl.attrArn,
    });

    this.attrArn = webAcl.attrArn;
  }
}
