import url from "url";
import path from "path";
import fs from "fs/promises";
import { Construct } from "constructs";
import { Duration as CdkDuration, IgnoreMode } from "aws-cdk-lib/core";
import { Platform } from "aws-cdk-lib/aws-ecr-assets";
import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
import { AssetCode, Code, Runtime, Function as CdkFunction, } from "aws-cdk-lib/aws-lambda";
import { Project, LinuxBuildImage, BuildSpec, ComputeType, } from "aws-cdk-lib/aws-codebuild";
import { RetentionDays, LogRetention } from "aws-cdk-lib/aws-logs";
import { Stack } from "./Stack.js";
import { Function, useFunctions, } from "./Function.js";
import { toCdkDuration } from "./util/duration.js";
import { attachPermissionsToRole } from "./util/permission.js";
import { bindEnvironment, bindPermissions, getReferencedSecrets, } from "./util/functionBinding.js";
import { useDeferredTasks } from "./deferred_task.js";
import { useProject } from "../project.js";
import { useRuntimeHandlers } from "../runtime/handlers.js";
import { Colors } from "../cli/colors.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 "sst/constructs";
 *
 * new Cron(stack, "Cron", {
 *   schedule: "rate(1 minute)",
 *   job: "src/lambda.main",
 * });
 * ```
 */
export class Job extends Construct {
    id;
    props;
    job;
    liveDevJob;
    _jobManager;
    constructor(scope, id, props) {
        super(scope, props.cdk?.id || id);
        const app = this.node.root;
        const stack = Stack.of(scope);
        this.id = id;
        this.props = props;
        const isLiveDevEnabled = app.mode === "dev" && (this.props.enableLiveDev === false ? false : true);
        this.validateContainerProps();
        this.validateMemoryProps();
        this.job = this.createCodeBuildJob();
        if (!stack.isActive) {
            this._jobManager = this.createJobManager();
        }
        else if (isLiveDevEnabled) {
            this.liveDevJob = this.createLiveDevJob();
            this._jobManager = this.createJobManager();
        }
        else {
            this._jobManager = this.createJobManager();
            this.buildCodeBuildProjectCode();
        }
        this.createLogRetention();
        this.attachPermissions(props.permissions || []);
        this.bind(props.bind || []);
        Object.entries(props.environment || {}).forEach(([key, value]) => {
            this.addEnvironment(key, value);
        });
        useFunctions().add(this.node.addr, {
            ...props,
            runtime: this.convertJobRuntimeToFunctionRuntime(),
        });
    }
    getConstructMetadata() {
        return {
            type: "Job",
            data: {
                handler: this.props.handler,
            },
        };
    }
    /** @internal */
    getFunctionBinding() {
        return {
            clientPackage: "job",
            variables: {
                functionName: {
                    type: "plain",
                    value: this._jobManager.functionName,
                },
            },
            permissions: {
                "lambda:*": [this._jobManager.functionArn],
            },
        };
    }
    /**
     * Binds additional resources to job.
     *
     * @example
     * ```js
     * job.bind([STRIPE_KEY, bucket]);
     * ```
     */
    bind(constructs) {
        this.liveDevJob?.bind(constructs);
        this.bindForCodeBuild(constructs);
    }
    /**
     * 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"]);
     * ```
     */
    attachPermissions(permissions) {
        this.liveDevJob?.attachPermissions(permissions);
        this.attachPermissionsForCodeBuild(permissions);
    }
    /**
     * Attaches additional environment variable to the job.
     *
     * @example
     * ```js
     * fn.addEnvironment({
     *   DEBUG: "*"
     * });
     * ```
     */
    addEnvironment(name, value) {
        this.liveDevJob?.addEnvironment(name, value);
        this.addEnvironmentForCodeBuild(name, value);
    }
    createCodeBuildJob() {
        const { cdk, runtime, handler, memorySize, timeout, container } = this.props;
        const app = this.node.root;
        return new Project(this, "JobProject", {
            projectName: app.logicalPrefixedName(this.node.id),
            environment: {
                computeType: this.normalizeMemorySize(memorySize || "3 GB"),
            },
            environmentVariables: {
                SST_APP: { value: app.name },
                SST_STAGE: { value: app.stage },
                SST_SSM_PREFIX: { value: useProject().config.ssmPrefix },
            },
            timeout: this.normalizeTimeout(timeout || "8 hours"),
            buildSpec: BuildSpec.fromObject({
                version: "0.2",
                phases: {
                    build: {
                        commands: [
                        // commands will be set after the code is built
                        ],
                    },
                },
            }),
            vpc: cdk?.vpc,
            securityGroups: cdk?.securityGroups,
            subnetSelection: cdk?.vpcSubnets,
        });
    }
    createLiveDevJob() {
        // 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, {
            ...this.props,
            runtime: this.convertJobRuntimeToFunctionRuntime(),
            memorySize: 1024,
            timeout: "10 seconds",
            environment: {
                ...this.props.environment,
                SST_DEBUG_JOB: "true",
            },
        });
        fn._doNotAllowOthersToBind = true;
        return fn;
    }
    createLogRetention() {
        const { logRetention } = this.props;
        if (!logRetention)
            return;
        new LogRetention(this, "LogRetention", {
            logGroupName: `/aws/codebuild/${this.job.projectName}`,
            retention: RetentionDays[logRetention.toUpperCase()],
            logRetentionRetryOptions: {
                maxRetries: 100,
            },
        });
    }
    buildCodeBuildProjectCode() {
        const { handler, architecture, runtime, container } = this.props;
        useDeferredTasks().add(async () => {
            if (runtime === "container")
                Colors.line(`➜  Building the container image for the "${this.node.id}" job...`);
            // Build function
            const result = await useRuntimeHandlers().build(this.node.addr, "deploy");
            if (result.type === "error") {
                throw new Error([`Failed to build job "${handler}"`, ...result.errors].join("\n"));
            }
            // No need to update code for container runtime
            // Note: we could set the commands in `createCodeBuildJob` but
            //       in `sst dev`, we want to avoid changing the CodeBuild resources
            //       when `cmd` changes.
            if (runtime === "container") {
                const image = LinuxBuildImage.fromAsset(this, "ContainerImage", {
                    directory: handler,
                    platform: architecture === "arm_64"
                        ? Platform.custom("linux/arm64")
                        : Platform.custom("linux/amd64"),
                    file: container?.file,
                    exclude: [".sst"],
                    ignoreMode: IgnoreMode.GLOB,
                });
                image.repository?.grantPull(this.job.role);
                const project = this.job.node.defaultChild;
                project.environment = {
                    ...project.environment,
                    type: architecture === "arm_64" ? "ARM_CONTAINER" : "LINUX_CONTAINER",
                    image: image.imageId,
                    imagePullCredentialsType: "SERVICE_ROLE",
                };
                project.source = {
                    type: "NO_SOURCE",
                    buildSpec: [
                        "version: 0.2",
                        "phases:",
                        "  build:",
                        "    commands:",
                        `      - ${container.cmd
                            .map((arg) => (arg.includes(" ") ? `"${arg}"` : arg))
                            .join(" ")}`,
                    ].join("\n"),
                };
                return;
            }
            // Create wrapper that calls the handler
            const parsed = path.parse(result.handler);
            const importName = parsed.ext.substring(1);
            const importPath = `./${path
                .join(parsed.dir, parsed.name)
                .split(path.sep)
                .join(path.posix.sep)}.mjs`;
            await fs.writeFile(path.join(result.out, "handler-wrapper.mjs"), [
                `console.log("")`,
                `console.log("//////////////////////")`,
                `console.log("// Start of the job //")`,
                `console.log("//////////////////////")`,
                `console.log("")`,
                `import { ${importName} } from "${importPath}";`,
                `const event = JSON.parse(process.env.SST_PAYLOAD);`,
                `const result = await ${importName}(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("")`,
                `process.exit(0)`,
            ].join("\n"));
            // Update job's commands
            const code = AssetCode.fromAsset(result.out);
            const codeConfig = code.bind(this);
            const project = this.job.node.defaultChild;
            const image = LinuxBuildImage.fromDockerRegistry(
            // ARM images can be found here https://hub.docker.com/r/amazon/aws-lambda-nodejs
            architecture === "arm_64"
                ? "amazon/aws-lambda-nodejs:16.2023.07.13.14"
                : "amazon/aws-lambda-nodejs:16");
            project.environment = {
                ...project.environment,
                type: architecture === "arm_64" ? "ARM_CONTAINER" : "LINUX_CONTAINER",
                image: image.imageId,
            };
            image.repository?.grantPull(this.job.role);
            project.source = {
                type: "S3",
                location: `${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
                buildSpec: [
                    "version: 0.2",
                    "phases:",
                    "  build:",
                    "    commands:",
                    `      - node handler-wrapper.mjs`,
                ].join("\n"),
            };
            this.attachPermissions([
                new PolicyStatement({
                    actions: ["s3:*"],
                    effect: Effect.ALLOW,
                    resources: [
                        `arn:${Stack.of(this).partition}:s3:::${codeConfig.s3Location?.bucketName}/${codeConfig.s3Location?.objectKey}`,
                    ],
                }),
            ]);
        });
    }
    createJobManager() {
        return new CdkFunction(this, "Manager", {
            code: Code.fromAsset(path.join(__dirname, "../support/job-manager/")),
            handler: "index.handler",
            runtime: Runtime.NODEJS_16_X,
            timeout: CdkDuration.seconds(10),
            memorySize: 1024,
            environment: {
                SST_JOB_PROVIDER: this.liveDevJob ? "lambda" : "codebuild",
                SST_JOB_RUNNER: this.liveDevJob
                    ? this.liveDevJob.functionArn
                    : this.job.projectName,
            },
            initialPolicy: [
                this.liveDevJob
                    ? new PolicyStatement({
                        effect: Effect.ALLOW,
                        actions: ["lambda:InvokeFunction"],
                        resources: [this.liveDevJob.functionArn],
                    })
                    : new PolicyStatement({
                        effect: Effect.ALLOW,
                        actions: ["codebuild:StartBuild", "codebuild:StopBuild"],
                        resources: [this.job.projectArn],
                    }),
            ],
        });
    }
    bindForCodeBuild(constructs) {
        // 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.addEnvironmentForCodeBuild(key, value));
            // Bind permissions
            const permissions = bindPermissions(c);
            Object.entries(permissions).forEach(([action, resources]) => this.attachPermissionsForCodeBuild([
                new PolicyStatement({
                    actions: [action],
                    effect: 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 });
    }
    validateContainerProps() {
        const { runtime, container } = this.props;
        if (runtime === "container") {
            if (!container) {
                throw new Error(`No commands defined for the ${this.node.id} Job.`);
            }
        }
    }
    validateMemoryProps() {
        const { architecture, memorySize } = this.props;
        if (architecture === "arm_64") {
            if (memorySize === "7 GB" || memorySize === "145 GB") {
                throw new Error(`ARM architecture only supports "3 GB" and "15 GB" memory sizes for the ${this.node.id} Job.`);
            }
        }
    }
    normalizeMemorySize(memorySize) {
        if (memorySize === "3 GB") {
            return ComputeType.SMALL;
        }
        else if (memorySize === "7 GB") {
            return ComputeType.MEDIUM;
        }
        else if (memorySize === "15 GB") {
            return ComputeType.LARGE;
        }
        else if (memorySize === "145 GB") {
            return 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;
    }
    convertJobRuntimeToFunctionRuntime() {
        const { runtime } = this.props;
        return runtime === "container" ? "container" : "nodejs16.x";
    }
}
