import { createHash } from "node:crypto";
import { createReadStream, createWriteStream } from "node:fs";
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { pipeline } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
import { createRequire } from "node:module";

const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const siteDirectory = resolve(scriptDirectory, "..");
const projectDirectory = resolve(siteDirectory, "..");
const releaseFile = resolve(siteDirectory, "release.json");
const accessFile = resolve(siteDirectory, "release-access.conf");
const checksumFile = resolve(siteDirectory, "downloads", "SHA256SUMS");
const current = JSON.parse(await readFile(releaseFile, "utf8"));
const artifact = resolve(siteDirectory, "downloads", current.fileName);
const argumentsSet = new Set(process.argv.slice(2));
const require = createRequire(import.meta.url);

if (!["--check", "--write", "--require-publishable"].some((argument) => argumentsSet.has(argument))) {
  throw new Error("Usage: node audit-release.mjs --check | --write [--require-publishable]");
}

const hashFile = async (file) => {
  const hash = createHash("sha256");
  for await (const chunk of createReadStream(file)) hash.update(chunk);
  return hash.digest("hex");
};

const pipeCommandToFile = async (command, commandArguments, output) => {
  const child = spawn(command, commandArguments, { stdio: ["ignore", "pipe", "pipe"] });
  let stderr = "";
  child.stderr.setEncoding("utf8");
  child.stderr.on("data", (chunk) => {
    stderr += chunk;
  });
  const finished = new Promise((resolveExit, rejectExit) => {
    child.on("error", rejectExit);
    child.on("close", (code) => {
      if (code === 0) resolveExit();
      else rejectExit(new Error(`${basename(command)} failed with code ${code}: ${stderr.trim()}`));
    });
  });
  await Promise.all([pipeline(child.stdout, createWriteStream(output)), finished]);
};

const extractApplicationArchive = async (temporary, appArchive) => {
  if (artifact.toLowerCase().endsWith(".zip")) {
    await pipeCommandToFile("unzip", ["-p", artifact, "resources/app.asar"], appArchive);
    return;
  }

  if (!artifact.toLowerCase().endsWith(".exe")) {
    throw new Error(`Unsupported launcher artifact: ${basename(artifact)}`);
  }

  const { getPath7za } = require(resolve(projectDirectory, "windows-launcher/node_modules/app-builder-lib/out/toolsets/7zip.js"));
  const sevenZip = await getPath7za();
  const portablePayload = join(temporary, "app-64.7z");
  await pipeCommandToFile(sevenZip, ["e", "-so", artifact, "$PLUGINSDIR/app-64.7z"], portablePayload);
  await pipeCommandToFile(sevenZip, ["e", "-so", portablePayload, "resources/app.asar"], appArchive);
};

const inspectEmbeddedConfiguration = async () => {
  const temporary = await mkdtemp(join(tmpdir(), "bglands-launcher-site-"));
  try {
    const appArchive = join(temporary, "app.asar");
    await extractApplicationArchive(temporary, appArchive);
    const asar = require(resolve(projectDirectory, "windows-launcher/node_modules/@electron/asar"));
    const rawConfig = asar.extractFile(appArchive, "dist/resources/launcher.config.json");
    return JSON.parse(rawConfig.toString("utf8"));
  } finally {
    await rm(temporary, { recursive: true, force: true });
  }
};

const metadata = await stat(artifact);
const sha256 = await hashFile(artifact);
const additionalDownloads = await Promise.all((current.additionalDownloads ?? []).map(async (download) => {
  if (basename(download.fileName) !== download.fileName) {
    throw new Error(`Unsafe additional download name: ${download.fileName}`);
  }
  const file = resolve(siteDirectory, "downloads", download.fileName);
  const [fileMetadata, fileSha256] = await Promise.all([stat(file), hashFile(file)]);
  return {
    label: String(download.label),
    fileName: download.fileName,
    url: `./downloads/${download.fileName}`,
    sizeBytes: fileMetadata.size,
    sizeLabel: `${Math.round(fileMetadata.size / 1024 / 1024)} Mo`,
    sha256: fileSha256,
  };
}));
const embedded = await inspectEmbeddedConfiguration();
const clientId = String(embedded?.microsoft?.clientId ?? "").trim();
const validClientId = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(clientId)
  && clientId !== "00000000-0000-0000-0000-000000000000";
const previewDistribution = current.distributionMode === "preview";
const minecraftApproved = current.minecraftApproval === "approved";
const downloadEnabled = validClientId || previewDistribution;
const statusMessage = validClientId
  ? minecraftApproved
    ? "Release officielle vérifiée et prête au téléchargement."
    : "AppID Microsoft BG LANDS intégré · approbation Minecraft Services en attente."
  : previewDistribution
    ? "Préversion configurable : l’administrateur peut maintenant ajouter l’AppID dans Paramètres → Connexion Microsoft."
    : "La build publique Windows finalise sa connexion Microsoft officielle.";
const next = {
  ...current,
  downloadEnabled,
  distributionMode: validClientId && minecraftApproved ? "stable" : current.distributionMode ?? "blocked",
  version: embedded.launcherVersion,
  fileName: basename(artifact),
  url: `./downloads/${basename(artifact)}`,
  sizeBytes: metadata.size,
  sizeLabel: `${Math.round(metadata.size / 1024 / 1024)} Mo`,
  sha256,
  publishedAt: metadata.mtime.toISOString(),
  statusMessage,
  additionalDownloads,
};

const accessConfiguration = downloadEnabled
  ? `# Generated state: ${validClientId ? "verified launcher release" : "explicit preview release"} available.\nlocation /downloads/ {\n  include /etc/nginx/security-headers.conf;\n  add_header Content-Disposition 'attachment' always;\n  add_header Cache-Control "public, max-age=300" always;\n  try_files $uri =404;\n}\n`
  : `# Generated state: the current artifact is intentionally unavailable because\n# its embedded Microsoft Application ID is empty.\nlocation /downloads/ {\n  include /etc/nginx/security-headers.conf;\n  add_header Retry-After "3600" always;\n  add_header Cache-Control "no-store" always;\n  return 503;\n}\n`;

if (argumentsSet.has("--write")) {
  await Promise.all([
    writeFile(releaseFile, `${JSON.stringify(next, null, 2)}\n`),
    writeFile(accessFile, accessConfiguration),
    writeFile(checksumFile, [
      `${sha256}  ${basename(artifact)}`,
      ...additionalDownloads.map((download) => `${download.sha256}  ${download.fileName}`),
      "",
    ].join("\n")),
  ]);
}

if (argumentsSet.has("--check")) {
  const mismatches = [];
  if (current.sizeBytes !== metadata.size) mismatches.push("sizeBytes");
  if (current.sizeLabel !== next.sizeLabel) mismatches.push("sizeLabel");
  if (current.sha256 !== sha256) mismatches.push("sha256");
  if (current.version !== embedded.launcherVersion) mismatches.push("version");
  if (current.downloadEnabled !== downloadEnabled) mismatches.push("downloadEnabled");
  if (JSON.stringify(current.additionalDownloads ?? []) !== JSON.stringify(additionalDownloads)) mismatches.push("additionalDownloads");
  const currentAccess = await readFile(accessFile, "utf8");
  if (currentAccess !== accessConfiguration) mismatches.push("release-access.conf");
  if (mismatches.length > 0) {
    throw new Error(`Release metadata out of sync: ${mismatches.join(", ")}. Run npm run prepare:release.`);
  }
}

if (argumentsSet.has("--require-publishable") && (!validClientId || !minecraftApproved)) {
  throw new Error("Release blocked: Microsoft Application ID is missing or Minecraft Services approval is not confirmed.");
}

process.stdout.write(
  `${validClientId && minecraftApproved ? "PUBLISHABLE" : downloadEnabled ? "PREVIEW" : "BLOCKED"} launcher v${embedded.launcherVersion} · ${next.sizeLabel} · sha256 ${sha256}\n`,
);
