import { Buffer } from "node:buffer";
import { createHash } from "node:crypto";
import {
  existsSync,
  mkdtempSync,
  readFileSync,
  readdirSync,
  rmSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, relative, resolve, sep } from "node:path";
import { spawnSync } from "node:child_process";
import process from "node:process";
import { fileURLToPath } from "node:url";

const root = resolve(import.meta.dirname, "..");
const source = join(root, "packages", "language-server", "src", "server.ts");
const destination = join(root, "editors", "vscode", "src", "language-server.cjs");
const scriptPath = fileURLToPath(import.meta.url);
const check = process.argv.includes("--check");
const temporary = mkdtempSync(join(tmpdir(), "wrnexus-editor-lsp-"));
const output = join(temporary, "language-server.cjs");

function normalizeText(value) {
  return value.replace(/\r\n?/g, "\n");
}

function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

function walk(directory) {
  const files = [];
  for (const entry of readdirSync(directory)) {
    const path = join(directory, entry);
    const stat = statSync(path);
    if (stat.isDirectory()) files.push(...walk(path));
    else if (stat.isFile() && path.endsWith(".ts") && !path.endsWith(".test.ts")) files.push(path);
  }
  return files;
}

const sourceFiles = [
  ...walk(join(root, "packages", "language-server", "src")),
  ...walk(join(root, "packages", "syntax", "src")),
  ...walk(join(root, "packages", "typecheck", "src")),
  join(root, "packages", "language-server", "package.json"),
  join(root, "packages", "syntax", "package.json"),
  join(root, "packages", "typecheck", "package.json"),
].sort();

function fingerprint(files) {
  const hash = createHash("sha256");
  for (const file of files) {
    hash.update(relative(root, file).split(sep).join("/"));
    hash.update("\0");
    hash.update(normalizeText(readFileSync(file, "utf8")));
    hash.update("\0");
  }
  return hash.digest("hex");
}

const sourceHash = fingerprint(sourceFiles);
const generatorHash = sha256(normalizeText(readFileSync(scriptPath, "utf8")));

function makeNodeExecutable(bundle) {
  let value = normalizeText(bundle).replace(/^#!\/usr\/bin\/env bun\n/, "#!/usr/bin/env node\n");
  const bunWrapper = "(function(exports, require, module, __filename, __dirname) {";
  const invocation = "})(exports, require, module, __filename, __dirname);";
  if (
    value.includes("// @bun @bun-cjs") &&
    value.includes(bunWrapper) &&
    !value.trimEnd().endsWith(invocation)
  ) {
    value = value.replace(/\}\)\s*$/, `${invocation}\n`);
  }
  return value;
}

function stampBundle(bundle) {
  const withoutOldStamp = bundle
    .replace(/^\/\/ WRN editor language server source hash: [a-f0-9]{64}\n/m, "")
    .replace(/^\/\/ WRN editor language server generator hash: [a-f0-9]{64}\n/m, "");
  const lines = withoutOldStamp.split("\n");
  const insertion = lines[0]?.startsWith("#!") ? 1 : 0;
  lines.splice(
    insertion,
    0,
    `// WRN editor language server source hash: ${sourceHash}`,
    `// WRN editor language server generator hash: ${generatorHash}`,
  );
  return `${lines.join("\n").replace(/\n+$/, "")}\n`;
}

function rpc(value) {
  const body = JSON.stringify(value);
  return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
}

function verifyStarts(file) {
  const input = [
    rpc({ jsonrpc: "2.0", id: 1, method: "initialize", params: { rootPath: root } }),
    rpc({ jsonrpc: "2.0", id: 2, method: "shutdown", params: {} }),
    rpc({ jsonrpc: "2.0", method: "exit", params: {} }),
  ].join("");
  const result = spawnSync(process.execPath, [file], {
    cwd: root,
    input,
    encoding: "utf8",
    timeout: 10_000,
  });
  if (result.error) throw result.error;
  if (result.status !== 0 || !result.stdout.includes("WRNexus Language Server")) {
    throw new Error(
      `VS Code language server smoke test failed.\n${result.stderr || result.stdout || `exit ${result.status}`}`,
    );
  }
}

try {
  const built = spawnSync(
    process.env.WRNEXUS_BUN_BINARY || "bun",
    ["build", source, "--target=node", "--format=cjs", `--outfile=${output}`],
    { cwd: root, encoding: "utf8" },
  );
  if (built.status !== 0) throw new Error(built.stderr || built.stdout || "LSP bundle failed");

  const generated = stampBundle(makeNodeExecutable(readFileSync(output, "utf8")));
  writeFileSync(output, generated, "utf8");
  verifyStarts(output);

  if (check) {
    const existing = existsSync(destination)
      ? normalizeText(readFileSync(destination, "utf8"))
      : "";
    const embeddedSourceHash = existing.match(
      /^\/\/ WRN editor language server source hash: ([a-f0-9]{64})$/m,
    )?.[1];
    const embeddedGeneratorHash = existing.match(
      /^\/\/ WRN editor language server generator hash: ([a-f0-9]{64})$/m,
    )?.[1];
    const exact = existing === generated;
    const sourceCompatible =
      embeddedSourceHash === sourceHash && embeddedGeneratorHash === generatorHash;
    if (!exact && !sourceCompatible) {
      throw new Error(
        `${relative(root, destination)} is stale. Run bun run --cwd editors/vscode build and commit the result.`,
      );
    }
    verifyStarts(destination);
    process.stdout.write(
      `Verified ${relative(root, destination)} matches the language server sources and starts under Node.\n`,
    );
  } else {
    writeFileSync(destination, generated, "utf8");
    process.stdout.write(
      `Built ${relative(root, destination)} and verified its Node entrypoint.\n`,
    );
  }
} finally {
  rmSync(temporary, { recursive: true, force: true });
}
