#!/usr/bin/env nu
# install/assemble-bundle.nu — assemble a self-contained ontoref distribution
# bundle (ADR-038) from an ALREADY cross-compiled binary plus the data layer.
#
# This NEVER recompiles. It consumes target/<triple>/release/ontoref-daemon and
# packages it with the bootstrapper, the CLI wrapper, a target-arch nickel, the
# data layer (reflection/ ontology/ domains/ templates/ public/ install/) and a
# config skeleton into dist/ontoref-<os>-<arch>.tar.gz (+ .sha256). The curl|sh
# installer (install/install.sh) extracts this and mirrors install.nu's layout.
#
#   nu install/assemble-bundle.nu --target x86_64-unknown-linux-musl
#   nu install/assemble-bundle.nu smoke --target x86_64-unknown-linux-musl
#
# Config is taken ONLY from install/resources/ — never from the (possibly
# corrupt) per-project .ontoref/config.ncl.

# Map a cargo target triple to the bundle's os/arch suffix.
def target-to-osarch [target: string]: nothing -> record {
  match $target {
    "x86_64-unknown-linux-musl"  => { os: "linux", arch: "amd64" }
    "aarch64-unknown-linux-musl" => { os: "linux", arch: "arm64" }
    "x86_64-unknown-linux-gnu"   => { os: "linux", arch: "amd64" }
    "aarch64-unknown-linux-gnu"  => { os: "linux", arch: "arm64" }
    _ => { error make { msg: $"unsupported target triple: ($target)" } }
  }
}

# Resolve the cargo target directory (honours .cargo/config.toml target-dir).
def cargo-target-dir [root: string]: nothing -> string {
  let meta = (do { cd $root; ^cargo metadata --format-version 1 --no-deps } | complete)
  if $meta.exit_code == 0 {
    $meta.stdout | from json | get target_directory
  } else {
    $"($root)/target"
  }
}

# Recursively copy every file under src into dest, preserving the relative tree.
def copy-tree [src: string, dest: string]: nothing -> int {
  if not ($src | path exists) { return 0 }
  mkdir $dest
  let files = (glob $"($src)/**/*" | where { |f| ($f | path type) == "file" })
  for src_file in $files {
    let rel         = ($src_file | str replace $"($src)/" "")
    let dest_file   = $"($dest)/($rel)"
    let dest_parent = ($dest_file | path dirname)
    mkdir $dest_parent
    cp $src_file $dest_file
  }
  $files | length
}

# Repo-side reflection source: .ontoref/reflection (ADR-032) or legacy reflection/.
def reflection-src [root: string]: nothing -> string {
  if ($"($root)/.ontoref/reflection" | path exists) {
    $"($root)/.ontoref/reflection"
  } else {
    $"($root)/reflection"
  }
}

# Locate a target-arch nickel binary. Explicit --nickel wins; otherwise look for
# a pre-fetched per-arch binary under dist/nickel/<target>/nickel; otherwise fall
# back to the host nickel with a loud warning (arch may not match the target).
def resolve-nickel [root: string, target: string, explicit: string]: nothing -> string {
  if ($explicit | is-not-empty) {
    if not ($explicit | path exists) {
      error make { msg: $"--nickel path does not exist: ($explicit)" }
    }
    return $explicit
  }
  let prefetched = $"($root)/dist/nickel/($target)/nickel"
  if ($prefetched | path exists) { return $prefetched }
  let host = (which nickel)
  if ($host | is-empty) {
    error make { msg: "nickel not found — pass --nickel <target-arch-binary> or place it at dist/nickel/<target>/nickel" }
  }
  let host_path = ($host | first | get path)
  print $"  (ansi yellow)warn(ansi reset) bundling host nickel ($host_path) — arch may differ from target ($target); pass --nickel for cross bundles"
  $host_path
}

# Assemble the bundle. Returns the tarball path.
def assemble [target: string, root: string, bin_dir: string, version: string, nickel: string, out_dir: string]: nothing -> string {
  let osarch = (target-to-osarch $target)
  let resolved_bin_dir = if ($bin_dir | is-not-empty) {
    $bin_dir
  } else {
    $"((cargo-target-dir $root))/($target)/release"
  }

  let daemon_src = $"($resolved_bin_dir)/ontoref-daemon"
  if not ($daemon_src | path exists) {
    error make { msg: $"binary not found: ($daemon_src)\n  run: cross build --target ($target) --release" }
  }
  let tier_src = $"($resolved_bin_dir)/ontoref-tier"

  let nickel_bin = (resolve-nickel $root $target $nickel)

  let stage = $"($out_dir)/.stage-($osarch.os)-($osarch.arch)"
  if ($stage | path exists) { rm -rf $stage }
  mkdir $"($stage)/bin"
  mkdir $"($stage)/data"
  mkdir $"($stage)/config"

  # ── bin/ ───────────────────────────────────────────────────────────────────
  cp $daemon_src $"($stage)/bin/ontoref-daemon.bin"
  if ($tier_src | path exists) { cp $tier_src $"($stage)/bin/ontoref-tier" }
  cp $"($root)/install/ontoref-daemon-boot" $"($stage)/bin/ontoref-daemon-boot"
  cp $"($root)/install/ontoref-global"      $"($stage)/bin/ontoref-global"
  cp $nickel_bin $"($stage)/bin/nickel"
  for f in (glob $"($stage)/bin/*" | where { |p| ($p | path type) == "file" }) { chmod +x $f }

  # ── data/ — reflection, protocol ontology, domains, templates, UI, install ──
  let refl_n = (copy-tree (reflection-src $root) $"($stage)/data/reflection")
  let ont_n  = (copy-tree $"($root)/ontology"    $"($stage)/data/ontology")
  let dom_n  = (copy-tree $"($root)/domains"     $"($stage)/data/domains")
  let tpl_n  = (copy-tree $"($root)/templates"   $"($stage)/data/templates")
  let ui_n   = (copy-tree $"($root)/crates/ontoref-daemon/templates" $"($stage)/data/templates")
  let pub_n  = (copy-tree $"($root)/crates/ontoref-daemon/public"    $"($stage)/data/public")
  mkdir $"($stage)/data/install"
  for f in (glob $"($root)/install/*.nu") {
    cp $f $"($stage)/data/install/(($f | path basename))"
  }

  # ── config/ — skeleton ONLY from install/resources (never .ontoref/config.ncl) ──
  let res = $"($root)/install/resources"
  for name in ["config.ncl", "projects.ncl", "remote-projects.ncl", "streams.json"] {
    let src = $"($res)/($name)"
    if ($src | path exists) { cp $src $"($stage)/config/($name)" }
  }
  copy-tree $"($res)/schemas" $"($stage)/config/schemas" | ignore

  # ── BUNDLE_INFO.json — version/target provenance for the installer ──────────
  let info = {
    schema: "ontoref-bundle/1",
    version: $version,
    target: $target,
    os: $osarch.os,
    arch: $osarch.arch,
    components: { reflection: $refl_n, ontology: $ont_n, domains: $dom_n, templates: ($tpl_n + $ui_n), public: $pub_n },
  }
  $info | to json | save --force $"($stage)/BUNDLE_INFO.json"

  # ── tar.gz + sha256 ──────────────────────────────────────────────────────────
  mkdir $out_dir
  let tarball = $"($out_dir)/ontoref-($osarch.os)-($osarch.arch).tar.gz"
  let tar_res = (do { ^tar -czf $tarball -C $stage "." } | complete)
  if $tar_res.exit_code != 0 {
    error make { msg: $"tar failed: ($tar_res.stderr)" }
  }
  let digest = (open --raw $tarball | hash sha256)
  $"($digest)  (($tarball | path basename))\n" | save --force $"($tarball).sha256"
  rm -rf $stage

  print $"✓ bundle  ($tarball)"
  print $"  sha256  ($digest)"
  print $"  layout  bin/{ontoref-daemon.bin,ontoref-daemon-boot,ontoref-global,nickel} data/ config/"
  $tarball
}

# Assemble a bundle for one target triple.
def main [
  --target: string = "x86_64-unknown-linux-musl"  # cargo target triple
  --bin-dir: string = ""                            # override binary dir (default: <target_dir>/<triple>/release)
  --root: string = ""                               # repo root (default: PWD)
  --version: string = ""                            # version stamp (default: workspace.package.version)
  --nickel: string = ""                             # target-arch nickel binary to bundle
  --out-dir: string = ""                            # output dir (default: <root>/dist)
]: nothing -> nothing {
  let root = if ($root | is-empty) { $env.PWD } else { $root }
  let out  = if ($out_dir | is-empty) { $"($root)/dist" } else { $out_dir }
  let ver  = if ($version | is-not-empty) {
    $version
  } else {
    let cargo = (open $"($root)/Cargo.toml")
    $cargo.workspace.package.version
  }
  assemble $target $root $bin_dir $ver $nickel $out | ignore
}

# Smoke gate (ADR-038): assemble the native bundle, extract it, run the daemon
# binary, and confirm the expected layout. Fails the release pipeline BEFORE any
# distribution step. Native-arch only — an arm64 binary cannot run on amd64.
def "main smoke" [
  --target: string = "x86_64-unknown-linux-musl"
  --root: string = ""
]: nothing -> nothing {
  let root = if ($root | is-empty) { $env.PWD } else { $root }
  let osarch = (target-to-osarch $target)
  let out = $"($root)/dist"
  let cargo = (open $"($root)/Cargo.toml")
  let ver = $cargo.workspace.package.version

  let tarball = (assemble $target $root "" $ver "" $out)

  let probe = $"($out)/.smoke-($osarch.os)-($osarch.arch)"
  if ($probe | path exists) { rm -rf $probe }
  mkdir $probe
  let untar = (do { ^tar -xzf $tarball -C $probe } | complete)
  if $untar.exit_code != 0 {
    error make { msg: $"smoke: extraction failed: ($untar.stderr)" }
  }

  for entry in ["bin/ontoref-daemon.bin", "bin/ontoref-daemon-boot", "bin/ontoref-global", "bin/nickel", "BUNDLE_INFO.json"] {
    if not ($"($probe)/($entry)" | path exists) {
      error make { msg: $"smoke: bundle missing ($entry)" }
    }
  }

  let ver_res = (do { ^$"($probe)/bin/ontoref-daemon.bin" --version } | complete)
  if $ver_res.exit_code != 0 {
    error make { msg: $"smoke: daemon binary failed to run --version:\n($ver_res.stderr)" }
  }
  print $"✓ smoke   daemon runs: (($ver_res.stdout | str trim))"
  rm -rf $probe
}
