import url from "url";
import path from "path";
import spawn from "cross-spawn";
import { Construct } from "constructs";
import { Effect, Policy, PolicyStatement, } from "aws-cdk-lib/aws-iam";
import { RetentionDays } from "aws-cdk-lib/aws-logs";
import { Architecture, AssetCode, Runtime, Code, Function as CdkFunction, } from "aws-cdk-lib/aws-lambda";
import { Bucket } from "aws-cdk-lib/aws-s3";
import { Duration as CdkDuration, CustomResource, } from "aws-cdk-lib/core";
import { useProject } from "../project.js";
import { useRuntimeHandlers } from "../runtime/handlers.js";
import { useFunctions, } from "./Function.js";
import { Stack } from "./Stack.js";
import { bindEnvironment, bindPermissions, getReferencedSecrets, } from "./util/functionBinding.js";
import { attachPermissionsToRole } from "./util/permission.js";
import { toCdkSize } from "./util/size.js";
import { toCdkDuration } from "./util/duration.js";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
/////////////////////
// Construct
/////////////////////
export class SsrFunction extends Construct {
    id;
    function;
    assetReplacer;
    assetReplacerPolicy;
    props;
    /** @internal */
    _doNotAllowOthersToBind = true;
    constructor(scope, id, props) {
        super(scope, id);
        this.id = id;
        this.props = {
            ...props,
            environment: props.environment || {},
            permissions: props.permissions || [],
        };
        // Create function with placeholder code
        const assetBucket = "placeholder";
        const assetKey = "placeholder";
        const { assetReplacer, assetReplacerPolicy } = this.createCodeReplacer(assetBucket, assetKey);
        this.function = this.createFunction(assetBucket, assetKey);
        this.attachPermissions(props.permissions || []);
        this.bind(props.bind || []);
        this.function.node.addDependency(assetReplacer);
        this.assetReplacer = assetReplacer;
        this.assetReplacerPolicy = assetReplacerPolicy;
    }
    get role() {
        return this.function.role;
    }
    get functionArn() {
        return this.function.functionArn;
    }
    get functionName() {
        return this.function.functionName;
    }
    addEnvironment(key, value) {
        return this.function.addEnvironment(key, value);
    }
    addFunctionUrl(props) {
        return this.function.addFunctionUrl(props);
    }
    grantInvoke(grantee) {
        return this.function.grantInvoke(grantee);
    }
    async build() {
        const { bundle, handler } = this.props;
        const code = bundle
            ? await this.buildAssetFromBundle(bundle)
            : await this.buildAssetFromHandler();
        const codeConfig = code.bind(this.function);
        const assetBucket = codeConfig.s3Location?.bucketName;
        const assetKey = codeConfig.s3Location?.objectKey;
        this.updateCodeReplacer(assetBucket, assetKey);
        this.updateFunction(code, assetBucket, assetKey);
    }
    attachPermissions(permissions) {
        attachPermissionsToRole(this.function.role, permissions);
    }
    createFunction(assetBucket, assetKey) {
        const { architecture, runtime, timeout, memorySize, handler, logRetention, } = this.props;
        return new CdkFunction(this, `ServerFunction`, {
            ...this.props,
            handler: handler.split(path.sep).join(path.posix.sep),
            logRetention: logRetention ?? RetentionDays.THREE_DAYS,
            code: Code.fromBucket(Bucket.fromBucketName(this, "IServerFunctionBucket", assetBucket), assetKey),
            runtime: runtime === "nodejs14.x"
                ? Runtime.NODEJS_14_X
                : runtime === "nodejs16.x"
                    ? Runtime.NODEJS_16_X
                    : Runtime.NODEJS_18_X,
            architecture: architecture || Architecture.ARM_64,
            memorySize: typeof memorySize === "string"
                ? toCdkSize(memorySize).toMebibytes()
                : memorySize,
            timeout: typeof timeout === "string"
                ? toCdkDuration(timeout)
                : CdkDuration.seconds(timeout),
            logRetentionRetryOptions: logRetention && { maxRetries: 100 },
        });
    }
    createCodeReplacer(assetBucket, assetKey) {
        const { environment } = this.props;
        // Note: Source code for the Lambda functions have "{{ ENV_KEY }}" in them.
        //       They need to be replaced with real values before the Lambda
        //       functions get deployed.
        // - "*.js" files: ie. Next.js server function
        // - "*.html" files: ie. SvelteKit prerendered pages data
        // - "*.json" files: ie. SvelteKit prerendered + SSR data
        const stack = Stack.of(this);
        const policy = new Policy(this, "AssetReplacerPolicy", {
            statements: [
                new PolicyStatement({
                    effect: Effect.ALLOW,
                    actions: ["s3:GetObject", "s3:PutObject"],
                    resources: [`arn:${stack.partition}:s3:::${assetBucket}/*`],
                }),
            ],
        });
        stack.customResourceHandler.role?.attachInlinePolicy(policy);
        const resource = new CustomResource(this, "AssetReplacer", {
            serviceToken: stack.customResourceHandler.functionArn,
            resourceType: "Custom::AssetReplacer",
            properties: {
                bucket: assetBucket,
                key: assetKey,
                replacements: Object.entries(environment).map(([key, value]) => ({
                    files: "**/*.@(*js|json|html)",
                    search: `{{ ${key} }}`,
                    replace: value,
                })),
            },
        });
        resource.node.addDependency(policy);
        return { assetReplacer: resource, assetReplacerPolicy: policy };
    }
    bind(constructs) {
        const app = this.node.root;
        this.function.addEnvironment("SST_APP", app.name);
        this.function.addEnvironment("SST_STAGE", app.stage);
        this.function.addEnvironment("SST_SSM_PREFIX", useProject().config.ssmPrefix);
        // Get referenced secrets
        const referencedSecrets = [];
        constructs.forEach((c) => referencedSecrets.push(...getReferencedSecrets(c)));
        [...constructs, ...referencedSecrets].forEach((c) => {
            // Bind environment
            const env = bindEnvironment(c);
            Object.entries(env).forEach(([key, value]) => this.function.addEnvironment(key, value));
            // Bind permissions
            const permissions = bindPermissions(c);
            Object.entries(permissions).forEach(([action, resources]) => this.attachPermissions([
                new PolicyStatement({
                    actions: [action],
                    effect: Effect.ALLOW,
                    resources,
                }),
            ]));
        });
    }
    async buildAssetFromHandler() {
        useFunctions().add(this.node.addr, {
            handler: this.props.handler,
            runtime: this.props.runtime,
            nodejs: this.props.nodejs,
            copyFiles: this.props.copyFiles,
        });
        // Build function
        const bundle = await useRuntimeHandlers().build(this.node.addr, "deploy");
        // create wrapper that calls the handler
        if (bundle.type === "error")
            throw new Error([
                `There was a problem bundling the SSR function for the "${this.node.id}" Site.`,
                ...bundle.errors,
            ].join("\n"));
        return AssetCode.fromAsset(bundle.out);
    }
    async buildAssetFromBundle(bundle) {
        // Note: cannot point the bundle to the `.open-next/server-function`
        //       b/c the folder contains node_modules. And pnpm node_modules
        //       contains symlinks. CDK cannot zip symlinks correctly.
        //       https://github.com/aws/aws-cdk/issues/9251
        //       We will zip the folder ourselves.
        const outputPath = path.resolve(useProject().paths.artifacts, `SsrFunction-${this.node.id}-${this.node.addr}`);
        const script = path.resolve(__dirname, "../support/ssr-site-function-archiver.mjs");
        const result = spawn.sync("node", [script, path.join(bundle), path.join(outputPath, "server-function.zip")], { stdio: "inherit" });
        if (result.status !== 0) {
            throw new Error(`There was a problem generating the assets package.`);
        }
        return AssetCode.fromAsset(path.join(outputPath, "server-function.zip"));
    }
    updateCodeReplacer(assetBucket, assetKey) {
        const stack = Stack.of(this);
        const cfnReplacer = this.assetReplacer.node
            .defaultChild;
        cfnReplacer.addPropertyOverride("bucket", assetBucket);
        cfnReplacer.addPropertyOverride("key", assetKey);
        const cfnPolicy = this.assetReplacerPolicy.node.defaultChild;
        cfnPolicy.addPropertyOverride("PolicyDocument.Statement.0.Resource", `arn:${stack.partition}:s3:::${assetBucket}/*`);
    }
    updateFunction(code, assetBucket, assetKey) {
        const cfnFunction = this.function.node.defaultChild;
        cfnFunction.code = {
            s3Bucket: assetBucket,
            s3Key: assetKey,
        };
        code.bindToResource(cfnFunction);
    }
    /** @internal */
    getConstructMetadata() {
        return {
            type: "Function",
            data: {
                arn: this.functionArn,
                handler: this.props.handler,
                localId: this.node.addr,
                secrets: [],
            },
        };
    }
    /** @internal */
    getFunctionBinding() {
        return undefined;
    }
}
