const esbuild = require("esbuild");
const path = require("path");
const fs = require("fs");
const { execSync, spawn } = require("child_process");
const { pick } = require("ramda");
const { globSync } = require("glob");
const remapping = require("@jridgewell/remapping");
const {
  DEFAULT_VISUALIZER_TEMPLATE,
  NODE_MODULES_PKG_REGEX,
} = require("./constants");

// colors for console output
const consoleColors = {
  info: "\x1b[34m", // Blue
  success: "\x1b[32m", // Green
  warning: "\x1b[33m", // Yellow
  error: "\x1b[31m", // Red
  reset: "\x1b[0m", // Reset
};

const createDefinitions = process => {
  return Object.keys(process.env).reduce((acc, key) => {
    acc[`process.env.${key}`] = JSON.stringify(process.env[key]);
    return acc;
  }, {});
};

const entryPoint = file => path.join(process.cwd(), file);

const writeMetafile = (metafile, outdir) => {
  const metaFilePath = path.join(outdir, "meta.json");
  fs.writeFileSync(metaFilePath, JSON.stringify(metafile, null, 2));
  return metaFilePath;
};

const generateBundleReport = async ({ metaFilePath, outdir, template }) => {
  try {
    console.info(
      `${consoleColors.info}[bundle-analyzer]${consoleColors.reset} Generating bundle report...`
    );

    const htmlFilePath = path.join(outdir, "bundle-report.html");
    execSync(
      `npx esbuild-visualizer --metadata ${metaFilePath} --filename ${htmlFilePath} --template ${template}`,
      { stdio: "inherit" }
    );

    console.info(
      `${consoleColors.success}[bundle-analyzer]${consoleColors.reset} Bundle report generated at ${htmlFilePath}`
    );

    const command = process.platform === "win32" ? "start" : "open";
    spawn(command, [htmlFilePath], { detached: true, stdio: "ignore" });
    console.info(
      `${consoleColors.success}[bundle-analyzer]${consoleColors.reset} Bundle report opened in browser`
    );

    return htmlFilePath;
  } catch (error) {
    console.error(
      `${consoleColors.error}[bundle-analyzer]${consoleColors.reset} Failed to generate bundle report: ${error.message}`
    );
    throw error;
  }
};

const loadUpstreamMap = jsAbsPath => {
  const mapPath = jsAbsPath + ".map";
  if (!fs.existsSync(mapPath)) return null;

  try {
    const map = JSON.parse(fs.readFileSync(mapPath, "utf-8"));
    if (!map.sources?.length || map.sources.every(s => !s)) return null;

    return map;
  } catch {
    return null;
  }
};

const normalizeSourcePath = source => {
  const match = source.match(NODE_MODULES_PKG_REGEX);
  if (match) return `${match[1]}:${match[2]}`;

  return source;
};

const fixSourcemaps = outdir => {
  const mapFiles = globSync("**/*.map", {
    cwd: outdir,
    absolute: true,
    nodir: true,
  });

  if (!mapFiles.length) return;

  for (const mapFile of mapFiles) {
    let parsed;

    try {
      const raw = fs.readFileSync(mapFile, "utf-8");
      parsed = JSON.parse(raw);
    } catch {
      continue;
    }

    if (!parsed.sources?.length) continue;

    try {
      const mapDir = path.dirname(mapFile);

      const result = remapping(parsed, (sourcePath, ctx) => {
        if (!sourcePath) return null;

        const baseDir = ctx.importer
          ? path.dirname(path.resolve(mapDir, ctx.importer))
          : mapDir;
        const absPath = path.resolve(baseDir, sourcePath);
        if (!absPath.includes("node_modules/@bigbinary")) return null;

        return loadUpstreamMap(absPath);
      });

      result.sources = result.sources.map(normalizeSourcePath);

      fs.writeFileSync(mapFile, JSON.stringify(result));
    } catch {
      parsed.sources = parsed.sources.map(normalizeSourcePath);
      fs.writeFileSync(mapFile, JSON.stringify(parsed));
    }
  }
};

const build = async config => {
  try {
    console.info(
      `${consoleColors.info}[esbuild]${consoleColors.reset} Building...`
    );

    const entrypoint = process.argv.includes("--entrypoint")
      ? process.argv[process.argv.indexOf("--entrypoint") + 1]
      : null;

    const template = process.argv.includes("--template")
      ? process.argv[process.argv.indexOf("--template") + 1]
      : DEFAULT_VISUALIZER_TEMPLATE;

    const buildConfig = entrypoint
      ? { ...config, entryPoints: pick([entrypoint], config.entryPoints) }
      : config;

    const result = await esbuild.build(buildConfig);

    fixSourcemaps(buildConfig.outdir);

    if (result.metafile) {
      const metaFilePath = writeMetafile(result.metafile, buildConfig.outdir);
      await generateBundleReport({
        metaFilePath,
        outdir: buildConfig.outdir,
        template,
      });
    }

    console.info(
      `${consoleColors.success}[esbuild]${consoleColors.reset} Build successful`
    );
  } catch (error) {
    console.error(`Build error: ${error.message}`);
    const errorMessage = `${consoleColors.error}[esbuild]${consoleColors.reset} Build failed`;
    throw new Error(errorMessage);
  }
};

const watch = async config => {
  const ctx = await esbuild.context(config);
  await ctx.watch();
  await ctx.serve({
    servedir: config.outdir,
    port: parseInt(process.env.DEVSERVER_PORT || "8000"),
  });
};

module.exports = {
  consoleColors,
  createDefinitions,
  entryPoint,
  build,
  watch,
};
