import { Construct } from "constructs";
import * as iam from "aws-cdk-lib/aws-iam";
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 * as apigV2Domain from "./util/apiGatewayV2Domain.js";
import * as apigV2AccessLog from "./util/apiGatewayV2AccessLog.js";
/////////////////////
// Construct
/////////////////////
/**
 * The `WebSocketApi` construct is a higher level CDK construct that makes it easy to create a WebSocket API.
 *
 * @example
 * ```js
 * import { WebSocketApi } from "@serverless-stack/resources";
 *
 * new WebSocketApi(stack, "Api", {
 *   routes: {
 *     $connect: "src/connect.main",
 *     $default: "src/default.main",
 *     $disconnect: "src/disconnect.main",
 *     sendMessage: "src/sendMessage.main",
 *   },
 * });
 * ```
 */
export class WebSocketApi extends Construct {
    constructor(scope, id, props) {
        super(scope, props?.cdk?.id || id);
        this.id = id;
        this.props = props || {};
        this.cdk = {};
        this.functions = {};
        this.apigRoutes = {};
        this.permissionsAttachedForAllRoutes = [];
        this.createWebSocketApi();
        this.createWebSocketStage();
        this.addAuthorizer();
        this.addRoutes(this, this.props.routes || {});
        // Allows functions to make ApiGatewayManagementApi.postToConnection calls.
        this.attachPermissions([
            new iam.PolicyStatement({
                effect: iam.Effect.ALLOW,
                actions: ["execute-api:ManageConnections"],
                resources: [this._connectionsArn],
            }),
        ]);
    }
    /**
     * Url of the WebSocket API
     */
    get url() {
        return this.cdk.webSocketStage.url;
    }
    /**
     * Custom domain url if it's configured
     */
    get customDomainUrl() {
        return this._customDomainUrl;
    }
    /**
     * List of routes of the websocket api
     */
    get routes() {
        return Object.keys(this.functions);
    }
    get _connectionsArn() {
        return Stack.of(this).formatArn({
            service: "execute-api",
            resourceName: `${this.cdk.webSocketStage.stageName}/POST/*`,
            resource: this.cdk.webSocketApi.apiId,
        });
    }
    /**
     * Add routes to an already created WebSocket API
     *
     * @example
     * ```js
     * api.addRoutes(stack, {
     *   "$connect": "src/connect.main",
     * })
     * ```
     */
    addRoutes(scope, routes) {
        Object.keys(routes).forEach((routeKey) => {
            // add route
            const fn = this.addRoute(scope, routeKey, routes[routeKey]);
            // attached existing permissions
            this.permissionsAttachedForAllRoutes.forEach((permissions) => fn.attachPermissions(permissions));
        });
    }
    /**
     * 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, `$connect`.
     *
     * @example
     * ```js
     * const fn = api.getFunction("$connect");
     * ```
     */
    getFunction(routeKey) {
        return this.functions[this.normalizeRouteKey(routeKey)];
    }
    /**
     * Get the instance of the internally created Route, for a given route key where the `routeKey` is the key used to define a route. For example, `$connect`.
     *
     * @example
     * ```js
     * const route = api.getRoute("$connect");
     * ```
     */
    getRoute(routeKey) {
        return this.apigRoutes[this.normalizeRouteKey(routeKey)];
    }
    /**
     * 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) {
        Object.values(this.functions).forEach((fn) => fn.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
     * api.attachPermissionsToRoute("$connect", ["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: "WebSocketApi",
            data: {
                httpApiId: this.cdk.webSocketApi.apiId,
                customDomainUrl: this._customDomainUrl,
                routes: Object.entries(this.functions).map(([routeKey, fn]) => ({
                    route: routeKey,
                    fn: getFunctionRef(fn),
                })),
            },
        };
    }
    /** @internal */
    getFunctionBinding() {
        return {
            clientPackage: "api",
            variables: {
                url: {
                    environment: this.customDomainUrl || this.url,
                    parameter: this.customDomainUrl || this.url,
                },
            },
            permissions: {},
        };
    }
    createWebSocketApi() {
        const { cdk } = this.props;
        const id = this.node.id;
        const app = this.node.root;
        if (isCDKConstruct(cdk?.webSocketApi)) {
            this.cdk.webSocketApi = cdk?.webSocketApi;
        }
        else {
            // Validate input
            if (isCDKConstruct(cdk?.webSocketStage)) {
                throw new Error(`Cannot import the "webSocketStage" when the "webSocketApi" is not imported.`);
            }
            const webSocketApiProps = (cdk?.webSocketApi ||
                {});
            // Create WebSocket API
            this.cdk.webSocketApi = new apig.WebSocketApi(this, "Api", {
                apiName: app.logicalPrefixedName(id),
                ...webSocketApiProps,
            });
        }
    }
    createWebSocketStage() {
        const { cdk, accessLog, customDomain } = this.props;
        if (isCDKConstruct(cdk?.webSocketStage)) {
            if (accessLog !== undefined) {
                throw new Error(`Cannot configure the "accessLog" when "webSocketStage" is a construct`);
            }
            if (customDomain !== undefined) {
                throw new Error(`Cannot configure the "customDomain" when "webSocketStage" is a construct`);
            }
            this.cdk.webSocketStage = cdk?.webSocketStage;
        }
        else {
            const webSocketStageProps = (cdk?.webSocketStage ||
                {});
            // Validate input
            if (webSocketStageProps.domainMapping !== undefined) {
                throw new Error(`Do not configure the "webSocketStage.domainMapping". Use the "customDomain" to configure the Api domain.`);
            }
            // Configure Custom Domain
            const customDomainData = apigV2Domain.buildCustomDomainData(this, customDomain);
            let domainMapping;
            if (customDomainData) {
                if (customDomainData.isApigDomainCreated) {
                    this.cdk.domainName = customDomainData.apigDomain;
                }
                if (customDomainData.isCertificatedCreated) {
                    this.cdk.certificate =
                        customDomainData.certificate;
                }
                domainMapping = {
                    domainName: customDomainData.apigDomain,
                    mappingKey: customDomainData.mappingKey,
                };
                this._customDomainUrl = `wss://${customDomainData.url}`;
            }
            // Create stage
            this.cdk.webSocketStage = new apig.WebSocketStage(this, "Stage", {
                webSocketApi: this.cdk.webSocketApi,
                stageName: this.node.root.stage,
                autoDeploy: true,
                domainMapping,
                ...webSocketStageProps,
            });
            // Configure Access Log
            this.cdk.accessLogGroup = apigV2AccessLog.buildAccessLogData(this, accessLog, this.cdk.webSocketStage, true);
        }
    }
    addAuthorizer() {
        const { authorizer } = this.props;
        if (!authorizer || authorizer === "none") {
            this.authorizer = "none";
        }
        else if (authorizer === "iam") {
            this.authorizer = "iam";
        }
        else if (authorizer.cdk?.authorizer) {
            this.authorizer = authorizer.cdk.authorizer;
        }
        else if (!authorizer.function) {
            throw new Error(`Missing "function" for authorizer`);
        }
        else {
            this.authorizer = new apigAuthorizers.WebSocketLambdaAuthorizer("WebSocketAuthorizer", authorizer.function, {
                authorizerName: authorizer.name,
                identitySource: authorizer.identitySource,
            });
        }
    }
    addRoute(scope, routeKey, routeValue) {
        ///////////////////
        // Normalize routeKey
        ///////////////////
        routeKey = this.normalizeRouteKey(routeKey);
        if (this.functions[routeKey]) {
            throw new Error(`A route already exists for "${routeKey}"`);
        }
        ///////////////////
        // Create Function
        ///////////////////
        const lambda = Fn.fromDefinition(scope, routeKey, Fn.isInlineDefinition(routeValue) ? routeValue : routeValue.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.`);
        ///////////////////
        // Get authorization
        ///////////////////
        const { authorizationType, authorizer } = this.buildRouteAuth();
        ///////////////////
        // Create route
        ///////////////////
        const route = new apig.WebSocketRoute(scope, `Route_${routeKey}`, {
            webSocketApi: this.cdk.webSocketApi,
            routeKey,
            integration: new apigIntegrations.WebSocketLambdaIntegration(`Integration_${routeKey}`, lambda),
            authorizer: routeKey === "$connect" ? authorizer : undefined,
        });
        ///////////////////
        // Configure authorization
        ///////////////////
        // Note: as of CDK v1.138.0, aws-apigatewayv2.WebSocketRoute does not
        //       support IAM authorization type. We need to manually configure it.
        if (routeKey === "$connect") {
            // 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;
            cfnRoute.authorizationType = authorizationType;
        }
        ///////////////////
        // Store function
        ///////////////////
        this.apigRoutes[routeKey] = route;
        this.functions[routeKey] = lambda;
        return lambda;
    }
    buildRouteAuth() {
        if (this.authorizer === "none") {
            return { authorizationType: "NONE" };
        }
        else if (this.authorizer === "iam") {
            return { authorizationType: "AWS_IAM" };
        }
        return {
            authorizationType: "CUSTOM",
            authorizer: this.authorizer,
        };
    }
    normalizeRouteKey(routeKey) {
        return routeKey.trim();
    }
}
