#!/usr/bin/env bash
# snapshot-dist.sh — runtime-dist snapshot with git-id freshness guard
# mega-string in scripts/extension/live.sh)
#
# Purpose:
#   Snapshots dist/chrome into a per-run runtime-dist so Chrome's loaded
#   extension can't be ripped out mid-rebuild, then verifies the snapshot's
#   "from git id:" matches the source dist (catches mid-rebuild copies).
#
# Inputs (flags):
#   --dist <dir>           source dist (required, e.g. <repo>/dist/chrome)
#   --target <dir>         absolute checkout containment root
#   --runtime-root <dir>   absolute directory that owns the snapshot
#   --runtime-dist <dir>   snapshot destination (required, recreated)
#   --wait-iterations <n>  manifest wait loop length, 2s each (default 180)
#   --summary <file>       optional standard summary.json
#   --exact                preserve every source entry (release artifacts)
#
# Outputs:
#   <runtime-dist>/ snapshot (excludes _metadata); optional --summary file
#   {feature,status,inputs,outputs,generatedAt}.
#   Exit 0 — snapshot fresh; 1 — manifest never appeared, rsync failed, or
#   git-id mismatch (mid-rebuild); 2 — bad args.
#
# Never touches: the source dist (read-only); anything outside
# --runtime-dist and --summary.
set -euo pipefail

DIST=""
TARGET_ROOT=""
RUNTIME_ROOT=""
RUNTIME_DIST=""
WAIT_ITERATIONS=180
SUMMARY=""
EXACT=false
require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
while [ "$#" -gt 0 ]; do
  case "$1" in
    --dist) require_value "$@"; DIST="$2"; shift 2 ;;
    --target) require_value "$@"; TARGET_ROOT="$2"; shift 2 ;;
    --runtime-root) require_value "$@"; RUNTIME_ROOT="$2"; shift 2 ;;
    --runtime-dist) require_value "$@"; RUNTIME_DIST="$2"; shift 2 ;;
    --wait-iterations) require_value "$@"; WAIT_ITERATIONS="$2"; shift 2 ;;
    --summary) require_value "$@"; SUMMARY="$2"; shift 2 ;;
    --exact) EXACT=true; shift ;;
    -h|--help)
      echo "Usage: snapshot-dist.sh --dist <dir> --target <dir> --runtime-root <dir> --runtime-dist <dir> [--wait-iterations <n>] [--summary <file>]"
      exit 0
      ;;
    *) echo "Unknown arg: $1" >&2; exit 2 ;;
  esac
done
[ -n "$DIST" ] || { echo "Missing --dist" >&2; exit 2; }
[ -n "$TARGET_ROOT" ] || { echo "Missing --target" >&2; exit 2; }
[ -n "$RUNTIME_ROOT" ] || { echo "Missing --runtime-root" >&2; exit 2; }
[ -n "$RUNTIME_DIST" ] || { echo "Missing --runtime-dist" >&2; exit 2; }
case "$WAIT_ITERATIONS" in ''|*[!0-9]*) echo "Invalid --wait-iterations (must be numeric): $WAIT_ITERATIONS" >&2; exit 2 ;; esac

status=fail
finish() {
  if [ -n "$SUMMARY" ]; then
    mkdir -p "$(dirname "$SUMMARY")"
    STATUS_FOR_SUMMARY="$status" DIST_FOR_SUMMARY="$DIST" RUNTIME_DIST_FOR_SUMMARY="$RUNTIME_DIST" SUMMARY_PATH="$SUMMARY" node <<'NODE' || true
const fs = require('fs');
fs.writeFileSync(process.env.SUMMARY_PATH, `${JSON.stringify({
  feature: 'extension/snapshot-dist',
  status: process.env.STATUS_FOR_SUMMARY,
  inputs: { dist: process.env.DIST_FOR_SUMMARY },
  outputs: { runtimeDist: process.env.RUNTIME_DIST_FOR_SUMMARY },
  generatedAt: new Date().toISOString(),
}, null, 2)}\n`);
NODE
  fi
}
trap finish EXIT

for i in $(seq 1 "$WAIT_ITERATIONS"); do
  [ -f "$DIST/manifest.json" ] && break
  if [ $((i % 15)) -eq 0 ]; then
    echo "[recipe-harness] waiting for dist manifest (~$((i * 2))s): $DIST/manifest.json" >&2
  fi
  sleep 2
done
test -f "$DIST/manifest.json" || { echo "snapshot-dist: no manifest at $DIST/manifest.json" >&2; exit 1; }

echo "[recipe-harness] snapshotting dist -> runtime-dist: $RUNTIME_DIST" >&2
node - "$DIST" "$TARGET_ROOT" "$RUNTIME_ROOT" "$RUNTIME_DIST" <<'NODE'
const fs = require('node:fs');
const path = require('node:path');

const [sourceInput, targetInput, rootInput, destinationInput] = process.argv.slice(2);
const source = path.resolve(sourceInput);
const target = path.resolve(targetInput);
const root = path.resolve(rootInput);
const destination = path.resolve(destinationInput);

function refuse(message) {
  throw new Error(`snapshot-dist: ${message}`);
}

if (![targetInput, rootInput, destinationInput].every(path.isAbsolute)) {
  refuse('--target, --runtime-root, and --runtime-dist must be absolute paths.');
}
const targetStat = fs.lstatSync(target, { throwIfNoEntry: false });
if (!targetStat?.isDirectory() || targetStat.isSymbolicLink()) {
  refuse(`target is not a regular directory: ${target}.`);
}
const rootRelative = path.relative(target, root);
if (!rootRelative || rootRelative === '..' || rootRelative.startsWith(`..${path.sep}`) || path.isAbsolute(rootRelative)) {
  refuse(`runtime root must be a child of ${target}.`);
}
const relative = path.relative(root, destination);
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
  refuse(`runtime destination must be a child of ${root}.`);
}
if (
  source === destination ||
  source.startsWith(`${destination}${path.sep}`) ||
  destination.startsWith(`${source}${path.sep}`)
) {
  refuse('source and runtime destination must be separate directory trees.');
}

function ensureDirectoryTree(base, relativePath) {
  let current = base;
  const components = relativePath.split(path.sep).filter(Boolean);
  for (const component of components) {
    current = path.join(current, component);
    const stat = fs.lstatSync(current, { throwIfNoEntry: false });
    if (!stat) {
      fs.mkdirSync(current, { mode: 0o700 });
      continue;
    }
    if (!stat.isDirectory() || stat.isSymbolicLink()) {
      refuse(`runtime path contains an unsafe entry: ${current}.`);
    }
  }
}

ensureDirectoryTree(target, rootRelative);
ensureDirectoryTree(root, path.relative(root, path.dirname(destination)));
const realTarget = fs.realpathSync(target);
const realRoot = fs.realpathSync(root);
const realParent = fs.realpathSync(path.dirname(destination));
if (
  !realRoot.startsWith(`${realTarget}${path.sep}`) ||
  (realParent !== realRoot && !realParent.startsWith(`${realRoot}${path.sep}`))
) {
  refuse(`runtime destination escapes ${root}.`);
}
const destinationStat = fs.lstatSync(destination, { throwIfNoEntry: false });
if (destinationStat && (!destinationStat.isDirectory() || destinationStat.isSymbolicLink())) {
  refuse(`runtime destination is not a regular directory: ${destination}.`);
}
if (destinationStat) fs.rmSync(destination, { recursive: true });
fs.mkdirSync(destination, { mode: 0o700 });
NODE
if $EXACT; then
  rsync -a --delete "$DIST/" "$RUNTIME_DIST/" || exit 1
else
  rsync -a --delete --exclude _metadata "$DIST/" "$RUNTIME_DIST/" || exit 1
fi
echo "[recipe-harness] runtime-dist snapshot complete" >&2

# Freshness guard: the loaded runtime-dist must match dist/chrome's git id. A
# mismatch means the rsync caught a mid-rebuild dist; abort rather than load
# an inconsistent bundle (the "Element type is invalid: undefined" class of crash).
node -e 'const fs=require("fs");const id=p=>{try{return (JSON.parse(fs.readFileSync(p,"utf8")).description||"").match(/from git id: *([0-9a-f]+)/i)?.[1]||""}catch{return""}};const [distManifest,runtimeManifest]=process.argv.slice(-2);const d=id(distManifest),r=id(runtimeManifest);if(d&&d!==r){console.error("runtime-dist git id "+r+" != dist "+d+" (mid-rebuild?); aborting");process.exit(1)}' "$DIST/manifest.json" "$RUNTIME_DIST/manifest.json" || exit 1
status=pass
