#!/bin/bash
# Uninstall the last Fuel Dispatch update and restore the previous version.
# Use this from the server console when an update takes the site down:
#   sudo bash /opt/fuel-dispatch/uninstall-last-update.sh
set -u
DEST="${FUEL_DEST:-/opt/fuel-dispatch}"
DATA="${FUEL_DATA:-/var/lib/fuel-dispatch}"
HIST="$DATA/program-history"
DL="$DEST/downloads"

echo "=== Uninstall last Fuel Dispatch update ==="
echo "Program folder: $DEST"
echo "Data folder:    $DATA"

if [ ! -d "$DEST" ]; then
  echo "Fuel Dispatch is not installed at $DEST"
  exit 1
fi

systemctl stop fuel-dispatch 2>/dev/null || true
sleep 1

USED=""
USED_BUILD=""

node - "$DEST" "$HIST" "$DL" <<'NODE'
const fs = require("fs");
const path = require("path");
const os = require("os");
const dest = process.argv[2];
const histDir = process.argv[3];
const dlDir = process.argv[4];

function copyDir(a, b) {
  fs.mkdirSync(b, { recursive: true });
  fs.readdirSync(a, { withFileTypes: true }).forEach(ent => {
    const s = path.join(a, ent.name);
    const d = path.join(b, ent.name);
    if (ent.isDirectory()) copyDir(s, d);
    else fs.copyFileSync(s, d);
  });
}

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

function programOk(file) {
  try {
    require("child_process").execFileSync(process.execPath, ["--check", file], { stdio: "pipe" });
    return true;
  } catch (e) {
    return false;
  }
}

function currentBuild() {
  try {
    const s = fs.readFileSync(path.join(dest, "server.cjs"), "utf8");
    const m = s.match(/const APP_BUILD\s*=\s*(\d+)/);
    return m ? Number(m[1]) : 0;
  } catch (e) {
    return 0;
  }
}

function asarFrom(buf) {
  if (buf.slice(0, 4).toString("ascii") === "FDUP") {
    const len = buf.readUInt32LE(4);
    return buf.slice(8 + len);
  }
  return buf;
}

function extractAsar(buf, tmp) {
  const headerPickleSize = buf.readUInt32LE(4);
  const jsonLen = buf.readUInt32LE(12);
  const header = JSON.parse(buf.slice(16, 16 + jsonLen).toString("utf8"));
  const dataOffset = 8 + headerPickleSize;
  const list = [];
  (function walk(files, prefix) {
    Object.keys(files || {}).forEach(name => {
      const node = files[name];
      const rel = prefix ? prefix + "/" + name : name;
      if (node && node.files) walk(node.files, rel);
      else if (node && node.size != null) list.push({ rel, offset: Number(node.offset), size: Number(node.size) });
    });
  })(header.files || header, "");
  list.forEach(f => {
    const out = path.join(tmp, f.rel);
    fs.mkdirSync(path.dirname(out), { recursive: true });
    fs.writeFileSync(out, buf.slice(dataOffset + f.offset, dataOffset + f.offset + f.size));
  });
}

function applyExtracted(tmp) {
  const server = path.join(tmp, "server.cjs");
  if (!fs.existsSync(server) || !programOk(server)) return false;
  ["server.cjs", "db.cjs", "main.cjs", "preload.cjs", "imapInbox.cjs", "package.json", "icon.png", "install-server.sh", "install-public.sh", "uninstall-last-update.sh"].forEach(name => {
    const src = path.join(tmp, name);
    if (fs.existsSync(src) && fs.statSync(src).isFile()) copyFile(src, path.join(dest, name));
  });
  try {
    fs.readdirSync(tmp).forEach(name => {
      if (!/\.cjs$/i.test(name)) return;
      const src = path.join(tmp, name);
      if (fs.existsSync(src) && fs.statSync(src).isFile()) copyFile(src, path.join(dest, name));
    });
  } catch (e) {}
  if (fs.existsSync(path.join(tmp, "renderer"))) copyDir(path.join(tmp, "renderer"), path.join(dest, "renderer"));
  const sql = path.join(tmp, "node_modules", "sql.js");
  if (fs.existsSync(sql)) copyDir(sql, path.join(dest, "node_modules", "sql.js"));
  return programOk(path.join(dest, "server.cjs"));
}

function snapshots() {
  if (!fs.existsSync(histDir)) return [];
  return fs.readdirSync(histDir).map(id => {
    const dir = path.join(histDir, id);
    try {
      if (!fs.statSync(dir).isDirectory()) return null;
    } catch (e) { return null; }
    let meta = {};
    try { meta = JSON.parse(fs.readFileSync(path.join(dir, "rollback.json"), "utf8")); } catch (e) {}
    return {
      id,
      dir,
      build: Number(meta.build) || Number(String(id).split("-")[0]) || 0,
      savedAt: meta.savedAt || "",
      hasServer: fs.existsSync(path.join(dir, "server.cjs"))
    };
  }).filter(s => s && s.hasServer).sort((a, b) => String(b.savedAt).localeCompare(String(a.savedAt)) || b.build - a.build);
}

function fduList() {
  if (!fs.existsSync(dlDir)) return [];
  return fs.readdirSync(dlDir).filter(n => /^FuelDispatchUpdate-(?:test-)?v\d+\.fdu$/i.test(n)).map(name => {
    const build = Number((name.match(/v(\d+)/i) || [])[1]) || 0;
    return { name, build, path: path.join(dlDir, name) };
  }).filter(x => x.build > 0).sort((a, b) => b.build - a.build);
}

const nowBuild = currentBuild();
console.log("Current build: " + (nowBuild || "unknown"));

const snaps = snapshots();
const prevSnap = snaps.find(s => !nowBuild || s.build < nowBuild) || snaps[0];
if (prevSnap) {
  console.log("Restoring saved version " + prevSnap.build + " from " + prevSnap.id);
  if (applyExtracted(prevSnap.dir) && programOk(path.join(dest, "server.cjs"))) {
    console.log("USED=snapshot:" + prevSnap.build);
    process.exit(0);
  }
  console.log("Saved version did not pass the program check. Trying an update file…");
}

const fdus = fduList().filter(u => !nowBuild || u.build < nowBuild);
for (const u of fdus) {
  console.log("Trying update file " + u.name);
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fuel-uninst-"));
  try {
    extractAsar(asarFrom(fs.readFileSync(u.path)), tmp);
    if (applyExtracted(tmp) && programOk(path.join(dest, "server.cjs"))) {
      console.log("USED=fdu:" + u.build);
      try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (e) {}
      process.exit(0);
    }
  } catch (err) {
    console.log("  skipped: " + (err && err.message ? err.message : err));
  }
  try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (e) {}
}

console.error("Could not restore a previous version. No usable snapshot or update file was found.");
process.exit(2);
NODE
RC=$?

if [ "$RC" -ne 0 ]; then
  echo "Uninstall failed. The current program files were left as they are."
  systemctl start fuel-dispatch 2>/dev/null || true
  exit "$RC"
fi

id -u fueldispatch >/dev/null 2>&1 && chown -R fueldispatch:fueldispatch "$DEST" "$DATA" 2>/dev/null || true

systemctl daemon-reload 2>/dev/null || true
systemctl start fuel-dispatch
sleep 2
systemctl restart caddy 2>/dev/null || true
sleep 1

echo
echo "=== Service status ==="
systemctl --no-pager --full status fuel-dispatch | head -n 20 || true
echo
echo "=== Local check ==="
curl -sS -m 4 http://127.0.0.1:8080/api/status || echo DOWN
echo
echo "Done. Open the site again. If it still fails, run:"
echo "  sudo bash $DL/recover-fuel-dispatch.sh"
