///|
let loader_source : String =
  #|export async function instantiate(options = {}) {
  #|  const url = options.url ? new URL(options.url, import.meta.url) : new URL('./library.wasm', import.meta.url);
  #|  try {
  #|    let bytes = options.bytes;
  #|    if (!bytes) {
  #|      if (url.protocol === 'file:' && typeof process !== 'undefined' && process.versions?.node) {
  #|        bytes = await (await import('node:fs/promises')).readFile(url);
  #|      } else {
  #|        const response = await fetch(url);
  #|        if (!response.ok) throw new Error(`HTTP ${response.status}`);
  #|        bytes = await response.arrayBuffer();
  #|      }
  #|    }
  #|    const { instance } = await WebAssembly.instantiate(bytes, options.imports ?? {}, COMPILE_OPTIONS);
  #|    return instance.exports;
  #|  } catch (cause) {
  #|    throw new Error(`Cannot initialize Wasm-GC asset ${url}. Requires Wasm-GC, configured JS string builtins and host imports. Supply bytes or url to relocate the asset. ${cause.message}`, { cause });
  #|  }
  #|}

///|
async fn extract_source(host : Host, archive : String, dest : String) -> Unit {
  let listing = host.checked("unzip", ["-Z1", archive], parent(archive))
  for line in listing.split("\n") {
    if line.is_empty() {
      continue
    }
    let name = line.trim_end(chars="/").to_owned()
    safe_relative(name)
    for part in name.split("/") {
      if [
          ".git", ".npmrc", ".env", "credentials.json", "node_modules", "_build",
          ".mooncakes",
        ].contains(part.to_owned()) ||
        part.has_prefix(".env.") {
        invalid("sensitive or generated source archive input: " + name)
      }
    }
  }
  let modes = host.checked("unzip", ["-Z", "-l", archive], parent(archive))
  for line in modes.split("\n") {
    if line.has_prefix("l") {
      invalid("source archive contains a symlink")
    }
  }
  host.mkdir(dest, recursive=true, allow_exist=false)
  ignore(host.checked("unzip", ["-q", archive, "-d", dest], parent(archive)))
}

///|
async fn find_files(dir : String, suffix : String) -> Array[String] {
  let result = []
  for name in @fs.readdir(dir, include_hidden=true, sort=true) {
    let path = dir + "/" + name
    match @fs.kind(path, follow_symlink=false) {
      Directory => result.append(find_files(path, suffix))
      Regular => if name.has_suffix(suffix) { result.push(path) }
      _ => invalid("non-regular file: " + path)
    }
  }
  result
}

///|
async fn unique_file(dir : String, suffix : String) -> String {
  let found = find_files(dir, suffix)
  if found.length() != 1 {
    failed("ARTIFACT_LAYOUT", "expected one " + suffix + " under " + dir)
  }
  found[0]
}

///|
async fn tool_identities(host : Host) -> Json {
  let m : Map[String, Json] = Map([])
  for
    item in [
      ("moon", ["version", "--all"]),
      ("node", ["--version"]),
      ("npm", ["--version"]),
      ("tsc", ["--version"]),
      ("playwright", ["--version"]),
    ] {
    m[item.0] = Json::string(host.checked(item.0, item.1, "/"))
  }
  m["pubman"] = Json::string(version)
  m["executionArtifact"] = Json::string(file_digest(absolute(@env.args()[0])))
  m["verificationImplementation"] = Json::string(
    digest(@utf8.encode(loader_source + browser_harness + implementation_id)),
  )
  Json::object(m)
}

///|
async fn copy_generated(host : Host, src : String, dest : String) -> Unit {
  for path in find_files(src, "") {
    if path.has_suffix(".js") ||
      path.has_suffix(".mjs") ||
      path.has_suffix(".d.ts") {
      copy_file(host, path, dest + path[src.length():].to_owned())
    }
  }
}

///|
/// Inspect literal module references in JS/TS. TypeScript remains the final
/// authority on declaration resolution, including package dependencies.
async fn check_references(root : String, deps : Json) -> Unit {
  for file in find_files(root, "") {
    if !(file.has_suffix(".js") ||
      file.has_suffix(".mjs") ||
      file.has_suffix(".d.ts")) {
      continue
    }
    let text = @fs.read_file(file).text()
    // Tokenize quotes only. This handles import/export specifiers and reference
    // directives without treating comments or arbitrary JS as a manifest.
    for quote in ["\"", "'"] {
      let parts = text.split(quote).map(x => x.to_owned()).collect()
      for i = 1; i < parts.length(); i = i + 2 {
        let s = parts[i]
        let context = parts[i - 1].trim_end().to_owned()
        let reference = [
          "from", "import", "import(", "require(", "new URL(", "path=", "types=",
        ].any(marker => context.has_suffix(marker))
        if !reference {
          continue
        }
        if s.has_prefix("../") || s.has_prefix("./") {
          let target = future_path(parent(file) + "/" + s)
          if !inside(root, target) {
            invalid("escaping local reference: " + s)
          }
          let resolved = if @fs.exists(target) {
            true
          } else if s.has_suffix(".js") {
            @fs.exists(target[:target.length() - 3].to_owned() + ".d.ts")
          } else {
            false
          }
          if !resolved {
            invalid("missing local reference " + s + " in " + file)
          }
        } else if file.has_suffix(".d.ts") && !s.contains(" ") && !s.is_empty() {
          // Only strings in import/from/require and triple-slash references are dependencies.
          let context = parts[i - 1].trim_end().to_owned()
          if context.has_suffix("from") ||
            context.has_suffix("import") ||
            context.has_suffix("require(") {
            let parts = s.split("/").map(x => x.to_owned()).collect()
            let name = if s.has_prefix("@") && parts.length() >= 2 {
              parts[0] + "/" + parts[1]
            } else {
              parts[0]
            }
            if field(deps, name) == Json::null() {
              invalid("undeclared declaration dependency: " + name)
            }
          }
        }
      }
    }
  }
}

///|
async fn capture_inputs(host : Host, c : Config, dest : String) -> Unit {
  let paths = []
  for e in c.entries {
    paths.push(e.wrapper)
    if e.declaration is String(s) {
      paths.push(s)
    }
  }
  for a in c.assets {
    paths.push(string_field(a, "source"))
  }
  for k in ["node", "browser", "types"] {
    paths.append(strings(field(c.checks, k)))
  }
  for p in paths {
    if !@fs.exists(dest + "/" + p) {
      copy_file(host, contained(c.root, p), dest + "/" + p)
    }
  }
}

///|
async fn pack(c : Config, host : Host) -> Json {
  // Build outside every workspace ancestor. Staging output is on the same
  // filesystem as the destination to permit a final rename.
  host.mkdir(parent(c.output), recursive=true, allow_exist=true)
  let lock = host.create(c.output + ".pack-lock", allow_existing=false)
  defer lock.close()
  defer host.remove(c.output + ".pack-lock")
  let prior_guard = if @fs.exists(c.output) {
    Some(host.create(c.output + "/.operation-lock", allow_existing=false))
  } else {
    None
  }
  let mut prior_lock_path = c.output + "/.operation-lock"
  defer (if prior_guard is Some(file) {
    file.close()
    if @fs.exists(prior_lock_path) {
      host.remove(prior_lock_path)
    }
  })
  let work = host.tmpdir(prefix="pubman-build-")
  defer host.rmdir(work, recursive=true)
  let stage = c.output + ".staging-" + basename(work)
  host.mkdir(stage, allow_exist=false)
  let mut committed = false
  defer (if !committed { host.rmdir(stage, recursive=true) })
  let payload = stage + "/payload"
  host.mkdir(payload, recursive=true)
  let tools = tool_identities(host)
  ignore(
    host.checked("moon", ["package", "--target-dir", work + "/capture"], c.root),
  )
  let archive = unique_file(work + "/capture/publish", ".zip")
  copy_file(host, archive, payload + "/source.zip")
  let source = work + "/source"
  extract_source(host, payload + "/source.zip", source)
  let source_inventory = inventory(source)
  // Repack the extracted snapshot; the inventory must be exactly reusable.
  ignore(
    host.checked("moon", ["package", "--target-dir", work + "/repack"], source),
  )
  let repack = unique_file(work + "/repack/publish", ".zip")
  extract_source(host, repack, work + "/repacked")
  if inventory(work + "/repacked") != source_inventory {
    failed("SOURCE_INVENTORY", "source snapshot does not survive repackaging")
  }
  capture_inputs(host, c, payload + "/inputs")
  copy_file(host, c.config_path, payload + "/config.json")
  let pkg = work + "/npm"
  host.mkdir(pkg, recursive=true)
  let dependencies : Map[String, Json] = Map([])
  for e in c.entries {
    let snapshot = work + "/snapshot-" + e.target
    extract_source(host, payload + "/source.zip", snapshot)
    if @fs.exists(snapshot + "/moon.work") {
      invalid("source archive must not contain moon.work")
    }
    ignore(metadata(@fs.read_file(snapshot + "/moon.mod").text()))
    // Dependencies resolve in the standalone snapshot, never the original workspace.
    ignore(host.checked("moon", ["check"], snapshot))
    dependencies[e.target] = Json::string(
      host.checked("moon", ["tree"], snapshot),
    )
    let target = work + "/target-" + e.target
    ignore(
      host.checked(
        "moon",
        [
          "build",
          e.pkg,
          "--target",
          e.target,
          "--release",
          "--frozen",
          "--target-dir",
          target,
        ],
        snapshot,
      ),
    )
    let output = target + "/" + e.target + "/release/build/" + e.pkg
    let public_entry = if e.target == "js" { "js" } else { "wasm" }
    if e.target == "js" {
      let js = unique_file(output, ".js")
      copy_generated(host, output, pkg + "/internal/js")
      let copied = pkg + "/internal/js/" + basename(js)
      if basename(js) != "library.js" {
        host.rename(copied, pkg + "/internal/js/library.js")
      }
    } else {
      copy_file(
        host,
        unique_file(output, ".wasm"),
        pkg + "/internal/wasm/library.wasm",
      )
      host.write_file(
        pkg + "/internal/wasm/loader.mjs",
        loader_source.replace(old="COMPILE_OPTIONS", new=e.loader.stringify()),
      )
    }
    copy_file(
      host,
      payload + "/inputs/" + e.wrapper,
      pkg + "/" + public_entry + "/index.mjs",
    )
    match e.declaration {
      String(p) =>
        copy_file(
          host,
          payload + "/inputs/" + p,
          pkg + "/" + public_entry + "/index.d.ts",
        )
      Object(_) => {
        let declared = target +
          "/" +
          e.target +
          "/release/build/" +
          string_field(e.declaration, "path")
        if !@fs.exists(declared) {
          invalid("missing compiler declaration: " + declared)
        }
        copy_generated(host, parent(declared), pkg + "/" + public_entry)
        let copied = pkg + "/" + public_entry + "/" + basename(declared)
        if basename(declared) != "index.d.ts" {
          host.rename(copied, pkg + "/" + public_entry + "/index.d.ts")
        }
      }
      _ => invalid("missing declaration")
    }
  }
  for a in c.assets {
    copy_file(
      host,
      payload + "/inputs/" + string_field(a, "source"),
      pkg + "/" + string_field(a, "destination"),
    )
  }
  let deps = if field(c.npm, "dependencies") == Json::null() {
    Json::empty_object()
  } else {
    field(c.npm, "dependencies")
  }
  check_references(pkg, deps)
  let manifest : Json = {
    "name": field(c.npm, "name"),
    "version": field(c.meta, "version"),
    "type": "module",
    "description": opt_string(
      c.npm,
      "description",
      opt_string(c.meta, "description", ""),
    ),
    "license": field(c.meta, "license"),
    "repository": field(c.meta, "repository"),
    "types": "./js/index.d.ts",
    "exports": {
      ".": { "types": "./js/index.d.ts", "import": "./js/index.mjs" },
      "./wasm": { "types": "./wasm/index.d.ts", "import": "./wasm/index.mjs" },
    },
    "dependencies": deps,
  }
  write_json(host, pkg + "/package.json", manifest)
  let npm_inventory = inventory(pkg)
  let packed = @json.parse(
    host.checked(
      "npm",
      ["pack", "--json", "--ignore-scripts", "--pack-destination", payload],
      pkg,
    ),
  )
  let info = array(packed)[0]
  host.rename(
    payload + "/" + string_field(info, "filename"),
    payload + "/package.tgz",
  )
  let expected = match npm_inventory {
    Object(m) => m.keys().to_array()
    _ => []
  }
  let actual = array(field(info, "files")).map(f => string_field(f, "path"))
  if expected.length() != actual.length() ||
    expected.any(p => !actual.contains(p)) {
    failed("NPM_INVENTORY", "npm pack changed the declared file inventory")
  }
  write_json(host, payload + "/source-inventory.json", source_inventory)
  write_json(host, payload + "/npm-inventory.json", npm_inventory)
  write_json(host, payload + "/tools.json", tools)
  write_json(host, payload + "/dependencies.json", Json::object(dependencies))
  let integrity = "sha512-" +
    @base64.encode(
      Bytes::from_array(
        @crypto.sha512(@fs.read_file(payload + "/package.tgz").binary())[:],
      )[:],
    )
  let bundle : Json = {
    "schemaVersion": 1,
    "module": field(c.meta, "name"),
    "npm": field(c.npm, "name"),
    "version": field(c.meta, "version"),
    "integrity": integrity,
    "tag": field(c.npm, "tag"),
    "files": inventory(payload),
  }
  write_json(host, stage + "/bundle.json", bundle)
  host.write_file(stage + "/.pubman-owned", "pubman bundle v1\n")
  let backup = stage + ".previous"
  let prior = prior_guard is Some(_)
  if @fs.exists(c.output) != prior {
    failed(
      "OUTPUT_CHANGED", "output changed while packaging; refusing replacement",
    )
  }
  if prior &&
    @fs.read_file(c.output + "/.pubman-owned").text() != "pubman bundle v1\n" {
    failed("OUTPUT_CHANGED", "output ownership changed while packaging")
  }
  if prior {
    host.rename(c.output, backup, replace=false)
    prior_lock_path = backup + "/.operation-lock"
  }
  errdefer (if prior && !committed {
    host.rename(backup, c.output, replace=false)
    prior_lock_path = c.output + "/.operation-lock"
  })
  host.rename(stage, c.output, replace=false)
  committed = true
  if prior {
    host.rmdir(backup, recursive=true)
  }
  { "dryRun": false, "bundle": c.output, "identity": bundle }
}