/* eslint-disable @typescript-eslint/ban-types */
// Note: disabling ban-type rule so we don't get an error referencing the class Function
import path from "path";
import fs from "fs-extra";
import * as cdk from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as logs from "aws-cdk-lib/aws-logs";
import * as ssm from "aws-cdk-lib/aws-ssm";
import { State, Runtime, FunctionConfig } from "@serverless-stack/core";
import { Stack } from "./Stack.js";
import { Secret, Parameter } from "./Config.js";
import { toCdkSize } from "./util/size.js";
import { toCdkDuration } from "./util/duration.js";
import { attachPermissionsToRole } from "./util/permission.js";
import * as functionUrlCors from "./util/functionUrlCors.js";
import url from "url";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const supportedRuntimes = [
    lambda.Runtime.NODEJS,
    lambda.Runtime.NODEJS_4_3,
    lambda.Runtime.NODEJS_6_10,
    lambda.Runtime.NODEJS_8_10,
    lambda.Runtime.NODEJS_10_X,
    lambda.Runtime.NODEJS_12_X,
    lambda.Runtime.NODEJS_14_X,
    lambda.Runtime.NODEJS_16_X,
    lambda.Runtime.PYTHON_2_7,
    lambda.Runtime.PYTHON_3_6,
    lambda.Runtime.PYTHON_3_7,
    lambda.Runtime.PYTHON_3_8,
    lambda.Runtime.PYTHON_3_9,
    lambda.Runtime.DOTNET_CORE_1,
    lambda.Runtime.DOTNET_CORE_2,
    lambda.Runtime.DOTNET_CORE_2_1,
    lambda.Runtime.DOTNET_CORE_3_1,
    lambda.Runtime.DOTNET_6,
    lambda.Runtime.JAVA_8,
    lambda.Runtime.JAVA_11,
    lambda.Runtime.GO_1_X,
];
/**
 * The `Function` construct is a higher level CDK construct that makes it easy to create a Lambda Function with support for Live Lambda Development.
 *
 * @example
 *
 * ```js
 * import { Function } from "@serverless-stack/resources";
 *
 * new Function(stack, "MySnsLambda", {
 *   handler: "src/sns/index.main",
 * });
 * ```
 */
export class Function extends lambda.Function {
    constructor(scope, id, props) {
        const app = scope.node.root;
        const stack = Stack.of(scope);
        // Merge with app defaultFunctionProps
        // note: reverse order so later prop override earlier ones
        stack.defaultFunctionProps
            .slice()
            .reverse()
            .forEach((per) => {
            props = Function.mergeProps(per, props);
        });
        // Set defaults
        const functionName = props.functionName &&
            (typeof props.functionName === "string"
                ? props.functionName
                : props.functionName({ stack, functionProps: props }));
        const handler = props.handler;
        const timeout = Function.normalizeTimeout(props.timeout);
        const srcPath = Function.normalizeSrcPath(props.srcPath || ".");
        const runtime = Function.normalizeRuntime(props.runtime);
        const architecture = (() => {
            if (props.architecture === "arm_64")
                return lambda.Architecture.ARM_64;
            if (props.architecture === "x86_64")
                return lambda.Architecture.X86_64;
            return undefined;
        })();
        const memorySize = Function.normalizeMemorySize(props.memorySize);
        const diskSize = Function.normalizeDiskSize(props.diskSize);
        const tracing = lambda.Tracing[(props.tracing || "active").toUpperCase()];
        const logRetention = props.logRetention &&
            logs.RetentionDays[props.logRetention.toUpperCase()];
        let bundle = props.bundle;
        const permissions = props.permissions || [];
        const isLiveDevEnabled = props.enableLiveDev === false ? false : true;
        // Validate handler
        if (!handler) {
            throw new Error(`No handler defined for the "${id}" Lambda function`);
        }
        // Validate input
        const isNodeRuntime = runtime.toString().startsWith("nodejs");
        const isPythonRuntime = runtime.toString().startsWith("python");
        if (isNodeRuntime) {
            bundle = bundle === undefined ? true : props.bundle;
            if (!bundle && srcPath === ".") {
                throw new Error(`Bundle cannot be disabled for the "${id}" function since the "srcPath" is set to the project root. Read more here — https://github.com/serverless-stack/sst/issues/78`);
            }
        }
        else if (isPythonRuntime) {
            bundle = bundle === undefined ? {} : props.bundle;
            if (srcPath === ".") {
                throw new Error(`Cannot set the "srcPath" to the project root for the "${id}" function.`);
            }
        }
        const localId = path.posix
            .join(scope.node.path, id)
            .replace(/\$/g, "-")
            .replace(/\//g, "-")
            .replace(/\./g, "-");
        // Handle local development (ie. sst start)
        // - set runtime to nodejs12.x for non-Node runtimes (b/c the stub is in Node)
        // - set retry to 0. When the debugger is disconnected, the Cron construct
        //   will still try to periodically invoke the Lambda, and the requests would
        //   fail and retry. So when launching `sst start`, a couple of retry requests
        //   from recent failed request will be received. And this behavior is confusing.
        if (isLiveDevEnabled &&
            app.local &&
            app.debugEndpoint &&
            app.debugBucketName &&
            app.debugBucketArn) {
            // If debugIncreaseTimeout is enabled:
            //   set timeout to 900s. This will give people more time to debug the function
            //   without timing out the request. Note API Gateway requests have a maximum
            //   timeout of 29s. In this case, the API will timeout, but the Lambda function
            //   will continue to run.
            let debugOverrideProps;
            if (app.debugIncreaseTimeout) {
                debugOverrideProps = {
                    timeout: cdk.Duration.seconds(900),
                };
            }
            if (app.debugBridge) {
                super(scope, id, {
                    ...props,
                    architecture,
                    code: lambda.Code.fromAsset(path.resolve(__dirname, "../dist/bridge_client/")),
                    handler: "handler",
                    functionName,
                    runtime: lambda.Runtime.NODEJS_16_X,
                    memorySize,
                    ephemeralStorageSize: diskSize,
                    timeout,
                    tracing,
                    environment: {
                        ...(props.environment || {}),
                        SST_DEBUG_ENDPOINT: app.debugEndpoint,
                    },
                    layers: Function.buildLayers(scope, id, props),
                    logRetention,
                    ...(debugOverrideProps || {}),
                });
            }
            else {
                super(scope, id, {
                    ...props,
                    architecture,
                    code: lambda.Code.fromAsset(path.resolve(__dirname, "./support/bridge")),
                    handler: "bridge.handler",
                    functionName,
                    runtime: isNodeRuntime ? runtime : lambda.Runtime.NODEJS_16_X,
                    memorySize,
                    ephemeralStorageSize: diskSize,
                    timeout,
                    tracing,
                    environment: {
                        ...(props.environment || {}),
                        SST_DEBUG_SRC_PATH: srcPath,
                        SST_DEBUG_SRC_HANDLER: handler,
                        SST_DEBUG_ENDPOINT: app.debugEndpoint,
                        SST_DEBUG_BUCKET_NAME: app.debugBucketName,
                    },
                    layers: Function.buildLayers(scope, id, props),
                    logRetention,
                    retryAttempts: 0,
                    ...(debugOverrideProps || {}),
                });
                this.attachPermissions(["iot"]);
            }
            State.Function.append(app.appPath, {
                id: localId,
                handler: handler,
                runtime: runtime.toString(),
                srcPath: srcPath,
                bundle: props.bundle,
            });
            this.addEnvironment("SST_FUNCTION_ID", localId);
            this.attachPermissions([
                new iam.PolicyStatement({
                    actions: ["s3:*"],
                    effect: iam.Effect.ALLOW,
                    resources: [app.debugBucketArn, `${app.debugBucketArn}/*`],
                }),
            ]);
        }
        // Handle remove (ie. sst remove)
        else if (app.skipBuild) {
            // Note: need to override runtime as CDK does not support inline code
            //       for some runtimes.
            super(scope, id, {
                ...props,
                architecture,
                code: lambda.Code.fromAsset(path.resolve(__dirname, "../assets/Function/placeholder-stub")),
                handler: "placeholder",
                functionName,
                runtime: lambda.Runtime.NODEJS_16_X,
                memorySize,
                ephemeralStorageSize: diskSize,
                timeout,
                tracing,
                environment: props.environment,
                layers: Function.buildLayers(scope, id, props),
                logRetention,
            });
        }
        // Handle build
        else {
            const bundled = Runtime.Handler.bundle({
                id: localId,
                root: app.appPath,
                handler: handler,
                runtime: runtime.toString(),
                srcPath: srcPath,
                bundle: props.bundle,
            });
            // Python builder returns AssetCode instead of directory
            const code = (() => {
                if ("directory" in bundled) {
                    Function.copyFiles(bundle, srcPath, bundled.directory);
                    return lambda.AssetCode.fromAsset(bundled.directory);
                }
                return bundled.asset;
            })();
            super(scope, id, {
                ...props,
                architecture,
                code: code,
                handler: bundled.handler,
                functionName,
                runtime,
                memorySize,
                ephemeralStorageSize: diskSize,
                timeout,
                tracing,
                environment: props.environment,
                layers: Function.buildLayers(scope, id, props),
                logRetention,
            });
        }
        this.props = props || {};
        if (isNodeRuntime) {
            // Enable reusing connections with Keep-Alive for NodeJs
            // Lambda function
            this.addEnvironment("AWS_NODEJS_CONNECTION_REUSE_ENABLED", "1", {
                removeInEdge: true,
            });
        }
        // Attach permissions
        if (permissions) {
            this.attachPermissions(permissions);
        }
        this.handleConfig();
        this.createUrl();
        app.registerLambdaHandler({
            bundle: props.bundle,
            handler: handler,
            runtime: runtime.toString(),
            srcPath,
        });
        this._isLiveDevEnabled = isLiveDevEnabled;
        this.localId = localId;
    }
    /**
     * The AWS generated URL of the Function.
     */
    get url() {
        return this.functionUrl?.url;
    }
    /**
     * Attaches additional permissions to function
     *
     * @example
     * ```js {20}
     * fn.attachPermissions(["s3"]);
     * ```
     */
    attachPermissions(permissions) {
        if (this.role) {
            attachPermissionsToRole(this.role, permissions);
        }
    }
    getConstructMetadata() {
        const { config } = this.props;
        return {
            type: "Function",
            data: {
                localId: this.localId,
                arn: this.functionArn,
                secrets: (config || [])
                    .filter((c) => c instanceof Secret)
                    .map((c) => c.name),
            },
        };
    }
    createUrl() {
        const { url } = this.props;
        if (url === false || url === undefined) {
            return;
        }
        let authType;
        let cors;
        if (url === true) {
            authType = lambda.FunctionUrlAuthType.NONE;
            cors = true;
        }
        else {
            authType =
                url.authorizer === "iam"
                    ? lambda.FunctionUrlAuthType.AWS_IAM
                    : lambda.FunctionUrlAuthType.NONE;
            cors = url.cors === undefined ? true : url.cors;
        }
        this.functionUrl = this.addFunctionUrl({
            authType,
            cors: functionUrlCors.buildCorsConfig(cors),
        });
    }
    handleConfig() {
        const app = this.node.root;
        const { config } = this.props;
        // Add environment variables
        this.addEnvironment("SST_APP", app.name, { removeInEdge: true });
        this.addEnvironment("SST_STAGE", app.stage, { removeInEdge: true });
        (config || []).forEach((c) => {
            if (c instanceof Secret) {
                this.addEnvironment(`${FunctionConfig.SECRET_ENV_PREFIX}${c.name}`, "1");
            }
            else if (c instanceof Parameter) {
                this.addEnvironment(`${FunctionConfig.PARAM_ENV_PREFIX}${c.name}`, c.value);
            }
        });
        // Attach permissions
        const hasSecrets = (config || []).some((c) => c instanceof Secret);
        if (hasSecrets) {
            this.attachPermissions([
                new iam.PolicyStatement({
                    actions: ["ssm:GetParameters"],
                    effect: iam.Effect.ALLOW,
                    resources: [
                        `arn:aws:ssm:${app.region}:${app.account}:parameter/sst/${app.name}/${app.stage}/*`,
                        `arn:aws:ssm:${app.region}:${app.account}:parameter/sst/${app.name}/${FunctionConfig.FALLBACK_STAGE}/*`,
                    ],
                }),
            ]);
        }
    }
    static buildLayers(scope, id, props) {
        return (props.layers || []).map((layer) => {
            if (typeof layer === "string") {
                return lambda.LayerVersion.fromLayerVersionArn(scope, `${id}${layer}`, layer);
            }
            return Function.handleImportedLayer(scope, layer);
        });
    }
    static normalizeMemorySize(memorySize) {
        if (typeof memorySize === "string") {
            return toCdkSize(memorySize).toMebibytes();
        }
        return memorySize || 1024;
    }
    static normalizeDiskSize(diskSize) {
        if (typeof diskSize === "string") {
            return toCdkSize(diskSize);
        }
        return cdk.Size.mebibytes(diskSize || 512);
    }
    static normalizeTimeout(timeout) {
        if (typeof timeout === "string") {
            return toCdkDuration(timeout);
        }
        return cdk.Duration.seconds(timeout || 10);
    }
    static normalizeRuntime(runtime) {
        runtime = runtime || "nodejs14.x";
        const runtimeClass = supportedRuntimes.find((per) => per.toString() === runtime);
        if (!runtimeClass) {
            throw new Error(`The specified runtime is not supported for sst.Function. Only NodeJS, Python, Go, and .NET runtimes are currently supported.`);
        }
        return runtimeClass;
    }
    static normalizeSrcPath(srcPath) {
        return srcPath.replace(/\/+$/, "");
    }
    static copyFiles(bundle, srcPath, buildPath) {
        if (!bundle)
            return;
        if (typeof bundle === "boolean")
            return;
        if (!bundle.copyFiles)
            return;
        bundle.copyFiles.forEach((entry) => {
            const fromPath = path.join(srcPath, entry.from);
            if (!fs.existsSync(fromPath))
                throw new Error(`Tried to copy nonexistent file from "${path.resolve(fromPath)}" - check copyFiles entry "${entry.from}"`);
            const to = entry.to || entry.from;
            if (path.isAbsolute(to))
                throw new Error(`Copy destination path "${to}" must be relative`);
            const toPath = path.join(buildPath, to);
            fs.copySync(fromPath, toPath);
        });
    }
    static handleImportedLayer(scope, layer) {
        const layerStack = Stack.of(layer);
        const currentStack = Stack.of(scope);
        // Use layer directly if:
        // - layer is created in the current stack; OR
        // - layer is imported (ie. layerArn is a string)
        if (layerStack === currentStack ||
            !cdk.Token.isUnresolved(layer.layerVersionArn)) {
            return layer;
        }
        // layer is created from another stack
        else {
            // set stack dependency b/c layerStack need to create the SSM first
            currentStack.addDependency(layerStack);
            // store layer ARN in SSM in layer's stack
            const parameterId = `${layer.node.id}Arn-${layer.node.addr}`;
            const parameterName = `/layers/${layerStack.node.id}/${parameterId}`;
            const existingSsmParam = layerStack.node.tryFindChild(parameterId);
            if (!existingSsmParam) {
                new ssm.StringParameter(layerStack, parameterId, {
                    parameterName,
                    stringValue: layer.layerVersionArn,
                });
            }
            // import layer from SSM value
            const layerId = `I${layer.node.id}-${layer.node.addr}`;
            const existingLayer = scope.node.tryFindChild(layerId);
            if (existingLayer) {
                return existingLayer;
            }
            else {
                return lambda.LayerVersion.fromLayerVersionArn(scope, layerId, ssm.StringParameter.valueForStringParameter(scope, parameterName));
            }
        }
    }
    static isInlineDefinition(definition) {
        return typeof definition === "string" || definition instanceof Function;
    }
    static fromDefinition(scope, id, definition, inheritedProps, inheritErrorMessage) {
        if (typeof definition === "string") {
            return new Function(scope, id, {
                ...(inheritedProps || {}),
                handler: definition,
            });
        }
        else if (definition instanceof Function) {
            if (inheritedProps && Object.keys(inheritedProps).length > 0) {
                throw new Error(inheritErrorMessage ||
                    `Cannot inherit default props when a Function is provided`);
            }
            return definition;
        }
        else if (definition instanceof lambda.Function) {
            throw new Error(`Please use sst.Function instead of lambda.Function for the "${id}" Function.`);
        }
        else if (definition.handler !== undefined) {
            return new Function(scope, id, Function.mergeProps(inheritedProps, definition));
        }
        throw new Error(`Invalid function definition for the "${id}" Function`);
    }
    static mergeProps(baseProps, props) {
        // Merge environment
        const environment = {
            ...(baseProps?.environment || {}),
            ...(props?.environment || {}),
        };
        const environmentProp = Object.keys(environment).length === 0 ? {} : { environment };
        // Merge layers
        const layers = [...(baseProps?.layers || []), ...(props?.layers || [])];
        const layersProp = layers.length === 0 ? {} : { layers };
        // Merge permissions
        let permissionsProp;
        if (baseProps?.permissions === "*") {
            permissionsProp = { permissions: baseProps.permissions };
        }
        else if (props?.permissions === "*") {
            permissionsProp = { permissions: props.permissions };
        }
        else {
            const permissions = (baseProps?.permissions || []).concat(props?.permissions || []);
            permissionsProp = permissions.length === 0 ? {} : { permissions };
        }
        return {
            ...(baseProps || {}),
            ...(props || {}),
            ...layersProp,
            ...environmentProp,
            ...permissionsProp,
        };
    }
}
