import {
  createVersion
} from "./chunk-FAAFWE4G.js";
import {
  assertIsDirectoryNotSymlink,
  assertRealpathInsideRoot,
  fromPosixRelPath,
  isPathInsideRoot,
  sha256String
} from "./chunk-457A4P3L.js";
import {
  parseExportBundle
} from "./chunk-OA3L7BFR.js";
import "./chunk-PZ5AY32C.js";

// src/transfer/capsule-merge.ts
import { lstat, mkdir, readFile, realpath, stat, writeFile } from "fs/promises";
import path from "path";
import { gunzipSync } from "zlib";
async function mergeCapsule(opts) {
  const archiveAbs = path.resolve(opts.sourceArchive);
  const rootAbs = path.resolve(opts.targetRoot);
  await assertIsDirectoryNotSymlink(rootAbs, "mergeCapsule", "targetRoot");
  const conflictMode = opts.conflictMode ?? "skip-conflicts";
  if (conflictMode !== "skip-conflicts" && conflictMode !== "prefer-source" && conflictMode !== "prefer-local") {
    throw new Error(
      `mergeCapsule: unknown conflictMode ${JSON.stringify(conflictMode)}; expected "skip-conflicts", "prefer-source", or "prefer-local"`
    );
  }
  const raw = await readFile(archiveAbs);
  let json;
  try {
    json = gunzipSync(raw).toString("utf-8");
  } catch (cause) {
    throw new Error(
      `mergeCapsule: archive is not a valid gzip file: ${archiveAbs}`,
      { cause }
    );
  }
  let parsedJson;
  try {
    parsedJson = JSON.parse(json);
  } catch (cause) {
    throw new Error(
      `mergeCapsule: archive is not valid JSON after gunzip: ${archiveAbs}`,
      { cause }
    );
  }
  const parsed = parseExportBundle(parsedJson);
  if (parsed.capsuleVersion !== 2) {
    throw new Error(
      "mergeCapsule: archive is V1; only V2 capsule archives are supported"
    );
  }
  const bundle = parsed.bundle;
  const manifest = bundle.manifest;
  const capsule = manifest.capsule;
  const manifestIndex = /* @__PURE__ */ new Map();
  for (const f of manifest.files) {
    manifestIndex.set(f.path, f);
  }
  if (manifestIndex.size !== manifest.files.length) {
    throw new Error("mergeCapsule: manifest contains duplicate file paths");
  }
  const recordPaths = /* @__PURE__ */ new Set();
  for (const rec of bundle.records) {
    if (recordPaths.has(rec.path)) {
      throw new Error(
        `mergeCapsule: bundle contains duplicate record path: ${rec.path}`
      );
    }
    recordPaths.add(rec.path);
  }
  const rootReal = await realpath(rootAbs).catch(() => rootAbs);
  const seenTargetPaths = /* @__PURE__ */ new Map();
  for (const rec of bundle.records) {
    const entry = manifestIndex.get(rec.path);
    if (!entry) {
      throw new Error(
        `mergeCapsule: archive checksum mismatch (record without manifest entry: ${rec.path})`
      );
    }
    const { sha256, bytes } = sha256String(rec.content);
    if (sha256 !== entry.sha256 || bytes !== entry.bytes) {
      throw new Error(
        `mergeCapsule: archive checksum mismatch for ${rec.path}: expected sha256=${entry.sha256} bytes=${entry.bytes}, got sha256=${sha256} bytes=${bytes}`
      );
    }
    if (rec.path.includes("\\")) {
      throw new Error(
        `mergeCapsule: record path contains backslash separators (Windows-style paths are not allowed): ${rec.path}`
      );
    }
    const posixNormalized = path.posix.normalize(rec.path);
    if (rec.path.startsWith("/") || rec.path.split("/").some((seg) => seg === "..") || posixNormalized.startsWith("..") || posixNormalized.startsWith("/")) {
      throw new Error(
        `mergeCapsule: record path escapes target root: ${rec.path}`
      );
    }
    const targetAbs = path.join(rootReal, fromPosixRelPath(rec.path));
    if (!isPathInsideRoot(rootReal, targetAbs)) {
      throw new Error(
        `mergeCapsule: record path escapes target root: ${rec.path}`
      );
    }
    await assertRealpathInsideRoot(rootReal, targetAbs, rec.path, "mergeCapsule");
    const targetLstat = await lstat(targetAbs).catch(() => null);
    if (targetLstat !== null && targetLstat.isSymbolicLink()) {
      throw new Error(
        `mergeCapsule: record target is a symlink and cannot be written to safely: ${rec.path}`
      );
    }
    const dedupKey = targetAbs.toLowerCase();
    const firstSourcePath = seenTargetPaths.get(dedupKey);
    if (firstSourcePath !== void 0) {
      throw new Error(
        `mergeCapsule: manifest contains two entries that resolve to the same target path: "${firstSourcePath}" and "${rec.path}" both map to "${rec.path}"`
      );
    }
    seenTargetPaths.set(dedupKey, rec.path);
  }
  for (const f of manifest.files) {
    if (!recordPaths.has(f.path)) {
      throw new Error(
        `mergeCapsule: archive checksum mismatch (manifest entry without record: ${f.path})`
      );
    }
  }
  const merged = [];
  const skipped = [];
  const conflicts = [];
  const sortedRecords = [...bundle.records].sort(
    (a, b) => a.path.localeCompare(b.path)
  );
  for (const rec of sortedRecords) {
    const targetAbs = path.join(rootReal, fromPosixRelPath(rec.path));
    const entry = manifestIndex.get(rec.path);
    const localContent = await readLocalFile(targetAbs);
    if (localContent === null) {
      await mkdir(path.dirname(targetAbs), { recursive: true });
      await writeFile(targetAbs, rec.content, "utf-8");
      merged.push({ sourcePath: rec.path, targetPath: rec.path, snapshotted: false });
      continue;
    }
    const { sha256: localSha256 } = sha256String(localContent);
    if (localSha256 === entry.sha256) {
      skipped.push({ path: rec.path, reason: "identical" });
      continue;
    }
    const { sha256: archiveSha256 } = sha256String(rec.content);
    conflicts.push({
      path: rec.path,
      archiveSha256,
      localSha256
    });
    if (conflictMode === "skip-conflicts" || conflictMode === "prefer-local") {
      skipped.push({ path: rec.path, reason: "conflict" });
      continue;
    }
    let snapshotted = false;
    if (opts.versioning && opts.versioning.enabled) {
      await createVersion(
        targetAbs,
        localContent,
        "manual",
        opts.versioning,
        opts.log,
        `capsule-merge: ${capsule.id}`,
        rootReal
      );
      snapshotted = true;
    }
    await writeFile(targetAbs, rec.content, "utf-8");
    merged.push({ sourcePath: rec.path, targetPath: rec.path, snapshotted });
  }
  merged.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
  skipped.sort((a, b) => a.path.localeCompare(b.path));
  conflicts.sort((a, b) => a.path.localeCompare(b.path));
  return { merged, skipped, conflicts, manifest };
}
async function readLocalFile(absPath) {
  const st = await stat(absPath).catch(() => null);
  if (!st || !st.isFile()) return null;
  return readFile(absPath, "utf-8");
}
export {
  mergeCapsule
};
//# sourceMappingURL=capsule-merge-DI7PNQ2H.js.map