// import path from "path";
import url from "url";
import path from "path";
import fs from "fs-extra";
import { Construct } from "constructs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as codebuild from "aws-cdk-lib/aws-codebuild";
import { Runtime, DeferBuilder } from "@serverless-stack/core";
import { Function } from "./Function.js";
import { toCdkDuration } from "./util/duration.js";
import { attachPermissionsToRole } from "./util/permission.js";
import { bindEnvironment, bindParameters, bindPermissions } from "./util/functionBinding.js";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
/////////////////////
// Construct
/////////////////////
/**
 * The `Cron` construct is a higher level CDK construct that makes it easy to create a cron job.
 *
 * @example
 *
 * ```js
 * import { Cron } from "@serverless-stack/resources";
 *
 * new Cron(stack, "Cron", {
 *   schedule: "rate(1 minute)",
 *   job: "src/lambda.main",
 * });
 * ```
 */
export class Job extends Construct {
    constructor(scope, id, props) {
        super(scope, props.cdk?.id || id);
        const app = this.node.root;
        this.id = id;
        this.props = props;
        this.localId = path.posix
            .join(scope.node.path, id)
            .replace(/\$/g, "-")
            .replace(/\//g, "-")
            .replace(/\./g, "-");
        this.isLiveDevEnabled = this.props.enableLiveDev === false ? false : true;
        this.job = this.createCodeBuildProject();
        if (app.local && this.isLiveDevEnabled) {
            this._jobInvoker = this.createLocalInvoker();
        }
        else {
            this._jobInvoker = this.createCodeBuildInvoker();
            this.buildCodeBuildProjectCode();
        }
        this.attachPermissions(props.permissions || []);
        this.addConfig(props.config || []);
        this.use(props.use || []);
        Object.entries(props.environment || {}).forEach(([key, value]) => {
            this.addEnvironment(key, value);
        });
    }
    getConstructMetadata() {
        return {
            type: "Job",
            data: {},
        };
    }
    /** @internal */
    getFunctionBinding() {
        return {
            clientPackage: "job",
            variables: {
                functionName: {
                    environment: this._jobInvoker.functionName,
                    parameter: this._jobInvoker.functionName,
                },
            },
            permissions: {
                "lambda:*": [this._jobInvoker.functionArn],
            },
        };
    }
    /**
     * Use additional constructs
     *
     * @example
     * ```js
     * const STRIPE_KEY = new Config.Secret(stack, "STRIPE_KEY");
     *
     * job.use([STRIPE_KEY]);
     * ```
     */
    use(constructs) {
        this._jobInvoker.use(constructs);
        this.useForCodeBuild(constructs);
    }
    /**
     * Attaches additional configs to job
     *
     * @example
     * ```js
     * const STRIPE_KEY = new Config.Secret(stack, "STRIPE_KEY");
     *
     * job.addConfig([STRIPE_KEY]);
     * ```
     */
    addConfig(config) {
        const app = this.node.root;
        this.use(config);
        if (config.length > 0) {
            app.reportWarning("usingConfig");
        }
    }
    /**
     * Attaches the given list of [permissions](Permissions.md) to the job. This allows the job to access other AWS resources.
     *
     * @example
     * ```js
     * job.attachPermissions(["ses", bucket]);
     * ```
     */
    attachPermissions(permissions) {
        this._jobInvoker.attachPermissions(permissions);
        this.attachPermissionsForCodeBuild(permissions);
    }
    /**
     * Attaches additional environment variable to the job
     *
     * @example
     * ```js
     * fn.addEnvironment({
     *   DEBUG: "*"
     * });
     * ```
     */
    addEnvironment(name, value) {
        this._jobInvoker.addEnvironment(name, value);
        this.addEnvironmentForCodeBuild(name, value);
    }
    createCodeBuildProject() {
        const app = this.node.root;
        return new codebuild.Project(this, "JobProject", {
            vpc: this.props.cdk?.vpc,
            projectName: app.logicalPrefixedName(this.node.id),
            environment: {
                // CodeBuild offers different build images. The newer ones have much quicker
                // boot time. The latest build image is STANDARD_6_0, which support Node.js 16.
                // But while testing, I found STANDARD_6_0 took 100s to boot. So for the
                // purpose of this demo, I use STANDARD_5_0. It takes 30s to boot.
                //buildImage: codebuild.LinuxBuildImage.STANDARD_6_0,
                //buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
                buildImage: codebuild.LinuxBuildImage.fromDockerRegistry("amazon/aws-lambda-nodejs:16"),
                computeType: this.normalizeMemorySize(this.props.memorySize || "3 GB"),
            },
            environmentVariables: {
                SST_APP: { value: app.name },
                SST_STAGE: { value: app.stage },
            },
            timeout: this.normalizeTimeout(this.props.timeout || "8 hours"),
            buildSpec: codebuild.BuildSpec.fromObject({
                version: "0.2",
                phases: {
                    build: {
                        commands: [
                        // commands will be set after the code is built
                        ],
                    },
                },
            }),
        });
    }
    buildCodeBuildProjectCode() {
        const { handler, srcPath: srcPathRaw, enableLiveDev: enableLiveDevRaw, } = this.props;
        const srcPath = Function.normalizeSrcPath(srcPathRaw || ".");
        const bundle = { format: "esm" };
        const app = this.node.root;
        // Handle remove (ie. sst remove)
        if (app.skipBuild) {
            // do nothing
        }
        // Handle build
        else {
            DeferBuilder.addTask(async () => {
                // Build function
                const bundled = await Runtime.Handler.bundle({
                    id: this.localId,
                    root: app.appPath,
                    handler,
                    runtime: "nodejs16.x",
                    srcPath,
                    bundle,
                });
                // This should always be true b/c runtime is always Node.js
                if ("directory" in bundled) {
                    // handle copy files
                    Function.copyFiles(bundle, srcPath, bundled.directory);
                    // create wrapper that calls the handler
                    const [file, module] = bundled.handler.split(".");
                    await fs.writeFile(path.join(bundled.directory, "handler-wrapper.js"), [
                        `console.log("")`,
                        `console.log("//////////////////////")`,
                        `console.log("// Start of the job //")`,
                        `console.log("//////////////////////")`,
                        `console.log("")`,
                        `import { ${module} } from "./${file}.js";`,
                        `const event = JSON.parse(process.env.SST_PAYLOAD);`,
                        `const result = await ${module}(event);`,
                        `console.log("")`,
                        `console.log("----------------------")`,
                        `console.log("")`,
                        `console.log("Result:", result);`,
                        `console.log("")`,
                        `console.log("//////////////////////")`,
                        `console.log("//  End of the job  //")`,
                        `console.log("//////////////////////")`,
                        `console.log("")`,
                    ].join("\n"));
                    const code = lambda.AssetCode.fromAsset(bundled.directory);
                    this.updateCodeBuildProjectCode(code, "handler-wrapper.js");
                }
            });
        }
    }
    updateCodeBuildProjectCode(code, script) {
        // Update job's commands
        const codeConfig = code.bind(this);
        const project = this.job.node.defaultChild;
        project.source = {
            type: "S3",
            location: `${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
            buildSpec: [
                "version: 0.2",
                "phases:",
                "  build:",
                "    commands:",
                `      - node ${script}`,
            ].join("\n"),
        };
        this.attachPermissions([
            new iam.PolicyStatement({
                actions: ["s3:*"],
                effect: iam.Effect.ALLOW,
                resources: [
                    `arn:aws:s3:::${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
                ],
            }),
        ]);
    }
    createLocalInvoker() {
        const { srcPath, handler, config, environment, permissions } = this.props;
        // Note: make the invoker function the same ID as the Job
        //       construct so users can identify the invoker function
        //       in the Console.
        const fn = new Function(this, this.node.id, {
            srcPath,
            handler,
            bundle: { format: "esm" },
            runtime: "nodejs16.x",
            timeout: 10,
            memorySize: 1024,
            config,
            environment: {
                SST_DEBUG_TYPE: "job",
            },
            permissions,
        });
        fn._disableBind = true;
        return fn;
    }
    createCodeBuildInvoker() {
        return new Function(this, this.node.id, {
            srcPath: path.resolve(path.join(__dirname, "../dist/support/job-invoker")),
            handler: "index.main",
            runtime: "nodejs16.x",
            timeout: 10,
            memorySize: 1024,
            environment: {
                PROJECT_NAME: this.job.projectName,
            },
            permissions: [
                new iam.PolicyStatement({
                    effect: iam.Effect.ALLOW,
                    actions: ["codebuild:StartBuild"],
                    resources: [this.job.projectArn],
                }),
            ],
            bundle: {
                format: "esm",
            },
        });
    }
    useForCodeBuild(constructs) {
        const app = this.node.root;
        constructs.forEach(c => {
            // Bind environment
            const env = bindEnvironment(c);
            Object.entries(env).forEach(([key, value]) => this.addEnvironmentForCodeBuild(key, value));
            // Bind parameters
            bindParameters(c);
            // Bind permissions
            const permissions = bindPermissions(c);
            Object.entries(permissions).forEach(([action, resources]) => this.attachPermissionsForCodeBuild([new iam.PolicyStatement({
                    actions: [action],
                    effect: iam.Effect.ALLOW,
                    resources,
                })]));
        });
    }
    attachPermissionsForCodeBuild(permissions) {
        attachPermissionsToRole(this.job.role, permissions);
    }
    addEnvironmentForCodeBuild(name, value) {
        const project = this.job.node.defaultChild;
        const env = project.environment;
        const envVars = env.environmentVariables;
        envVars.push({ name, value });
    }
    normalizeMemorySize(memorySize) {
        if (memorySize === "3 GB") {
            return codebuild.ComputeType.SMALL;
        }
        else if (memorySize === "7 GB") {
            return codebuild.ComputeType.MEDIUM;
        }
        else if (memorySize === "15 GB") {
            return codebuild.ComputeType.LARGE;
        }
        else if (memorySize === "145 GB") {
            return codebuild.ComputeType.X2_LARGE;
        }
        throw new Error(`Invalid memory size value for the ${this.node.id} Job.`);
    }
    normalizeTimeout(timeout) {
        const value = toCdkDuration(timeout);
        if (value.toSeconds() < 5 * 60 || value.toSeconds() > 480 * 60) {
            throw new Error(`Invalid timeout value for the ${this.node.id} Job.`);
        }
        return value;
    }
}
