// 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 scrapes/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 argOut = process.argv[2] || "";
const version = resolveReleaseVersion();
const RELEASE_NAME = `Verbatim-Beta-${version}`;
const OUT_ROOT = path.resolve(ROOT, argOut || path.join("scrapes", "updates", RELEASE_NAME));

const ROOT_FILES = new Set([
  "package.json",
  "package-lock.json",
  "tsconfig.json",
  "power.bat",
  "Start Nodarama.vbs",
  "Start Nodarama Mac.applescript",

  // boot/server/runtime
  "power.ts",
  "deck.ts",
  "deckControl.ts",
  "controller.ts",
  "sessionState.ts",
  "session.ts",
  "projectScope.ts",
  "pathList.ts",
  "paths.ts",

  // recorder/execution/capture/restore/playback
  "recorder.ts",
  "manifestContract.ts",
  "capture.ts",
  "restoreCore.ts",
  "restore.ts",
  "recovery.ts",
  "playbackCore.ts",
  "playback.ts",
  "hashState.ts",
  "systemActions.ts",

  // project/settings/AI relay
  "mgrSNP.ts",
  "eq.ts",
  "manifold.ts",
  "sysMenu.ts",

  // UI/static root files
  "monitor.html",
  "monitor.css",
  "monitorTabs.css",
  "monitor.js",
  "monitorProject.js",
  "monitorLaunchGate.js",
  "mgrSNP.js",
  "sysMenu.js",
  "contextCopy.js",
  "editor.js",
  "projectOutline.js",
  "projectSwitch.js",
  "site.webmanifest"
]);

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

const BLOCKED_NAMES = new Set([
  ".auth_access",
  ".auth_refresh",
  ".auth_token",
  ".env",
  "settings.json",
  "settings.local.json",
  "user.json"
]);

const BLOCKED_DIRS = new Set([
  ".eq",
  ".obs",
  ".temp",
  ".git",
  ".vscode",
  "backups",
  "build",
  "dist",
  "logs",
  "node_modules",
  "restore-points",
  "scrapes",
  "settings"
]);

const BLOCKED_FILE_PATTERNS = [
  /^\.auth_/i,
  /^\.env/i,
  /\.local$/i,
  /\.log$/i,
  /\.zip$/i,
  /token/i,
  /refresh/i,
  /secret/i,
  /credential/i,
  /password/i,
  /private/i
];

const BLOCKED_REL_PATHS = new Set([
  "sysutil/launch-verbatim.js"
]);

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 }> = [];

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);
  const patch = parts[2] || 0;
  parts[2] = patch >= 10 ? Math.floor(patch / 10) + 1 : patch + 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 forcedIndex = process.argv.findIndex(arg => arg === "--version");
  const forced = forcedIndex >= 0 ? String(process.argv[forcedIndex + 1] || "").trim() : "";
  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_NAMES.has(name)) return true;
  return BLOCKED_FILE_PATTERNS.some(pattern => pattern.test(name));
}

function shouldCopyDir(name: string): boolean {
  return !BLOCKED_DIRS.has(name);
}

function copyFile(src: string, dest: string, rel: string): void {
  const normalizedRel = rel.replace(/\\/g, "/");
  if (BLOCKED_REL_PATHS.has(normalizedRel)) return;
  if (/^assets\/screenshot\s/i.test(normalizedRel)) return;
  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()) {
      if (!shouldCopyDir(entry.name)) continue;
      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 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: true
    },
    includedSpecialFiles: [
      "projects/system.json"
    ],
    excluded: [
      "projects/*",
      "settings.json",
      "auth/session/token files",
      "logs",
      "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 file of ROOT_FILES) {
    const src = path.join(ROOT, file);
    if (!fs.existsSync(src) || !fs.statSync(src).isFile()) continue;
    copyFile(src, path.join(OUT_ROOT, file), file);
  }
}

function copyRootDirs(): void {
  for (const dir of ROOT_DIRS) {
    const src = path.join(ROOT, dir);
    if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) continue;
    copyDir(src, path.join(OUT_ROOT, dir), dir);
  }
}

function copySystemProjectMetadata(): void {
  const src = path.join(ROOT, "projects", "system.json");
  if (!fs.existsSync(src)) return;
  copyFile(src, path.join(OUT_ROOT, "projects", "system.json"), "projects/system.json");
}
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 record = {
    version,
    channel: "beta",
    active: 1,
    mandatory: 0,
    auto_apply: 1,
    package_file: zipName,
    sha256: zipHash,
    release_notes: `Verbatim Beta ${version}`,
    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 {
  ensureCleanOutput();
  copyRootFiles();
  copyRootDirs();
  copySystemProjectMetadata();
  scrubCopiedTextFiles(OUT_ROOT);
  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();
