import { copyFile, readFile, rm, writeFile } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import * as esbuild from "esbuild";
import { globSync } from "glob";

import pkg from "./package.json" with { type: "json" };

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const SRC_DIR = join(__dirname, "src");
const OUT_DIR = join(__dirname, "lib");

/**
 * Clean the output directory before building.
 *
 * @type {esbuild.Plugin}
 */
const cleanPlugin = {
  name: "clean",
  setup(build) {
    build.onStart(async () => {
      await rm(OUT_DIR, { recursive: true, force: true });
    });
  },
};

/**
 * Copy defaults.json to the output directory since other projects depend on it.
 *
 * @type {esbuild.Plugin}
 */
const copyDefaultsPlugin = {
  name: "copy-defaults",
  setup(build) {
    build.onEnd(async () => {
      await rm(join(OUT_DIR, "defaults.json"), {
        force: true,
      });
      await copyFile(
        join(SRC_DIR, "defaults.json"),
        join(OUT_DIR, "defaults.json"),
      );
    });
  },
};

/**
 * Log when the build ends.
 *
 * @type {esbuild.Plugin}
 */
const onEndPlugin = {
  name: "on-end",
  setup(build) {
    build.onEnd((result) => {
      // eslint-disable-next-line no-console
      console.log(`Build ended with ${result.errors.length} errors`);
    });
  },
};

/** The name of the virtual `entry-points` module. */
const SHARED_ENTRYPOINT = "entry-points";

/** The property name under which `upload-lib`'s namespace is exposed in `entry-points`. */
const UPLOAD_LIB_EXPORT = "uploadLib";

/** The relative source path of the `upload-lib` module that we re-export from `entry-points`. */
const UPLOAD_LIB_SRC = "./src/upload-lib";

/**
 * This plugin finds all source files that contain Action entry points. It then generates the
 * virtual `entry-points` module which imports all identified files, and re-exports their
 * `runWrapper` functions with suitable aliases.
 *
 * The virtual module additionally re-exports `upload-lib` under the `uploadLib` namespace so that
 * external consumers can access it via the small `lib/upload-lib.js` stub emitted below.
 * 
 * A tiny stub file is emitted for each Action entrypoint, and one for `upload-lib`. Each stub
 * imports the shared bundle and calls/re-exports from the respective entry point.
 *
 * @type {esbuild.Plugin}
 */
const entryPointsPlugin = {
  name: "entry-points",
  setup(build) {
    const namespace = "actions";
    const actions = [];

    const toPascal = (s) =>
      s.replace(/(^|-)([a-z0-9])/gi, (_, __, c) => c.toUpperCase());

    // Find the source files containing Action entry points.
    build.onStart(() => {
      const actionFiles = globSync("src/*-action{,-post}.ts");
      for (const actionFile of actionFiles) {
        const match = basename(actionFile).match(/(.*)-action(-post)?/);

        if (match.length < 2) {
          throw new Error(`'${actionFile}' didn't match expected pattern.`);
        }

        const actionName = match[1];
        const isPost = match[2] !== undefined;

        actions.push({
          path: actionFile,
          name: actionName,
          isPost,
          pascalCaseName: `${toPascal(actionName)}${isPost ? "Post" : ""}Action`,
        });
      }
    });

    // Resolve the virtual `entry-points` file and set the corresponding namespace.
    // Ideally, we'd `RegExp.escape` the entrypoint here, but that API isn't supported in Node 20.
    // Since we're dealing with a hardcoded string, this isn't too much of a problem.
    build.onResolve({ filter: new RegExp(`^${SHARED_ENTRYPOINT}$`) }, () => {
      return { path: SHARED_ENTRYPOINT, namespace };
    });

    // Generate the virtual `entry-points` file based on the Actions we discovered.
    // Restrict using the namespace. The path filter does not need to discriminate any further.
    build.onLoad({ filter: /.*/, namespace }, async () => {
      const wrapperTemplatePath = "entry-wrapper.js.tpl";
      const wrapperTemplate = await readFile(
        join(SRC_DIR, wrapperTemplatePath),
        "utf-8",
      );

      const actionsSorted = actions.sort((a, b) =>
        a.name.localeCompare(b.name),
      );
      const imports = actionsSorted
        .map(
          (action) =>
            `import * as ${action.pascalCaseName} from "./src/${basename(action.path)}";`,
        )
        .join("\n");
      const wrappers = actionsSorted
        .map((action) =>
          wrapperTemplate.replaceAll("__ACTION__", action.pascalCaseName),
        )
        .join("\n\n");

      // Also re-export the `upload-lib` namespace so that external consumers can reach it
      // via the `lib/upload-lib.js` stub without us having to bundle a second copy.
      const uploadLibReExport = `export * as ${UPLOAD_LIB_EXPORT} from "${UPLOAD_LIB_SRC}";`;

      return {
        contents: `"use strict";\n${imports}\n\n${uploadLibReExport}\n\n${wrappers}\n`,
        resolveDir: ".",
        loader: "ts",
      };
    });

    // Emit entry point stubs for each Action using the entry template.
    build.onEnd(async () => {
      const makeHeader = (templatePath, sourceFile) =>
        `// Automatically generated from '${templatePath}' for 'src/${basename(sourceFile)}'.\n\n`;

      // Read the entry point template.
      const actionTemplatePath = "action-entry.js.tpl";
      const actionTemplate = await readFile(
        join(SRC_DIR, actionTemplatePath),
        "utf-8",
      );

      // Write entry point stubs for each Action.
      for (const action of actions) {
        await writeFile(
          join(
            OUT_DIR,
            `${action.name}${action.isPost ? "-post" : ""}-entry.js`,
          ),
          makeHeader(actionTemplatePath, action.path) +
            actionTemplate.replaceAll("__ACTION__", action.pascalCaseName),
        );
      }

      // Write a small stub for `upload-lib` that re-exports it from the shared bundle.
      // External callers (e.g. internal testing environments) `require("./lib/upload-lib")`
      // and expect the same shape as before, so we expose the namespace as `module.exports`.
      const uploadLibStubTemplatePath = "upload-lib-stub.js.tpl";
      const uploadLibStubTemplate = await readFile(
        join(SRC_DIR, uploadLibStubTemplatePath),
        "utf-8",
      );
      await writeFile(
        join(OUT_DIR, "upload-lib.js"),
        makeHeader(uploadLibStubTemplatePath, `${UPLOAD_LIB_SRC}.ts`) +
          uploadLibStubTemplate.replaceAll(
            "__UPLOAD_LIB_EXPORT__",
            UPLOAD_LIB_EXPORT,
          ),
      );
    });
  },
};

const context = await esbuild.context({
  entryPoints: [{ in: SHARED_ENTRYPOINT, out: SHARED_ENTRYPOINT }],
  bundle: true,
  format: "cjs",
  outdir: OUT_DIR,
  platform: "node",
  external: ["./entry-points"],
  plugins: [cleanPlugin, copyDefaultsPlugin, entryPointsPlugin, onEndPlugin],
  target: ["node20"],
  define: {
    __CODEQL_ACTION_VERSION__: JSON.stringify(pkg.version),
  },
  metafile: true,
});

const result = await context.rebuild();
await writeFile(join(__dirname, "meta.json"), JSON.stringify(result.metafile));

await context.dispose();
