const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip");

class ArtifactManager {
  constructor(root, options = {}) { this.root = root; this.fetch = options.fetch || global.fetch; fs.mkdirSync(root, { recursive: true }); }
  status(entry) {
    const target = path.join(this.root, entry.filename || entry.id);
    if (!fs.existsSync(target)) return { installed: false, valid: false, path: target };
    return { installed: true, valid: sha256File(target) === entry.sha256, path: target, bytes: fs.statSync(target).size };
  }
  async download(entry, options = {}) {
    validateManifestEntry(entry);
    if (options.confirmed !== true) throw new Error("Download requires explicit setup confirmation.");
    const filename = entry.filename || path.basename(new URL(entry.url).pathname);
    const target = path.join(this.root, filename);
    const partial = `${target}.${process.pid}.partial`;
    fs.rmSync(partial, { force: true });
    try {
      const response = await this.fetch(entry.url, { redirect: "follow" });
      if (!response.ok || !response.body) throw new Error(`Download failed with HTTP ${response.status}.`);
      const handle = fs.createWriteStream(partial, { flags: "wx", mode: 0o600 });
      for await (const chunk of response.body) {
        if (!handle.write(chunk)) await new Promise((resolve) => handle.once("drain", resolve));
      }
      await new Promise((resolve, reject) => handle.end((error) => error ? reject(error) : resolve()));
      if (Number.isSafeInteger(entry.bytes) && fs.statSync(partial).size !== entry.bytes) throw new Error(`Downloaded size mismatch for ${entry.id}.`);
      const actual = sha256File(partial);
      if (actual !== entry.sha256) throw new Error(`Checksum mismatch for ${entry.id}.`);
      fs.renameSync(partial, target);
      return this.status({ ...entry, filename });
    } finally { fs.rmSync(partial, { force: true }); }
  }
  installZip(entry, archivePath) {
    validateManifestEntry(entry);
    if (sha256File(archivePath) !== entry.sha256) throw new Error(`Checksum mismatch for ${entry.id}.`);
    const target = path.join(this.root, entry.id);
    const staged = `${target}.${process.pid}.staged`;
    fs.rmSync(staged, { recursive: true, force: true });
    fs.mkdirSync(staged, { recursive: true });
    const zip = new AdmZip(archivePath);
    for (const item of zip.getEntries()) {
      const portable = String(item.entryName).replace(/\\/g, "/");
      const relative = path.posix.normalize(portable).replace(/^\/+/, "");
      if (!relative || relative === ".." || relative.startsWith("../") || /^[A-Za-z]:/.test(relative)) throw new Error("Runtime archive contains an unsafe path.");
    }
    zip.extractAllTo(staged, true);
    for (const expected of entry.expected_paths || []) if (!findBasename(staged, expected)) throw new Error(`Runtime is missing ${expected}.`);
    fs.rmSync(target, { recursive: true, force: true });
    fs.renameSync(staged, target);
    return { installed: true, path: target, backend: entry.backend, version: entry.id };
  }
}

function validateManifestEntry(entry) { if (!entry?.id || !/^https:\/\//.test(entry.url || "") || !/^[a-f0-9]{64}$/.test(entry.sha256 || "")) throw new Error("Artifact manifest entry is invalid."); }
function sha256File(target) { const hash = crypto.createHash("sha256"); const fd = fs.openSync(target, "r"); const buffer = Buffer.alloc(1024 * 1024); try { let read; while ((read = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, read)); } finally { fs.closeSync(fd); } return hash.digest("hex"); }
function findBasename(root, basename) { const pending = [root]; while (pending.length) { const current = pending.pop(); for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const target = path.join(current, entry.name); if (entry.isDirectory()) pending.push(target); else if (entry.name === basename) return target; } } return null; }

module.exports = { ArtifactManager, sha256File, validateManifestEntry };
