import path from "path";
import fs from "fs/promises";
import os from "os";
import { useRuntimeHandlers } from "../handlers.js";
import { useRuntimeWorkers } from "../workers.js";
import { Context } from "../../context/context.js";
import { spawn } from "child_process";
import { useRuntimeServerConfig } from "../server.js";
import { existsAsync, findBelow, isChild } from "../../util/fs.js";
import { useProject } from "../../project.js";
import { execAsync } from "../../util/process.js";
import url from "url";
import AdmZip from "adm-zip";
export const useJavaHandler = Context.memo(async () => {
    const workers = await useRuntimeWorkers();
    const server = await useRuntimeServerConfig();
    const handlers = useRuntimeHandlers();
    const processes = new Map();
    const sources = new Map();
    const runningBuilds = new Map();
    handlers.register({
        shouldBuild: (input) => {
            if (!input.file.endsWith(".java"))
                return false;
            const parent = sources.get(input.functionID);
            if (!parent)
                return false;
            return isChild(parent, input.file);
        },
        canHandle: (input) => input.startsWith("java"),
        startWorker: async (input) => {
            const proc = spawn(`java`, [
                `-cp`,
                [
                    url.fileURLToPath(new URL("../../support/java-runtime/release/*", import.meta.url)),
                ].join(os.platform() === "win32" ? ";" : ":"),
                "com.amazonaws.services.lambda.runtime.api.client.AWSLambda",
                input.handler,
            ], {
                env: {
                    ...process.env,
                    ...input.environment,
                    IS_LOCAL: "true",
                    AWS_LAMBDA_RUNTIME_API: `localhost:${server.port}/${input.workerID}`,
                },
                cwd: input.out,
            });
            proc.on("exit", () => workers.exited(input.workerID));
            proc.stdout.on("data", (data) => {
                workers.stdout(input.workerID, data.toString());
            });
            proc.stderr.on("data", (data) => {
                workers.stdout(input.workerID, data.toString());
            });
            processes.set(input.workerID, proc);
        },
        stopWorker: async (workerID) => {
            const proc = processes.get(workerID);
            if (proc) {
                proc.kill();
                processes.delete(workerID);
            }
        },
        build: async (input) => {
            const project = useProject();
            const srcPath = await findBelow(project.paths.root, "build.gradle");
            const buildBinary = await getGradleBinary(srcPath);
            const buildTask = input.props.java?.buildTask || "build";
            const outputDir = input.props.java?.buildOutputDir || "distributions";
            sources.set(input.functionID, srcPath);
            async function build() {
                // build
                await execAsync(`${buildBinary} ${buildTask} -Dorg.gradle.logging.level=${process.env.DEBUG ? "debug" : "lifecycle"}`, {
                    cwd: srcPath,
                });
                // unzip
                const buildOutput = path.join(srcPath, "build", outputDir);
                const zip = (await fs.readdir(buildOutput)).find((f) => f.endsWith(".zip"));
                await new Promise((resolve, reject) => {
                    const zipper = new AdmZip(path.join(buildOutput, zip));
                    zipper.extractAllToAsync(input.out, false, false, (err) => err ? reject(err) : resolve(undefined));
                });
            }
            try {
                // Run gradle build once per directory. Otherwise they'll interfere
                // with one another
                const buildPromise = runningBuilds.get(buildBinary) ?? build();
                runningBuilds.set(buildBinary, buildPromise);
                await buildPromise;
                runningBuilds.delete(buildBinary);
                return {
                    type: "success",
                    handler: input.props.handler,
                };
            }
            catch (ex) {
                return {
                    type: "error",
                    errors: [ex.stderr],
                };
            }
        },
    });
});
async function getGradleBinary(srcPath) {
    // Use a gradle wrapper if provided in the folder, otherwise fall back
    // to system "gradle"
    const gradleWrapperPath = path.resolve(path.join(srcPath, "gradlew"));
    return (await existsAsync(gradleWrapperPath)) ? gradleWrapperPath : "gradle";
}
