/**
 * Signed release manifest + build attestation (ML-DSA-87).
 * Private key: SARMAD_RELEASE_PRIVATE_KEY_HEX env or vps_scheduler/release_private.key (gitignored).
 */
const fs = require("fs");
const path = require("path");

const ROOT = path.join(__dirname, "..");
const MANIFEST_FILE = path.join(ROOT, "vps_scheduler", "release_manifest.json");
const PRIVATE_FILE = path.join(ROOT, "vps_scheduler", "release_private.key");
const PUBLIC_FILE = path.join(ROOT, "vps_scheduler", "release_public.key");

/** @type {any} */
let cryptoMod = null;
/** @type {Uint8Array|null} */
let privateKey = null;
/** @type {Uint8Array|null} */
let publicKey = null;
/** @type {object|null} */
let cachedManifest = null;

function stableStringify(value) {
  if (value === null || typeof value !== "object") {
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) {
    return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
  }
  const keys = Object.keys(value).sort();
  return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}";
}

function canonicalBytes(obj) {
  return Buffer.from(stableStringify(obj), "utf8");
}

async function loadCrypto() {
  if (cryptoMod) return cryptoMod;
  const w = require(path.join(__dirname, "exe_wallet.js"));
  cryptoMod = {
    sign: w.sign,
    verify: w.verify,
    bytesToHex: w.bytesToHex,
    hexToBytes: w.hexToBytes,
    publicKeyToAddress: w.publicKeyToAddress,
  };
  return cryptoMod;
}

function loadPrivateKeyBytes() {
  const hex =
    process.env.SARMAD_RELEASE_PRIVATE_KEY_HEX ||
    (fs.existsSync(PRIVATE_FILE) ? fs.readFileSync(PRIVATE_FILE, "utf8").trim() : "");
  if (!hex) return null;
  return Buffer.from(hex.replace(/\s+/g, ""), "hex");
}

function loadPublicKeyBytes() {
  const hex =
    process.env.SARMAD_RELEASE_PUBLIC_KEY_HEX ||
    (fs.existsSync(PUBLIC_FILE) ? fs.readFileSync(PUBLIC_FILE, "utf8").trim() : "");
  if (!hex) return null;
  return Buffer.from(hex.replace(/\s+/g, ""), "hex");
}

async function ensurePrivateKey() {
  if (privateKey) return true;
  const priv = loadPrivateKeyBytes();
  if (!priv || !priv.length) return false;
  privateKey = new Uint8Array(priv);
  return true;
}

function getPublicKeyUint8() {
  if (publicKey) return publicKey;
  const pubFromEnv = loadPublicKeyBytes();
  if (pubFromEnv && pubFromEnv.length) {
    publicKey = new Uint8Array(pubFromEnv);
    return publicKey;
  }
  return null;
}

async function signPayload(obj) {
  const ok = await ensurePrivateKey();
  if (!ok) throw new Error("release private key not configured");
  const crypto = await loadCrypto();
  const data = new Uint8Array(canonicalBytes(obj));
  return crypto.bytesToHex(crypto.sign(data, privateKey));
}

async function verifyPayload(obj, signatureHex, pubHexOverride) {
  const crypto = await loadCrypto();
  let pk = pubHexOverride ? crypto.hexToBytes(String(pubHexOverride).replace(/\s+/g, "")) : getPublicKeyUint8();
  if (!pk || !pk.length) return { ok: false, error: "no release public key" };
  const sig = crypto.hexToBytes(String(signatureHex || "").replace(/\s+/g, ""));
  if (!sig.length) return { ok: false, error: "missing signature" };
  const data = new Uint8Array(canonicalBytes(obj));
  const ok = crypto.verify(data, sig, pk);
  return ok ? { ok: true } : { ok: false, error: "bad release signature" };
}

async function signBuildAttestation(version, platform) {
  const payload = { kind: "build", platform: String(platform), version: String(version) };
  const signature = await signPayload(payload);
  return { payload, signature };
}

async function verifyBuildAttestation(version, platform, signatureHex) {
  const payload = { kind: "build", platform: String(platform), version: String(version) };
  return verifyPayload(payload, signatureHex);
}

function readManifestFile() {
  try {
    if (!fs.existsSync(MANIFEST_FILE)) return null;
    return JSON.parse(fs.readFileSync(MANIFEST_FILE, "utf8"));
  } catch (_) {
    return null;
  }
}

function writeManifestFile(manifest) {
  fs.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
  cachedManifest = manifest;
}

async function verifyManifest(manifest, pubHexOverride) {
  if (!manifest || typeof manifest !== "object") {
    return { ok: false, error: "missing manifest" };
  }
  const sig = manifest.signature;
  const body = { ...manifest };
  delete body.signature;
  return verifyPayload(body, sig, pubHexOverride);
}

async function getManifest() {
  if (cachedManifest) return cachedManifest;
  cachedManifest = readManifestFile();
  return cachedManifest;
}

async function buildAndSignManifest(opts) {
  const version = String(opts.version || "").trim();
  if (!version) throw new Error("version required");
  const attestAndroid = await signBuildAttestation(version, "android");
  const attestWindows = await signBuildAttestation(version, "windows");
  const manifest = {
    version,
    minVersion: String(opts.minVersion || version),
    publishedAt: Date.now(),
    artifacts: {
      android: {
        url: String(opts.apkUrl || `https://sarmad.pro/downloads/SRMD${version}.apk`),
        sha256: String(opts.apkSha256 || "").toLowerCase(),
      },
      windows: {
        url: String(opts.exeUrl || `https://sarmad.pro/downloads/SRMD-Coordinator-${version}.zip`),
        sha256: String(opts.exeSha256 || "").toLowerCase(),
      },
    },
    buildAttestations: {
      android: attestAndroid.signature,
      windows: attestWindows.signature,
    },
  };
  manifest.signature = await signPayload(manifest);
  writeManifestFile(manifest);
  return manifest;
}

async function verifyClientHeartbeat(body) {
  const strict = process.env.SARMAD_RELEASE_STRICT !== "0";
  const address = String(body.address || "");
  const publicKeyHex = String(body.publicKey || "");
  const signatureHex = String(body.signature || "");
  const clientVersion = String(body.clientVersion || "").trim();
  const platform = String(body.platform || "").trim().toLowerCase();
  const timestampMs = Number(body.timestampMs || 0);
  const buildAttestation = String(body.buildAttestation || "").trim();

  if (!address) return { ok: false, error: "address required" };

  if (!strict) {
    if (!publicKeyHex) return { ok: false, error: "publicKey required" };
    return { ok: true, legacy: true };
  }

  if (!publicKeyHex || !signatureHex || !clientVersion || !platform || !timestampMs || !buildAttestation) {
    return { ok: false, error: "signed client fields required (update to latest SRMD)" };
  }

  const skew = Number(process.env.SARMAD_HEARTBEAT_MAX_SKEW_MS || 5 * 60 * 1000);
  if (Math.abs(Date.now() - timestampMs) > skew) {
    return { ok: false, error: "heartbeat timestamp out of range" };
  }

  const manifest = await getManifest();
  const minVersion = manifest && manifest.minVersion ? String(manifest.minVersion) : null;
  if (minVersion && compareVersions(clientVersion, minVersion) < 0) {
    return { ok: false, error: "client version too old" };
  }

  const att = await verifyBuildAttestation(clientVersion, platform, buildAttestation);
  if (!att.ok) return att;

  const payload = {
    address,
    publicKey: publicKeyHex,
    chainHeight: Number(body.chainHeight || 0),
    tipHash: String(body.tipHash || ""),
    tipTimestamp: Number(body.tipTimestamp || 0),
    producingEnabled: body.producingEnabled !== false,
    clientVersion,
    platform,
    timestampMs,
    buildAttestation,
  };

  const crypto = await loadCrypto();
  const pk = crypto.hexToBytes(publicKeyHex);
  const sig = crypto.hexToBytes(signatureHex);
  if (!pk.length || !sig.length) return { ok: false, error: "bad key or signature encoding" };
  const derived = crypto.publicKeyToAddress(pk);
  if (derived !== address) return { ok: false, error: "address/pubkey mismatch" };
  const data = new Uint8Array(canonicalBytes(payload));
  if (!crypto.verify(data, sig, pk)) return { ok: false, error: "bad heartbeat signature" };
  return { ok: true };
}

function parseParts(v) {
  return String(v || "")
    .trim()
    .replace(/^v/i, "")
    .split(".")
    .map((x) => {
      const n = parseInt(x, 10);
      return Number.isFinite(n) ? n : 0;
    });
}

function compareVersions(a, b) {
  const pa = parseParts(a);
  const pb = parseParts(b);
  const n = Math.max(pa.length, pb.length);
  for (let i = 0; i < n; i++) {
    const da = pa[i] || 0;
    const db = pb[i] || 0;
    if (da !== db) return da < db ? -1 : 1;
  }
  return 0;
}

function getPublicKeyHex() {
  const pub = loadPublicKeyBytes();
  return pub ? pub.toString("hex") : "";
}

function peekManifest() {
  return cachedManifest || readManifestFile();
}

module.exports = {
  stableStringify,
  canonicalBytes,
  signPayload,
  verifyPayload,
  signBuildAttestation,
  verifyBuildAttestation,
  verifyManifest,
  getManifest,
  peekManifest,
  buildAndSignManifest,
  verifyClientHeartbeat,
  getPublicKeyHex,
  compareVersions,
  MANIFEST_FILE,
};
