import fs from "fs";
import os from "os";
import path from "path";
import crypto from "crypto";
import { build } from "esbuild";
import { fileURLToPath } from "url";
import { findSetupFile } from "../config/setup-loader.mjs";

const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
const SETUP_ENTRY = path.join(PACKAGE_ROOT, "lib", "setup", "index.mjs");
const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
const bundleCache = new Map();

export async function bundleK6File({
  productDir,
  serviceName,
  sourceFile,
}) {
  const absoluteSource = path.resolve(productDir, sourceFile);
  const bundleDir = path.join(productDir, ".testkit", "_bundles", serviceName || "shared");
  fs.mkdirSync(bundleDir, { recursive: true });

  const setupFile = findSetupFile(productDir);
  const cacheKey = await buildCacheKey(absoluteSource, setupFile);
  const cached = bundleCache.get(cacheKey);
  if (cached && fs.existsSync(cached)) {
    return cached;
  }

  const outputFile = path.join(
    bundleDir,
    `${path.basename(sourceFile, path.extname(sourceFile))}-${cacheKey.slice(0, 12)}.js`
  );
  const entryFile = path.join(
    bundleDir,
    `${path.basename(sourceFile, path.extname(sourceFile))}-${cacheKey.slice(0, 12)}.entry.mjs`
  );
  fs.writeFileSync(entryFile, buildBundleEntryModule({
    sourceFile: absoluteSource,
    setupFile,
  }));

  await build({
    absWorkingDir: bundleDir,
    bundle: true,
    entryPoints: [entryFile],
    format: "esm",
    legalComments: "none",
    outfile: outputFile,
    platform: "neutral",
    sourcemap: "inline",
    target: "es2020",
    plugins: [testkitPackageAliasPlugin()],
    external: [
      "k6",
      "k6/*",
    ],
  });

  bundleCache.set(cacheKey, outputFile);
  return outputFile;
}

async function buildCacheKey(sourceFile, setupFile = null) {
  const source = await fs.promises.readFile(sourceFile, "utf8");
  const packageJson = await fs.promises.readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8");
  const hash = crypto
    .createHash("sha256")
    .update(sourceFile)
    .update("\0")
    .update(source)
    .update("\0")
    .update(packageJson);

  if (setupFile && fs.existsSync(setupFile)) {
    hash.update("\0").update(setupFile).update("\0");
    hash.update(await fs.promises.readFile(setupFile, "utf8"));
  }

  return hash.digest("hex");
}

function testkitPackageAliasPlugin() {
  return {
    name: "testkit-package-alias",
    setup(buildApi) {
      buildApi.onResolve({ filter: /^@elench\/testkit(?:\/.*)?$/ }, (args) => {
        return {
          namespace: "file",
          path: resolvePackageSubpath(args.path),
        };
      });
    },
  };
}

function resolvePackageSubpath(specifier) {
  const subpath = specifier.slice("@elench/testkit".length);
  if (!subpath) return ROOT_ENTRY;
  if (subpath === "/setup") return SETUP_ENTRY;
  if (subpath === "/runtime") return RUNTIME_ENTRY;

  throw new Error(`Unsupported @elench/testkit import "${specifier}" in ${os.platform()}`);
}

function buildBundleEntryModule({ sourceFile, setupFile }) {
  const sourceImport = JSON.stringify(sourceFile);
  const setupRegistration = setupFile
    ? `
import * as repoSetupModule from ${JSON.stringify(setupFile)};
registerRepoSetup(repoSetupModule.default || repoSetupModule || null);
`
    : `
registerRepoSetup(null);
`;

  return `
import { registerRepoSetup } from "@elench/testkit/setup";
import * as suiteModule from ${sourceImport};
${setupRegistration}
const suite = normalizeTestkitSuite(suiteModule);
export const options = suite.options;
export function setup(...args) {
  return suite.setup(...args);
}
export default function exec(...args) {
  return suite.exec(...args);
}

function normalizeTestkitSuite(module) {
  const candidate = module?.default;
  if (!candidate || typeof candidate !== "object") {
    throw new Error(
      "testkit suite files must default-export the suite object returned by defineHttpSuite(...) or defineDalSuite(...). Example: export default defineHttpSuite(...) or const suite = defineHttpSuite(...); export default suite;"
    );
  }
  if (typeof candidate.exec !== "function") {
    throw new Error(
      "testkit suite default export must expose an exec(setupData) function"
    );
  }

  const setupFn = typeof candidate["setup"] === "function"
    ? (...args) => candidate.setup(...args)
    : () => null;

  return {
    options: candidate["options"],
    setup: setupFn,
    exec: (...args) => candidate.exec(...args),
  };
}
`;
}
