import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as cognito from "aws-cdk-lib/aws-cognito";
import * as apig from "@aws-cdk/aws-apigatewayv2-alpha";
import * as apigAuthorizers from "@aws-cdk/aws-apigatewayv2-authorizers-alpha";
import * as apigIntegrations from "@aws-cdk/aws-apigatewayv2-integrations-alpha";
import { Stack } from "./Stack.js";
import { getFunctionRef, isCDKConstruct } from "./Construct.js";
import { Function as Fn, } from "./Function.js";
import { toCdkDuration } from "./util/duration.js";
import * as apigV2Cors from "./util/apiGatewayV2Cors.js";
import * as apigV2Domain from "./util/apiGatewayV2Domain.js";
import * as apigV2AccessLog from "./util/apiGatewayV2AccessLog.js";
const PayloadFormatVersions = ["1.0", "2.0"];
/////////////////////
// Construct
/////////////////////
/**
 * The Api construct is a higher level CDK construct that makes it easy to create an API.
 *
 * @example
 *
 * ```ts
 * import { Api } from "@serverless-stack/resources";
 *
 * new Api(stack, "Api", {
 *   routes: {
 *     "GET    /notes": "src/list.main",
 *     "POST   /notes": "src/create.main",
 *     "GET    /notes/{id}": "src/get.main",
 *     "PUT    /notes/{id}": "src/update.main",
 *     "DELETE /notes/{id}": "src/delete.main",
 *   },
 * });
 * ```
 */
export class Api extends Construct {
    constructor(scope, id, props) {
        super(scope, id);
        this.props = props || {};
        this.cdk = {};
        this.routesData = {};
        this.authorizersData = {};
        this.permissionsAttachedForAllRoutes = [];
        this.createHttpApi();
        this.addAuthorizers(this.props.authorizers || {});
        this.addRoutes(this, this.props.routes || {});
    }
    /**
     * The AWS generated URL of the Api.
     */
    get url() {
        return this.cdk.httpApi.apiEndpoint;
    }
    /**
     * If custom domain is enabled, this is the custom domain URL of the Api.
     *
     * :::note
     * If you are setting the base mapping for the custom domain, you need to include the trailing slash while using the custom domain URL. For example, if the [`domainName`](#domainname) is set to `api.domain.com` and the [`path`](#path) is `v1`, the custom domain URL of the API will be `https://api.domain.com/v1/`.
     * :::
     */
    get customDomainUrl() {
        return this._customDomainUrl;
    }
    /**
     * The routes for the Api
     */
    get routes() {
        return Object.keys(this.routesData);
    }
    /**
     * The ARN of the internally created API Gateway HTTP API
     */
    get httpApiArn() {
        const stack = Stack.of(this);
        return `arn:${stack.partition}:apigateway:${stack.region}::/apis/${this.cdk.httpApi.apiId}`;
    }
    /**
     * The id of the internally created API Gateway HTTP API
     */
    get httpApiId() {
        return this.cdk.httpApi.apiId;
    }
    /**
     * Adds routes to the Api after it has been created.
     *
     * @example
     * ```js
     * api.addRoutes(stack, {
     *   "GET    /notes/{id}": "src/get.main",
     *   "PUT    /notes/{id}": "src/update.main",
     *   "DELETE /notes/{id}": "src/delete.main",
     * });
     * ```
     */
    addRoutes(scope, routes) {
        Object.keys(routes).forEach((routeKey) => {
            this.addRoute(scope, routeKey, routes[routeKey]);
        });
    }
    /**
     * Get the instance of the internally created Function, for a given route key where the `routeKey` is the key used to define a route. For example, `GET /notes`.
     *
     * @example
     * ```js
     * const api = new Api(stack, "Api", {
     *   routes: {
     *     "GET /notes": "src/list.main",
     *   },
     * });
     *
     * const listFunction = api.getFunction("GET /notes");
     * ```
     */
    getFunction(routeKey) {
        const route = this.routesData[this.normalizeRouteKey(routeKey)];
        if (!route)
            return;
        if ("function" in route)
            return route.function;
        return;
    }
    /**
     * Attaches the given list of permissions to all the routes. This allows the functions to access other AWS resources.
     *
     * @example
     *
     * ```js
     * api.attachPermissions(["s3"]);
     * ```
     */
    attachPermissions(permissions) {
        for (const route of Object.values(this.routesData)) {
            if ("function" in route) {
                route.function.attachPermissions(permissions);
            }
        }
        this.permissionsAttachedForAllRoutes.push(permissions);
    }
    /**
     * Attaches the given list of permissions to a specific route. This allows that function to access other AWS resources.
     *
     * @example
     * ```js
     * const api = new Api(stack, "Api", {
     *   routes: {
     *     "GET    /notes": "src/list.main",
     *   },
     * });
     *
     * api.attachPermissionsToRoute("GET /notes", ["s3"]);
     * ```
     *
     */
    attachPermissionsToRoute(routeKey, permissions) {
        const fn = this.getFunction(routeKey);
        if (!fn) {
            throw new Error(`Failed to attach permissions. Route "${routeKey}" does not exist.`);
        }
        fn.attachPermissions(permissions);
    }
    getConstructMetadata() {
        return {
            type: "Api",
            data: {
                graphql: false,
                url: this.cdk.httpApi.url,
                httpApiId: this.cdk.httpApi.apiId,
                customDomainUrl: this._customDomainUrl,
                routes: Object.entries(this.routesData).map(([key, data]) => {
                    if (data.type === "function")
                        return {
                            type: data.type,
                            route: key,
                            fn: getFunctionRef(data.function),
                        };
                    if (data.type === "pothos")
                        return {
                            type: data.type,
                            route: key,
                            fn: getFunctionRef(data.function),
                            schema: data.schema,
                            output: data.output,
                            commands: data.commands,
                        };
                    return { type: data.type, route: key };
                }),
            },
        };
    }
    createHttpApi() {
        const { cdk, cors, defaults, accessLog, customDomain } = this.props;
        const id = this.node.id;
        const app = this.node.root;
        if (isCDKConstruct(cdk?.httpApi)) {
            if (cors !== undefined) {
                throw new Error(`Cannot configure the "cors" when "cdk.httpApi" is a construct`);
            }
            if (accessLog !== undefined) {
                throw new Error(`Cannot configure the "accessLog" when "cdk.httpApi" is a construct`);
            }
            if (customDomain !== undefined) {
                throw new Error(`Cannot configure the "customDomain" when "cdk.httpApi" is a construct`);
            }
            if (cdk?.httpStages !== undefined) {
                throw new Error(`Cannot configure the "stages" when "cdk.httpApi" is a construct`);
            }
            this.cdk.httpApi = cdk?.httpApi;
        }
        else {
            const httpApiProps = (cdk?.httpApi || {});
            // Validate input
            if (httpApiProps.corsPreflight !== undefined) {
                throw new Error(`Cannot configure the "httpApi.corsPreflight" in the Api`);
            }
            if (httpApiProps.defaultDomainMapping !== undefined) {
                throw new Error(`Cannot configure the "httpApi.defaultDomainMapping" in the Api`);
            }
            // Handle Custom Domain
            const customDomainData = apigV2Domain.buildCustomDomainData(this, customDomain);
            let defaultDomainMapping;
            if (customDomainData) {
                if (customDomainData.isApigDomainCreated) {
                    this.cdk.domainName = customDomainData.apigDomain;
                }
                if (customDomainData.isCertificatedCreated) {
                    this.cdk.certificate =
                        customDomainData.certificate;
                }
                defaultDomainMapping = {
                    domainName: customDomainData.apigDomain,
                    mappingKey: customDomainData.mappingKey,
                };
                this._customDomainUrl = `https://${customDomainData.url}`;
            }
            this.cdk.httpApi = new apig.HttpApi(this, "Api", {
                apiName: app.logicalPrefixedName(id),
                corsPreflight: apigV2Cors.buildCorsConfig(cors),
                defaultDomainMapping,
                ...httpApiProps,
            });
            const httpStage = this.cdk.httpApi.defaultStage;
            // Configure throttling
            if (defaults?.throttle?.burst && defaults?.throttle?.rate) {
                const cfnStage = httpStage.node.defaultChild;
                cfnStage.defaultRouteSettings = {
                    ...(cfnStage.routeSettings || {}),
                    throttlingBurstLimit: defaults.throttle.burst,
                    throttlingRateLimit: defaults.throttle.rate,
                };
            }
            // Configure access log
            for (const def of cdk?.httpStages || []) {
                const stage = new apig.HttpStage(this, "Stage" + def.stageName, {
                    ...def,
                    httpApi: this.cdk.httpApi,
                });
                apigV2AccessLog.buildAccessLogData(this, accessLog, stage, false);
            }
            if (this.cdk.httpApi.defaultStage)
                this.cdk.accessLogGroup = apigV2AccessLog.buildAccessLogData(this, accessLog, this.cdk.httpApi.defaultStage, true);
        }
    }
    addAuthorizers(authorizers) {
        Object.entries(authorizers).forEach(([key, value]) => {
            if (key === "none") {
                throw new Error(`Cannot name an authorizer "none"`);
            }
            else if (key === "iam") {
                throw new Error(`Cannot name an authorizer "iam"`);
            }
            else if (value.type === "user_pool") {
                if (value.cdk?.authorizer) {
                    this.authorizersData[key] = value.cdk.authorizer;
                }
                else {
                    if (!value.userPool) {
                        throw new Error(`Missing "userPool" for "${key}" authorizer`);
                    }
                    const userPool = cognito.UserPool.fromUserPoolId(this, `Api-${this.node.id}-Authorizer-${key}-UserPool`, value.userPool.id);
                    const userPoolClients = value.userPool.clientIds &&
                        value.userPool.clientIds.map((clientId, i) => cognito.UserPoolClient.fromUserPoolClientId(this, `Api-${this.node.id}-Authorizer-${key}-UserPoolClient-${i}`, clientId));
                    this.authorizersData[key] =
                        new apigAuthorizers.HttpUserPoolAuthorizer(key, userPool, {
                            authorizerName: value.name,
                            identitySource: value.identitySource,
                            userPoolClients,
                            userPoolRegion: value.userPool.region,
                        });
                }
            }
            else if (value.type === "jwt") {
                if (value.cdk?.authorizer) {
                    this.authorizersData[key] = value.cdk.authorizer;
                }
                else {
                    if (!value.jwt) {
                        throw new Error(`Missing "jwt" for "${key}" authorizer`);
                    }
                    this.authorizersData[key] = new apigAuthorizers.HttpJwtAuthorizer(key, value.jwt.issuer, {
                        authorizerName: value.name,
                        identitySource: value.identitySource,
                        jwtAudience: value.jwt.audience,
                    });
                }
            }
            else if (value.type === "lambda") {
                if (value.cdk?.authorizer) {
                    this.authorizersData[key] = value.cdk.authorizer;
                }
                else {
                    if (!value.function) {
                        throw new Error(`Missing "function" for "${key}" authorizer`);
                    }
                    this.authorizersData[key] = new apigAuthorizers.HttpLambdaAuthorizer(key, value.function, {
                        authorizerName: value.name,
                        identitySource: value.identitySource,
                        responseTypes: value.responseTypes &&
                            value.responseTypes.map((type) => apigAuthorizers.HttpLambdaResponseType[type.toUpperCase()]),
                        resultsCacheTtl: value.resultsCacheTtl
                            ? toCdkDuration(value.resultsCacheTtl)
                            : cdk.Duration.seconds(0),
                    });
                }
            }
        });
    }
    addRoute(scope, routeKey, routeValue) {
        ///////////////////
        // Normalize routeKey
        ///////////////////
        routeKey = this.normalizeRouteKey(routeKey);
        if (this.routesData[routeKey]) {
            throw new Error(`A route already exists for "${routeKey}"`);
        }
        ///////////////////
        // Get path and method
        ///////////////////
        let postfixName;
        let httpRouteKey;
        let method;
        let path;
        if (routeKey === "$default") {
            postfixName = "default";
            httpRouteKey = apig.HttpRouteKey.DEFAULT;
            method = "ANY";
            path = routeKey;
        }
        else {
            const routeKeyParts = routeKey.split(" ");
            if (routeKeyParts.length !== 2) {
                throw new Error(`Invalid route ${routeKey}`);
            }
            method = routeKeyParts[0].toUpperCase();
            if (!apig.HttpMethod[method]) {
                throw new Error(`Invalid method defined for "${routeKey}"`);
            }
            path = routeKeyParts[1];
            if (path.length === 0) {
                throw new Error(`Invalid path defined for "${routeKey}"`);
            }
            postfixName = `${method}_${path}`;
            httpRouteKey = apig.HttpRouteKey.with(path, apig.HttpMethod[method]);
        }
        ///////////////////
        // Create route
        ///////////////////
        const [routeProps, integration] = (() => {
            if (Fn.isInlineDefinition(routeValue)) {
                const routeProps = {
                    function: routeValue,
                };
                return [
                    routeProps,
                    this.createFunctionIntegration(scope, routeKey, routeProps, postfixName),
                ];
            }
            if (routeValue.type === "alb") {
                return [
                    routeValue,
                    this.createAlbIntegration(scope, routeKey, routeValue, postfixName),
                ];
            }
            if (routeValue.type === "url") {
                return [
                    routeValue,
                    this.createHttpIntegration(scope, routeKey, routeValue, postfixName),
                ];
            }
            if (routeValue.type === "pothos") {
                return [
                    routeValue,
                    this.createPothosIntegration(scope, routeKey, routeValue, postfixName),
                ];
            }
            if ("function" in routeValue) {
                return [
                    routeValue,
                    this.createFunctionIntegration(scope, routeKey, routeValue, postfixName),
                ];
            }
            if ("handler" in routeValue)
                throw new Error(`Function definition must be nested under the "function" key in the route props for "${routeKey}". ie. { function: { handler: "myfunc.handler" } }`);
            throw new Error(`Invalid route type for "${routeKey}". Must be one of: alb, url, function`);
        })();
        const { authorizationType, authorizer, authorizationScopes } = this.buildRouteAuth(routeProps);
        const route = new apig.HttpRoute(scope, `Route_${postfixName}`, {
            httpApi: this.cdk.httpApi,
            routeKey: httpRouteKey,
            integration,
            authorizer,
            authorizationScopes,
        });
        ////////////////////
        // Configure route authorization type
        ////////////////////
        // Note: we need to explicitly set `cfnRoute.authorizationType` to `NONE`
        //       because if it were set to `AWS_IAM`, and then it is removed from
        //       the CloudFormation template (ie. set to undefined), CloudFormation
        //       doesn't updates the route. The route's authorizationType would still
        //       be `AWS_IAM`.
        const cfnRoute = route.node.defaultChild;
        if (authorizationType === "iam") {
            cfnRoute.authorizationType = "AWS_IAM";
        }
        else if (authorizationType === "none") {
            cfnRoute.authorizationType = "NONE";
        }
    }
    createHttpIntegration(scope, routeKey, routeProps, postfixName) {
        ///////////////////
        // Create integration
        ///////////////////
        const integration = new apigIntegrations.HttpUrlIntegration(`Integration_${postfixName}`, routeProps.url, routeProps.cdk?.integration);
        // Store route
        this.routesData[routeKey] = {
            type: "url",
            url: routeProps.url,
        };
        return integration;
    }
    createAlbIntegration(scope, routeKey, routeProps, postfixName) {
        ///////////////////
        // Create integration
        ///////////////////
        const integration = new apigIntegrations.HttpAlbIntegration(`Integration_${postfixName}`, routeProps.cdk?.albListener, routeProps.cdk?.integration);
        // Store route
        this.routesData[routeKey] = {
            type: "alb",
            alb: routeProps.cdk?.albListener,
        };
        return integration;
    }
    createPothosIntegration(scope, routeKey, routeProps, postfixName) {
        const result = this.createFunctionIntegration(scope, routeKey, {
            ...routeProps,
            type: "function",
            payloadFormatVersion: "2.0",
        }, postfixName);
        const data = this.routesData[routeKey];
        if (data.type === "function") {
            this.routesData[routeKey] = {
                ...data,
                type: "pothos",
                output: routeProps.output,
                schema: routeProps.schema,
                commands: routeProps.commands,
            };
        }
        return result;
    }
    createFunctionIntegration(scope, routeKey, routeProps, postfixName) {
        ///////////////////
        // Get payload format
        ///////////////////
        const payloadFormatVersion = routeProps.payloadFormatVersion ||
            this.props.defaults?.payloadFormatVersion ||
            "2.0";
        if (!PayloadFormatVersions.includes(payloadFormatVersion)) {
            throw new Error(`PayloadFormatVersion: sst.Api does not currently support ${payloadFormatVersion} payload format version. Only "V1" and "V2" are currently supported.`);
        }
        const integrationPayloadFormatVersion = payloadFormatVersion === "1.0"
            ? apig.PayloadFormatVersion.VERSION_1_0
            : apig.PayloadFormatVersion.VERSION_2_0;
        ///////////////////
        // Create Function
        ///////////////////
        const lambda = Fn.fromDefinition(scope, `Lambda_${postfixName}`, routeProps.function, this.props.defaults?.function, `The "defaults.function" cannot be applied if an instance of a Function construct is passed in. Make sure to define all the routes using FunctionProps, so the Api construct can apply the "defaults.function" to them.`);
        // Add an environment variable to determine if the function is an Api route.
        // If it is, when "sst start" is not connected, we want to return an 500
        // status code and a descriptive error message.
        const root = scope.node.root;
        if (root.local) {
            lambda.addEnvironment("SST_DEBUG_IS_API_ROUTE", "1", {
                removeInEdge: true,
            });
        }
        ///////////////////
        // Create integration
        ///////////////////
        const integration = new apigIntegrations.HttpLambdaIntegration(`Integration_${postfixName}`, lambda, {
            payloadFormatVersion: integrationPayloadFormatVersion,
        });
        // Store route
        this.routesData[routeKey] = {
            type: "function",
            function: lambda,
        };
        // Attached existing permissions
        this.permissionsAttachedForAllRoutes.forEach((permissions) => lambda.attachPermissions(permissions));
        return integration;
    }
    buildRouteAuth(routeProps) {
        const authorizerKey = routeProps?.authorizer || this.props.defaults?.authorizer || "none";
        if (authorizerKey === "none") {
            return {
                authorizationType: "none",
                authorizer: new apig.HttpNoneAuthorizer(),
            };
        }
        else if (authorizerKey === "iam") {
            return {
                authorizationType: "iam",
                authorizer: new apigAuthorizers.HttpIamAuthorizer(),
            };
        }
        if (!this.props.authorizers ||
            !this.props.authorizers[authorizerKey]) {
            throw new Error(`Cannot find authorizer "${authorizerKey}"`);
        }
        const authorizer = this.authorizersData[authorizerKey];
        const authorizationType = this.props.authorizers[authorizerKey].type;
        const authorizationScopes = authorizationType === "jwt" || authorizationType === "user_pool"
            ? routeProps?.authorizationScopes ||
                this.props.defaults?.authorizationScopes
            : undefined;
        return { authorizationType, authorizer, authorizationScopes };
    }
    normalizeRouteKey(routeKey) {
        return routeKey.split(/\s+/).join(" ");
    }
}
