// sysutil/export-system-release.ts
// Creates a system-only Nodarama Verbatim release/update copy.
// It excludes user projects, personal identity, auth/session state, logs, scrapes, backups, restore points, node_modules, and build output.
// Usage:
//   npx tsx sysutil/export-system-release.ts
//   npx tsx sysutil/export-system-release.ts logs/system/updates/nv-system-v1.0.1

import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { spawnSync } from "node:child_process";

const ROOT = path.resolve(__dirname, "..");
const cliArgs = process.argv.slice(2);
const version = resolveReleaseVersion();
const argOut = resolveOutputArg();
const RELEASE_NAME = `Verbatim-Beta-${version}`;
const OUT_ROOT = path.resolve(ROOT, argOut || path.join("logs", "system", "updates", RELEASE_NAME));

function isVersionArg(value: string): boolean {
  return /^\d+\.\d+\.\d+$/.test(String(value || "").trim());
}

function resolveForcedVersionArg(): string {
  const forcedIndex = cliArgs.findIndex(arg => arg === "--version");
  if (forcedIndex >= 0) return String(cliArgs[forcedIndex + 1] || "").trim();
  const positionalVersion = cliArgs.find(arg => isVersionArg(arg));
  return String(positionalVersion || "").trim();
}

function resolveOutputArg(): string {
  for (let index = 0; index < cliArgs.length; index++) {
    const arg = cliArgs[index];
    if (arg === "--version") {
      index++;
      continue;
    }
    if (arg.startsWith("--")) continue;
    if (isVersionArg(arg)) continue;
    return arg;
  }
  return "";
}

const BLOCKED_FILES = new Set([
  ".gitignore",
  "visualizer.js"
]);

const ALLOWED_SUBFOLDERS = new Set([
  "assets",
  "hypauth",
  "docs",
  "sysutil"
]);

const TEXT_EXTENSIONS = new Set([
  ".css", ".html", ".js", ".json", ".md", ".mjs", ".ts", ".txt", ".yml", ".yaml", ".svg"
]);

const manifestFiles: Array<{ path: string; bytes: number; sha256: string }> = [];

type ReleaseCheckResult = {
  ok: boolean;
  status: "passed" | "warnings" | "blockers" | "error";
  exitCode: number | null;
  passCount: number;
  warnCount: number;
  failCount: number;
  summary: string;
  recommendation: string;
  output: string;
};

let releaseCheckResult: ReleaseCheckResult | null = null;

function readPackageVersion(): string {
  try {
    const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"));
    return String(pkg?.version || "0.0.0");
  } catch {
    return "0.0.0";
  }
}

function writeJsonFile(filePath: string, value: any): void {
  fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
}

function bumpExportVersion(current: string): string {
  const parts = String(current || "0.0.0").replace(/^v/i, "").split(".").map(part => Number.parseInt(part.replace(/\D/g, "") || "0", 10));
  while (parts.length < 3) parts.push(0);
  const patch = parts[2] || 0;
  parts[2] = patch < 10 ? Number(`${patch}1`) : patch + 1;
  return `${parts[0]}.${parts[1]}.${parts[2]}`;
}

function bumpPublishVersion(current: string): string {
  const parts = String(current || "0.0.0").replace(/^v/i, "").split(".").map(part => Number.parseInt(part.replace(/\D/g, "") || "0", 10));
  while (parts.length < 3) parts.push(0);
  parts[2] = (parts[2] || 0) + 1;
  return `${parts[0]}.${parts[1]}.${parts[2]}`;
}

function bumpMilestoneVersion(current: string): string {
  const parts = String(current || "0.0.0").replace(/^v/i, "").split(".").map(part => Number.parseInt(part.replace(/\D/g, "") || "0", 10));
  while (parts.length < 3) parts.push(0);
  parts[1] = parts[1] + 1;
  parts[2] = 1;
  return `${parts[0]}.${parts[1]}.${parts[2]}`;
}

function syncPackageLockVersion(nextVersion: string): void {
  const lockPath = path.join(ROOT, "package-lock.json");
  if (!fs.existsSync(lockPath)) return;
  try {
    const lock = JSON.parse(fs.readFileSync(lockPath, "utf8"));
    lock.version = nextVersion;
    if (lock.packages?.[""]) lock.packages[""].version = nextVersion;
    writeJsonFile(lockPath, lock);
  } catch {}
}

function resolveReleaseVersion(): string {
  const forced = resolveForcedVersionArg();
  const noBump = process.argv.includes("--no-bump");
  const publish = process.argv.includes("--publish");
  const milestone = process.argv.includes("--milestone") || process.argv.includes("--official");

  const pkgPath = path.join(ROOT, "package.json");
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
  const current = String(pkg?.version || "0.0.0");
  const next = forced || (noBump ? current : milestone ? bumpMilestoneVersion(current) : publish ? bumpPublishVersion(current) : bumpExportVersion(current));
  pkg.version = next;
  writeJsonFile(pkgPath, pkg);
  syncPackageLockVersion(next);
  return next;
}

function ensureCleanOutput(): void {
  if (OUT_ROOT === ROOT || !OUT_ROOT.startsWith(ROOT)) {
    throw new Error(`Refusing unsafe output folder: ${OUT_ROOT}`);
  }
  fs.rmSync(OUT_ROOT, { recursive: true, force: true });
  fs.mkdirSync(OUT_ROOT, { recursive: true });
}

function isBlockedFileName(name: string): boolean {
  if (BLOCKED_FILES.has(name)) return true;
  if (/^Verbatim-Beta-.*\.(zip|update-record\.json|update\.sql)$/i.test(name)) return true;
  if (name === "VerbatimRecordingContract.txt") return true;
  return false;
}
function copyFile(src: string, dest: string, rel: string): void {
  const name = path.basename(src);
  if (isBlockedFileName(name)) return;

  fs.mkdirSync(path.dirname(dest), { recursive: true });
  fs.copyFileSync(src, dest);

  const stat = fs.statSync(dest);
  const sha256 = crypto.createHash("sha256").update(fs.readFileSync(dest)).digest("hex");
  manifestFiles.push({ path: rel.replace(/\\/g, "/"), bytes: stat.size, sha256 });
}

function copyDir(src: string, dest: string, relBase: string): void {
  if (!fs.existsSync(src)) return;

  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
    const childSrc = path.join(src, entry.name);
    const childDest = path.join(dest, entry.name);
    const childRel = path.join(relBase, entry.name);

    if (entry.isDirectory()) {
      copyDir(childSrc, childDest, childRel);
      continue;
    }

    if (entry.isFile()) copyFile(childSrc, childDest, childRel);
  }
}

function scrubTextFile(filePath: string): void {
  const ext = path.extname(filePath).toLowerCase();
  if (!TEXT_EXTENSIONS.has(ext)) return;

  let text = fs.readFileSync(filePath, "utf8");
  text = text.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted-email]");
  text = text.replace(/https:\/\/[a-z0-9-]+\.ngrok-free\.dev/gi, "[redacted-bridge-url]");
  text = text.replace(/u_[a-f0-9]{16,}/gi, "[redacted-user-id]");
  fs.writeFileSync(filePath, text, "utf8");
}

function scrubCopiedTextFiles(folder: string): void {
  for (const entry of fs.readdirSync(folder, { withFileTypes: true })) {
    const target = path.join(folder, entry.name);
    if (entry.isDirectory()) {
      scrubCopiedTextFiles(target);
      continue;
    }
    if (entry.isFile()) scrubTextFile(target);
  }
}

function refreshManifestHashes(): void {
  manifestFiles.length = 0;

  function walk(folder: string, relBase = ""): void {
    for (const entry of fs.readdirSync(folder, { withFileTypes: true })) {
      if (entry.name === "release-manifest.json") continue;
      const target = path.join(folder, entry.name);
      const rel = path.join(relBase, entry.name).replace(/\\/g, "/");
      if (entry.isDirectory()) {
        walk(target, rel);
        continue;
      }
      if (!entry.isFile()) continue;
      const stat = fs.statSync(target);
      const sha256 = crypto.createHash("sha256").update(fs.readFileSync(target)).digest("hex");
      manifestFiles.push({ path: rel, bytes: stat.size, sha256 });
    }
  }

  walk(OUT_ROOT);
  manifestFiles.sort((a, b) => a.path.localeCompare(b.path));
}

function parseReleaseCheckCounts(output: string): { passCount: number; warnCount: number; failCount: number } {
  const match = output.match(/PASS\s+(\d+)\s+·\s+WARN\s+(\d+)\s+·\s+FAIL\s+(\d+)/i);
  if (!match) return { passCount: 0, warnCount: 0, failCount: 0 };
  return {
    passCount: Number(match[1] || 0),
    warnCount: Number(match[2] || 0),
    failCount: Number(match[3] || 0)
  };
}

function runReleaseCheck(): ReleaseCheckResult {
  const command = process.platform === "win32"
    ? "npm run release:check"
    : "npm run release:check";
  const result = spawnSync(command, {
    cwd: ROOT,
    encoding: "utf8",
    shell: true,
    windowsHide: true
  });

  const output = [result.stdout || "", result.stderr || ""].filter(Boolean).join("\n").trim();
  const exitCode = typeof result.status === "number" ? result.status : null;
  const counts = parseReleaseCheckCounts(output);
  const failedToRun = Boolean(result.error || exitCode === null);
  const hasBlockers = counts.failCount > 0 || failedToRun;
  const hasWarnings = counts.warnCount > 0;
  const status: ReleaseCheckResult["status"] = failedToRun ? "error" : hasBlockers ? "blockers" : hasWarnings ? "warnings" : "passed";
  const summary = failedToRun
    ? `release check could not run: ${result.error?.message || "unknown error"}`
    : `release check ${status}: PASS ${counts.passCount} · WARN ${counts.warnCount} · FAIL ${counts.failCount}`;
  const recommendation = hasBlockers
    ? "Recommended action: fix release-check blockers, then run UPDATE again. Export continued so the update artifact is available for review only."
    : hasWarnings
      ? "Recommended action: review release-check warnings before publishing. Export continued."
      : "Release check passed. No preflight correction recommended.";

  return {
    ok: !hasBlockers,
    status,
    exitCode,
    ...counts,
    summary,
    recommendation,
    output: output || "[no release-check output captured]"
  };
}
function writeReleaseCheckReport(): void {
  if (!releaseCheckResult) return;
  const report = [
    "Nodarama Verbatim release preflight",
    `Version: ${version}`,
    `Status: ${releaseCheckResult.status}`,
    `Exit code: ${releaseCheckResult.exitCode ?? "none"}`,
    releaseCheckResult.summary,
    releaseCheckResult.recommendation,
    "",
    "Raw release-check output:",
    releaseCheckResult.output,
    ""
  ].join("\n");

  fs.writeFileSync(path.join(OUT_ROOT, "release-check-report.txt"), report, "utf8");
}

function releaseCheckNotes(): string[] {
  if (!releaseCheckResult) return [];
  if (releaseCheckResult.status === "passed") return [releaseCheckResult.summary];
  return [
    `PRE-FLIGHT WARNING: ${releaseCheckResult.summary}`,
    releaseCheckResult.recommendation,
    "See release-check-report.txt inside the exported update folder."
  ];
}

function writeReleaseManifest(): void {
  const manifest = {
    product: "Nodarama Verbatim",
    packageType: "system-overlay",
    version,
    exportedAt: new Date().toISOString(),
    updateRules: {
      overwriteSystemFiles: true,
      preserveUserProjects: true,
      preserveSettings: true,
      preserveAuth: true,
      includeSystemProjectMetadata: false
    },
    includedSpecialFiles: [],
    releaseCheck: releaseCheckResult ? {
      status: releaseCheckResult.status,
      ok: releaseCheckResult.ok,
      exitCode: releaseCheckResult.exitCode,
      passCount: releaseCheckResult.passCount,
      warnCount: releaseCheckResult.warnCount,
      failCount: releaseCheckResult.failCount,
      summary: releaseCheckResult.summary,
      recommendation: releaseCheckResult.recommendation,
      report: "release-check-report.txt"
    } : null,
    excluded: [
      "projects/*",
      "settings.json",
      "auth/session/token files",
      "logs runtime data",
      "legacy top-level scrapes",
      "restore-points",
      "backups",
      "node_modules",
      "build/dist output"
    ],
    files: manifestFiles
  };

  fs.writeFileSync(
    path.join(OUT_ROOT, "release-manifest.json"),
    JSON.stringify(manifest, null, 2),
    "utf8"
  );
}

function copyRootFiles(): void {
  for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) {
    if (!entry.isFile()) continue;
    if (isBlockedFileName(entry.name)) continue;

    const src = path.join(ROOT, entry.name);
    copyFile(src, path.join(OUT_ROOT, entry.name), entry.name);
  }
}
function copyRootDirs(): void {
  for (const dir of ALLOWED_SUBFOLDERS) {
    const src = path.join(ROOT, dir);
    if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) continue;
    copyDir(src, path.join(OUT_ROOT, dir), dir);
  }
}
function copyChromeAuxExtension(): void {
  const auxSrc = path.join(ROOT, "projects", "ChromeAux");
  const auxDest = path.join(OUT_ROOT, "EXT", "ChromeAux");
  const auxRelBase = path.join("EXT", "ChromeAux");

  if (!fs.existsSync(auxSrc) || !fs.statSync(auxSrc).isDirectory()) return;

  for (const entry of fs.readdirSync(auxSrc, { withFileTypes: true })) {
    const childSrc = path.join(auxSrc, entry.name);
    const childDest = path.join(auxDest, entry.name);
    const childRel = path.join(auxRelBase, entry.name);

    if (entry.isFile()) {
      if (entry.name === "project.json") continue;

      fs.mkdirSync(path.dirname(childDest), { recursive: true });
      fs.copyFileSync(childSrc, childDest);

      const stat = fs.statSync(childDest);
      const sha256 = crypto.createHash("sha256").update(fs.readFileSync(childDest)).digest("hex");
      manifestFiles.push({ path: childRel.replace(/\\/g, "/"), bytes: stat.size, sha256 });
      continue;
    }

    if (entry.isDirectory() && entry.name === "assets") {
      copyDir(childSrc, childDest, childRel);
    }
  }
}

function copySystemProjectMetadata(): void {
  return;
}
function hashFile(filePath: string): string {
  return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}

function quoteSql(value: string): string {
  return `'${String(value || "").replace(/'/g, "''")}'`;
}

function createReleaseZip(): string {
  const zipPath = path.join(path.dirname(OUT_ROOT), `${RELEASE_NAME}.zip`);
  fs.rmSync(zipPath, { force: true });

  if (process.platform === "win32") {
    const command = `Compress-Archive -Path ${JSON.stringify(path.join(OUT_ROOT, "*"))} -DestinationPath ${JSON.stringify(zipPath)} -Force`;
    const result = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], { cwd: ROOT, encoding: "utf8", windowsHide: true });
    if (result.status !== 0) throw new Error(`zip failed: ${result.stderr || result.stdout || "Compress-Archive failed"}`);
    return zipPath;
  }

  const result = spawnSync("zip", ["-qr", zipPath, "."], { cwd: OUT_ROOT, encoding: "utf8" });
  if (result.status !== 0) throw new Error(`zip failed: ${result.stderr || result.stdout || "zip failed"}`);
  return zipPath;
}

function writeServerUpdateRegistration(zipPath: string): void {
  const zipName = path.basename(zipPath);
  const zipHash = hashFile(zipPath);
  const filesJson = JSON.stringify(manifestFiles.map(file => file.path));
  const base = path.dirname(OUT_ROOT);
  const notes = [`Verbatim Beta ${version}`, ...releaseCheckNotes()].join("\n\n");
  const record = {
    version,
    channel: "beta",
    active: 1,
    mandatory: 0,
    auto_apply: 1,
    package_file: zipName,
    sha256: zipHash,
    release_notes: notes,
    release_check: releaseCheckResult ? {
      status: releaseCheckResult.status,
      ok: releaseCheckResult.ok,
      exitCode: releaseCheckResult.exitCode,
      passCount: releaseCheckResult.passCount,
      warnCount: releaseCheckResult.warnCount,
      failCount: releaseCheckResult.failCount,
      recommendation: releaseCheckResult.recommendation,
      report: "release-check-report.txt"
    } : null,
    files: manifestFiles.map(file => file.path)
  };

  fs.writeFileSync(path.join(base, `${RELEASE_NAME}.update-record.json`), JSON.stringify(record, null, 2), "utf8");

  const sql = [
    "-- Upload the ZIP to the website assets folder first.",
    `-- ZIP: ${zipName}`,
    `-- SHA256: ${zipHash}`,
    "",
    "INSERT INTO updates (version, channel, release_notes, files, mandatory, active, package_file, sha256, auto_apply, released_at)",
    `VALUES (${quoteSql(version)}, 'beta', ${quoteSql(record.release_notes)}, ${quoteSql(filesJson)}, 0, 1, ${quoteSql(zipName)}, ${quoteSql(zipHash)}, 1, NOW())`,
    "ON DUPLICATE KEY UPDATE",
    "  channel = VALUES(channel),",
    "  release_notes = VALUES(release_notes),",
    "  files = VALUES(files),",
    "  mandatory = VALUES(mandatory),",
    "  active = VALUES(active),",
    "  package_file = VALUES(package_file),",
    "  sha256 = VALUES(sha256),",
    "  auto_apply = VALUES(auto_apply),",
    "  released_at = VALUES(released_at);",
    ""
  ].join("\n");

  fs.writeFileSync(path.join(base, `${RELEASE_NAME}.update.sql`), sql, "utf8");
}

function main(): void {
  releaseCheckResult = runReleaseCheck();
  console.log(releaseCheckResult.summary);
  if (releaseCheckResult.status !== "passed") console.warn(releaseCheckResult.recommendation);

  ensureCleanOutput();
  copyRootFiles();
  copyRootDirs();
  copyChromeAuxExtension();
  copySystemProjectMetadata();
  scrubCopiedTextFiles(OUT_ROOT);
  writeReleaseCheckReport();
  refreshManifestHashes();
  writeReleaseManifest();

  const zipPath = createReleaseZip();
  writeServerUpdateRegistration(zipPath);

  console.log(`export complete: ${OUT_ROOT}`);
  console.log(`zip complete: ${zipPath}`);
  console.log(`zip sha256: ${hashFile(zipPath)}`);
  console.log(`files: ${manifestFiles.length}`);
  console.log(`next: upload ${path.basename(zipPath)} to website assets, then run ${RELEASE_NAME}.update.sql against the updates table`);
}

main();
