// sysutil/auto-system-update.ts
// Startup updater for beta/prerelease system overlays.
// Checks the Nodarama update manifest, downloads a newer zip, verifies it,
// extracts it, applies the release manifest, writes update state, and can relaunch.

import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { spawn, spawnSync } from "node:child_process";
import { requestUpdateManifest } from "../hypauth/hypauthcall";
import { logSystem } from "../playbackCore";
import {
  ROOT,
  TEMP_STATE_DIR,
  UPDATE_STATE_PATH
} from "../pathList";

const UPDATE_DIR = path.join(TEMP_STATE_DIR, "updates");
export type StartupUpdateResult = {
  ok: boolean;
  checked: boolean;
  updateAvailable: boolean;
  applied: boolean;
  restartRequired: boolean;
  version?: string;
  localVersion?: string;
  channel?: string;
  message: string;
  error?: string;
};

type UpdateManifestResponse = {
  ok?: boolean;
  updateAvailable?: boolean;
  latestVersion?: string;
  version?: string;
  channel?: string;
  packageUrl?: string;
  zipUrl?: string;
  downloadUrl?: string;
  sha256?: string;
  packageSha256?: string;
  zipSha256?: string;
  mode?: string;
  autoApply?: boolean;
  required?: boolean;
  message?: string;
  notes?: string;
};

function ensureUpdateDirs(): void {
  fs.mkdirSync(UPDATE_DIR, { recursive: true });
}

function writeUpdateState(state: Record<string, any>): void {
  try {
    fs.mkdirSync(TEMP_STATE_DIR, { recursive: true });
    fs.writeFileSync(UPDATE_STATE_PATH, JSON.stringify({ ...state, ts: Date.now() }, null, 2), "utf8");
  } catch {}
}

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 versionParts(value: string): number[] {
  return String(value || "0.0.0")
    .replace(/^v/i, "")
    .split(/[.-]/)
    .map(part => Number.parseInt(part.replace(/\D/g, "") || "0", 10));
}

function isNewerVersion(remote: string, local: string): boolean {
  const a = versionParts(remote);
  const b = versionParts(local);
  const length = Math.max(a.length, b.length, 3);
  for (let i = 0; i < length; i++) {
    const av = a[i] || 0;
    const bv = b[i] || 0;
    if (av > bv) return true;
    if (av < bv) return false;
  }
  return false;
}

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

async function downloadFile(url: string, outPath: string): Promise<void> {
  const response = await fetch(url);
  if (!response.ok || !response.body) throw new Error(`download_failed_${response.status}`);
  const buffer = Buffer.from(await response.arrayBuffer());
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
  fs.writeFileSync(outPath, buffer);
}

function removeDir(folder: string): void {
  fs.rmSync(folder, { recursive: true, force: true });
}

function extractZip(zipPath: string, outDir: string): void {
  removeDir(outDir);
  fs.mkdirSync(outDir, { recursive: true });

  if (process.platform === "win32") {
    const ps = spawnSync("powershell.exe", [
      "-NoProfile",
      "-ExecutionPolicy", "Bypass",
      "-Command",
      `Expand-Archive -LiteralPath ${JSON.stringify(zipPath)} -DestinationPath ${JSON.stringify(outDir)} -Force`
    ], { cwd: ROOT, encoding: "utf8", windowsHide: true });
    if (ps.status !== 0) throw new Error(`extract_failed: ${ps.stderr || ps.stdout || "powershell Expand-Archive failed"}`);
    return;
  }

  const unzip = spawnSync("unzip", ["-oq", zipPath, "-d", outDir], { cwd: ROOT, encoding: "utf8" });
  if (unzip.status !== 0) throw new Error(`extract_failed: ${unzip.stderr || unzip.stdout || "unzip failed"}`);
}

function findExtractedUpdateRoot(folder: string): string {
  const directManifest = path.join(folder, "release-manifest.json");
  if (fs.existsSync(directManifest)) return folder;

  const entries = fs.readdirSync(folder, { withFileTypes: true }).filter(entry => entry.isDirectory());
  if (entries.length === 1) {
    const nested = path.join(folder, entries[0].name);
    if (fs.existsSync(path.join(nested, "release-manifest.json"))) return nested;
  }

  throw new Error("release-manifest.json not found after extraction");
}

function applyExtractedUpdate(updateRoot: string): void {
  const result = spawnSync("npx", ["tsx", "sysutil/apply-system-update.ts", updateRoot], {
    cwd: ROOT,
    encoding: "utf8",
    shell: process.platform === "win32",
    windowsHide: true
  });
  if (result.status !== 0) {
    throw new Error(`apply_failed: ${result.stderr || result.stdout || "apply-system-update failed"}`);
  }
}

export function relaunchNodarama(): void {
  const command = process.platform === "win32" ? "cmd.exe" : "npm";
  const args = process.platform === "win32"
    ? ["/c", "start", "Nodarama", "cmd", "/c", "npm run power:on"]
    : ["run", "power:on"];

  const child = spawn(command, args, {
    cwd: ROOT,
    detached: true,
    stdio: "ignore",
    shell: false,
    windowsHide: true
  });
  child.unref();
}

export async function runStartupSystemUpdate(options?: { channel?: string; autoApply?: boolean; relaunch?: boolean; }): Promise<StartupUpdateResult> {
  const channel = options?.channel || "beta";
  const localVersion = readPackageVersion();

  try {
    ensureUpdateDirs();
    writeUpdateState({ phase: "checking", message: "Checking for Nodarama updates", channel, localVersion });
    logSystem("Update", { event: "startup_update_check", channel, localVersion });

    const manifest = await requestUpdateManifest(channel) as UpdateManifestResponse;
    const remoteVersion = String(manifest.latestVersion || manifest.version || "").trim();
    const packageUrl = String(manifest.packageUrl || manifest.zipUrl || manifest.downloadUrl || "").trim();
    const expectedHash = String(manifest.sha256 || manifest.packageSha256 || manifest.zipSha256 || "").trim().toLowerCase();
    const serverSaysAvailable = Boolean(manifest.updateAvailable);
    const newer = remoteVersion ? isNewerVersion(remoteVersion, localVersion) : false;
    const updateAvailable = Boolean(manifest.ok !== false && packageUrl && expectedHash && (serverSaysAvailable || newer));

    if (!updateAvailable) {
      writeUpdateState({ phase: "current", message: "Nodarama is up to date", channel, localVersion, remoteVersion });
      return { ok: true, checked: true, updateAvailable: false, applied: false, restartRequired: false, localVersion, version: remoteVersion, channel, message: "up_to_date" };
    }

    const shouldApply = options?.autoApply !== false && (manifest.autoApply !== false || manifest.required || manifest.mode === "auto" || channel === "beta");
    if (!shouldApply) {
      writeUpdateState({ phase: "available", message: "Update available", channel, localVersion, version: remoteVersion });
      return { ok: true, checked: true, updateAvailable: true, applied: false, restartRequired: false, localVersion, version: remoteVersion, channel, message: "update_available" };
    }

    const safeVersion = remoteVersion.replace(/[^a-zA-Z0-9._-]/g, "_") || "latest";
    const zipPath = path.join(UPDATE_DIR, `nv-system-v${safeVersion}.zip`);
    const extractDir = path.join(UPDATE_DIR, `nv-system-v${safeVersion}`);

    writeUpdateState({ phase: "downloading", message: `Downloading Nodarama ${remoteVersion}`, channel, localVersion, version: remoteVersion });
    logSystem("Update", { event: "startup_update_download", version: remoteVersion, channel });
    await downloadFile(packageUrl, zipPath);

    writeUpdateState({ phase: "verifying", message: "Verifying update package", channel, localVersion, version: remoteVersion });
    const actualHash = sha256(zipPath).toLowerCase();
    if (actualHash !== expectedHash) throw new Error("zip_sha256_mismatch");

    writeUpdateState({ phase: "extracting", message: "Extracting update package", channel, localVersion, version: remoteVersion });
    extractZip(zipPath, extractDir);
    const updateRoot = findExtractedUpdateRoot(extractDir);

    writeUpdateState({ phase: "applying", message: "Installing system update", channel, localVersion, version: remoteVersion });
    applyExtractedUpdate(updateRoot);

    writeUpdateState({ phase: "restart_required", message: "Update installed. Restarting Nodarama", channel, localVersion, version: remoteVersion, restartRequired: true });
    logSystem("Update", { event: "startup_update_applied", version: remoteVersion, channel, restartRequired: true });

    if (options?.relaunch) relaunchNodarama();

    return { ok: true, checked: true, updateAvailable: true, applied: true, restartRequired: true, localVersion, version: remoteVersion, channel, message: "update_applied_restart_required" };
  } catch (err: any) {
    const error = err?.message || String(err);
    writeUpdateState({ phase: "failed", message: "Update failed; continuing with current version", channel, localVersion, error });
    logSystem("Update", { event: "startup_update_failed", channel, localVersion, error });
    return { ok: false, checked: true, updateAvailable: false, applied: false, restartRequired: false, localVersion, channel, message: "update_failed_continuing", error };
  }
}
