// 런타임 소프트업데이트 — 앱 실행 시 R2에서 최신 코드(server.mjs/lcu_personal.mjs/index.html/package.json)를
// 받아 userData 캐시에 원자적으로 설치하고, 그 디렉터리에서 서버를 띄운다. 어떤 실패든 번들 코드로 폴백(앱은 항상 켜짐).
// → 사소한 수정은 R2에 런타임만 올리면 설치된 앱이 다음 실행에 알아서 적용(재다운로드 불필요).
const https = require("https");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

const R2_BASE = "https://pub-05f47b0b01de41feaa96c0efaedde627.r2.dev/companion-runtime";
const RUNTIME_FILES = ["server.mjs", "lcu_personal.mjs", "public/index.html", "package.json"];

function httpGet(url, { timeout = 9000, binary = false } = {}) {
  return new Promise((resolve, reject) => {
    const req = https.get(url, res => {
      if (res.statusCode !== 200) { res.resume(); return reject(new Error("HTTP " + res.statusCode + " " + url)); }
      const chunks = [];
      res.on("data", c => chunks.push(c));
      res.on("end", () => resolve(binary ? Buffer.concat(chunks) : Buffer.concat(chunks).toString("utf8")));
    });
    req.on("error", reject);
    req.setTimeout(timeout, () => req.destroy(new Error("timeout " + url)));
  });
}
function sha256(buf) { return crypto.createHash("sha256").update(buf).digest("hex"); }
function readRev(p) { try { return parseInt(String(fs.readFileSync(p, "utf8")).trim(), 10) || 0; } catch { return 0; } }
function cacheComplete(dir) { try { return RUNTIME_FILES.every(n => fs.existsSync(path.join(dir, n))); } catch { return false; } }

// 반환: 서버를 띄울 디렉터리(원격 캐시 경로 또는 null=번들). 절대 throw 안 함.
async function resolveRuntimeDir(bundledDir, userDataDir) {
  const cacheDir = path.join(userDataDir, "runtime");
  const bundledRev = readRev(path.join(bundledDir, "runtime.rev"));
  const newerCacheOrNull = () => (readRev(path.join(cacheDir, "rev")) > bundledRev && cacheComplete(cacheDir)) ? cacheDir : null;
  try {
    const manifest = JSON.parse(await httpGet(R2_BASE + "/manifest.json", { timeout: 8000 }));
    const remoteRev = parseInt(manifest.rev, 10) || 0;
    if (remoteRev <= bundledRev) return newerCacheOrNull();           // 번들이 최신 → 번들(다운그레이드 방지)
    if (readRev(path.join(cacheDir, "rev")) >= remoteRev && cacheComplete(cacheDir)) return cacheDir; // 이미 받아둠
    const byHash = Object.fromEntries((manifest.files || []).map(f => [f.name, f.sha256]));
    const tmp = path.join(userDataDir, "runtime_tmp_" + remoteRev);
    fs.rmSync(tmp, { recursive: true, force: true });
    for (const name of RUNTIME_FILES) {                                // 임시 디렉터리에 전부 받고 해시 검증
      const buf = await httpGet(R2_BASE + "/" + name.split("/").map(encodeURIComponent).join("/"), { timeout: 12000, binary: true });
      if (byHash[name] && sha256(buf) !== byHash[name]) throw new Error("hash mismatch: " + name);
      const dest = path.join(tmp, name);
      fs.mkdirSync(path.dirname(dest), { recursive: true });
      fs.writeFileSync(dest, buf);
    }
    fs.writeFileSync(path.join(tmp, "rev"), String(remoteRev));
    fs.rmSync(cacheDir, { recursive: true, force: true });             // 원자적 스왑(완전히 받은 뒤에만 교체)
    fs.renameSync(tmp, cacheDir);
    return cacheDir;
  } catch (e) {
    console.error("[runtime_sync] remote sync skipped:", e && e.message || e);
    return newerCacheOrNull();                                         // 어떤 실패든 기존 유효 캐시 or 번들
  }
}
module.exports = { resolveRuntimeDir, RUNTIME_FILES, R2_BASE };
