#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
# Copyright (C) Jarkko Sakkinen 2026
#
# Multi-platform release packaging.
#   linux-*/win32-x64 — cross-rs (Docker)
#   darwin-arm64 — native cargo on Apple Silicon; cross + local osxcross image on Linux
#
# Platforms:
#   linux-x64, linux-arm64, win32-x64 — official cross images
#   darwin-arm64 — host cargo when packaging on darwin-arm64; else local
#   cross-toolchains image (Apple SDK; required when packaging from Linux)
#
# Exit 0 if every requested platform was packaged.
# Exit 2 if a requested build fails while another succeeds.
# Exit 1 if nothing was packaged or a hard prerequisite is missing.
# PACKAGE_STRICT=1 fails when any requested platform is skipped or failed.

set -euo pipefail

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=package-target.sh
source "$script_dir/package-target.sh"

CARGO="${CARGO:-cargo}"
RUSTC="${RUSTC:-rustc}"
CROSS="${CROSS:-cross}"
NODE="${NODE:-node}"
FILE="${FILE:-file}"
READELF="${READELF:-readelf}"

export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--no-deprecation"
export PATH="${HOME}/.cargo/bin:${PATH}"

die() {
  printf '%s\n' "$1" >&2
  exit 1
}

require_command() {
  command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

for command in "$CARGO" "$RUSTC" "$CROSS" "$NODE" git tar "$FILE" docker; do
  require_command "$command"
done

if ! docker info >/dev/null 2>&1; then
  die "docker is not usable (is the daemon running?)"
fi

repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" \
  || die "not inside a Git repository"
cd "$repo_root"


# Package the highest semver tag reachable from HEAD on this branch (not tip).
# Source is built from a detached worktree at that tag; this script (and the
# npm/artifacts output dirs) stay on the caller's tree so packaging fixes on
# tip apply without tagging them first.
#
# Worktree path must be Docker-visible: Dory/macOS leaves /var/folders (TMPDIR)
# bind mounts empty, so never put the worktree under TMPDIR.
build_root="$repo_root"
package_worktree=

cleanup_package() {
  local status=$?
  if [[ -n "${package_worktree:-}" ]]; then
    git -C "$repo_root" worktree remove --force "$package_worktree" 2>/dev/null \
      || rm -rf "$package_worktree"
  fi
  return "$status"
}
trap cleanup_package EXIT

latest_tag="$(git tag --merged HEAD --list '[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1)"
[[ -n "$latest_tag" ]] || die "no semver tag reachable from HEAD"
[[ "$latest_tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
  || die "latest reachable tag is not semver X.Y.Z: $latest_tag"

tag_commit="$(git rev-parse "$latest_tag^{commit}" 2>/dev/null)" \
  || die "cannot resolve tag $latest_tag"
head_commit="$(git rev-parse HEAD)"

if [[ "$tag_commit" != "$head_commit" ]]; then
  printf 'packaging tag %s (%s); HEAD is %s — building tagged tree in a worktree\n' \
    "$latest_tag" "${tag_commit:0:12}" "${head_commit:0:12}"
  package_worktree="${XDG_CACHE_HOME:-$HOME/.cache}/goosedump/package-src/$latest_tag"
  mkdir -p "$(dirname "$package_worktree")"
  # Drop a leftover worktree from a previous interrupted run.
  if [[ -e "$package_worktree" ]]; then
    git -C "$repo_root" worktree remove --force "$package_worktree" 2>/dev/null \
      || rm -rf "$package_worktree"
  fi
  git worktree add --detach "$package_worktree" "$latest_tag" \
    || die "cannot create worktree for tag $latest_tag"
  [[ -f "$package_worktree/Cargo.toml" ]] \
    || die "worktree missing Cargo.toml: $package_worktree"
  build_root="$package_worktree"
else
  printf 'packaging tag %s (matches HEAD)\n' "$latest_tag"
fi

[[ -f "$build_root/Cross.toml" ]] \
  || die "missing Cross.toml in tagged tree $latest_tag (required for cross packaging)"

version="$("$NODE" -p "require('$build_root/package.json').version")"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "invalid package.json version: $version"
[[ "$version" == "$latest_tag" ]] \
  || die "package.json version $version does not match tag $latest_tag"

cargo_version="$(
  sed -n 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' \
    "$build_root/Cargo.toml" | head -1
)"
[[ "$version" == "$cargo_version" ]] \
  || die "package.json version $version does not match Cargo.toml $cargo_version"
rust_host_triple="$("$RUSTC" --print host-tuple)"
[[ -n "$rust_host_triple" ]] || die "cannot determine rustc host triple"

# Share the caller's target dir so incremental cross/cargo caches still hit.
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$repo_root/target}"

# platform|rust-triple|binary-name|required-image
platforms=(
  'linux-x64|x86_64-unknown-linux-musl|goosedump|'
  'linux-arm64|aarch64-unknown-linux-musl|goosedump|'
  'darwin-arm64|aarch64-apple-darwin|goosedump|ghcr.io/cross-rs/aarch64-apple-darwin-cross:local'
  'win32-x64|x86_64-pc-windows-gnu|goosedump.exe|'
)

requested=()
if (($# > 0)); then
  requested=("$@")
else
  for entry in "${platforms[@]}"; do
    IFS='|' read -r platform _ _ _ <<<"$entry"
    requested+=("$platform")
  done
fi

platform_entry() {
  local want="$1" entry platform
  for entry in "${platforms[@]}"; do
    IFS='|' read -r platform _ _ _ <<<"$entry"
    if [[ "$platform" == "$want" ]]; then
      printf '%s\n' "$entry"
      return 0
    fi
  done
  return 1
}

host_platform() {
  "$NODE" -e 'process.stdout.write(process.platform + "-" + process.arch)'
}

verify_linux_static() {
  local bin="$1"
  local description

  description="$($FILE -b "$bin")"
  printf '%s\n' "$description"
  if [[ "$description" != *"static"* ]]; then
    printf 'Linux musl release is not statically linked: %s\n' "$bin" >&2
    return 1
  fi
  if command -v "$READELF" >/dev/null 2>&1; then
    if "$READELF" -l "$bin" 2>/dev/null | grep -q 'INTERP'; then
      printf 'Linux musl release declares a dynamic ELF interpreter: %s\n' "$bin" >&2
      return 1
    fi
    if "$READELF" -d "$bin" 2>/dev/null | grep -q '(NEEDED)'; then
      printf 'Linux musl release has dynamic dependencies: %s\n' "$bin" >&2
      return 1
    fi
  fi
  return 0
}

smoke_host_binary() {
  local platform="$1"
  local bin="$2"

  if [[ "$platform" == "$(host_platform)" ]]; then
    # Fresh cp of an adhoc-signed Mach-O can fail Gatekeeper (SIGKILL) until re-signed.
    if [[ "$platform" == darwin-* ]] && command -v codesign >/dev/null 2>&1; then
      codesign --force -s - "$bin" >/dev/null 2>&1 || true
    fi
    "$bin" --version >/dev/null
  fi
}

# Host ~/.cargo/config.toml (mold/clang for all Linux) breaks cross image
# linkers. Use a dedicated CARGO_HOME under the user cache with no config.
#
# Dory (and similar FEX-backed Docker engines) leave the cross container
# running after cargo exits: only FEXServer --wait_pipe remains, so `cross`
# blocks forever after "Finished `release` profile". Reap that container;
# the binary is already on the host volume.
cross_container_ids() {
  docker ps -q --filter "status=running" --filter "name=${1}" 2>/dev/null || true
}

reap_stuck_cross_containers() {
  local ids

  ids="$(cross_container_ids "$1")"
  if [[ -n "$ids" ]]; then
    # shellcheck disable=SC2086
    docker kill $ids >/dev/null 2>&1 || true
    # shellcheck disable=SC2086
    docker rm -f $ids >/dev/null 2>&1 || true
  fi
}

# True when cargo has finished inside a still-running FEX cross container.
# Dory keeps FEXServer --wait_pipe alive after cargo exits; docker top still
# shows the original `sh -c '... cargo build ...'` argv under FEX, so do not
# treat the word "cargo" in that line as an active build.
fex_zombie_cross_container() {
  local triple="$1" id top logs
  local ids

  ids="$(cross_container_ids "$triple")"
  [[ -n "$ids" ]] || return 1
  for id in $ids; do
    top="$(docker top "$id" 2>/dev/null || true)"
    [[ -n "$top" ]] || continue
    printf '%s\n' "$top" | grep -Eq 'FEXServer' || continue
    # Active compile/link still running.
    if printf '%s\n' "$top" | grep -Eq 'rustc|cc1|clang|collect2'; then
      continue
    fi
    logs="$(docker logs "$id" 2>&1 || true)"
    if printf '%s\n' "$logs" | grep -Fq 'Finished `release` profile'; then
      return 0
    fi
  done
  return 1
}

build_with_cross() {
  local triple="$1"
  local cargo_home cross_pid status=0 binary source_bin
  local grace="${CROSS_FEX_GRACE_SECS:-5}"
  local deadline=$((SECONDS + ${CROSS_BUILD_TIMEOUT_SECS:-1800}))

  cargo_home="${XDG_CACHE_HOME:-$HOME/.cache}/goosedump/cross-cargo"
  mkdir -p "$cargo_home"
  rm -f "$cargo_home/config.toml" "$cargo_home/config"

  case "$triple" in
    *windows*) binary=goosedump.exe ;;
    *) binary=goosedump ;;
  esac
  source_bin="$CARGO_TARGET_DIR/$triple/release/$binary"

  # Build from tagged tree (worktree or repo); Cross.toml is read from CWD.
  # Inherit stdout/stderr so cargo progress still streams to the terminal.
  set +e
  (
    cd "$build_root" || exit 1
    CARGO_HOME="$cargo_home" "$CROSS" build --release --target "$triple"
  ) &
  cross_pid=$!

  while kill -0 "$cross_pid" 2>/dev/null; do
    if (( SECONDS >= deadline )); then
      printf 'cross build timed out for %s after %ss\n' \
        "$triple" "${CROSS_BUILD_TIMEOUT_SECS:-1800}" >&2
      reap_stuck_cross_containers "$triple"
      kill "$cross_pid" 2>/dev/null || true
      wait "$cross_pid" 2>/dev/null
      set -e
      return 1
    fi

    if fex_zombie_cross_container "$triple" && [[ -f "$source_bin" ]]; then
      # Confirm it stays finished (not a brief gap between compile steps).
      sleep "$grace"
      if kill -0 "$cross_pid" 2>/dev/null \
        && fex_zombie_cross_container "$triple" \
        && [[ -f "$source_bin" ]]; then
        printf 'warning: cross hung after cargo finished for %s (Dory FEX); killing container\n' \
          "$triple" >&2
        reap_stuck_cross_containers "$triple"
        wait "$cross_pid" 2>/dev/null
        set -e
        return 0
      fi
    fi
    sleep 2
  done

  wait "$cross_pid"
  status=$?
  set -e
  return "$status"
}

has_required_image() {
  local image="$1"
  [[ -z "$image" ]] || docker image inspect "$image" >/dev/null 2>&1
}

build_native() {
  local triple="$1"

  (
    cd "$build_root" || exit 1
    "$CARGO" build --release --target "$triple"
  )
}

built=()
skipped=()
failed=()

mkdir -p artifacts

for platform in "${requested[@]}"; do
  entry="$(platform_entry "$platform")" \
    || die "unknown platform: $platform (supported: linux-x64 linux-arm64 darwin-arm64 win32-x64)"

  IFS='|' read -r _ triple binary required_image <<<"$entry"
  package_dir="npm/$platform"
  [[ -f "$package_dir/package.json" ]] || die "missing $package_dir/package.json"

  # Compare Rust triples because Node's platform omits the libc/toolchain ABI
  # (for example, linux-x64 can target x86_64-unknown-linux-musl).
  if [[ "$(package_build_driver "$rust_host_triple" "$triple")" == cargo ]]; then
    printf '==> %s (%s via cargo)\n' "$platform" "$triple"
    if ! build_native "$triple"; then
      failed+=("$platform: cargo build failed")
      printf 'fail: %s (cargo build failed)\n' "$platform" >&2
      continue
    fi
  else
    printf '==> %s (%s via cross)\n' "$platform" "$triple"

    if ! has_required_image "$required_image"; then
      skipped+=("$platform: missing local cross image $required_image")
      printf 'skip: %s (build local cross image %s)\n' "$platform" "$required_image"
      continue
    fi

    if ! build_with_cross "$triple"; then
      failed+=("$platform: cross build failed")
      printf 'fail: %s (cross build failed)\n' "$platform" >&2
      continue
    fi
  fi

  source_bin="$CARGO_TARGET_DIR/$triple/release/$binary"
  if [[ ! -f "$source_bin" ]]; then
    failed+=("$platform: missing $source_bin")
    printf 'fail: %s (missing %s)\n' "$platform" "$source_bin" >&2
    continue
  fi

  case "$platform" in
    linux-*)
      if ! verify_linux_static "$source_bin"; then
        failed+=("$platform: not statically linked")
        continue
      fi
      ;;
  esac

  mkdir -p "$package_dir/bin"
  cp "$source_bin" "$package_dir/bin/$binary"
  chmod 755 "$package_dir/bin/$binary" 2>/dev/null || true

  if ! smoke_host_binary "$platform" "$package_dir/bin/$binary"; then
    failed+=("$platform: host smoke test failed")
    printf 'fail: %s (host smoke test failed)\n' "$platform" >&2
    continue
  fi

  tar -C "$package_dir/bin" -czf "artifacts/${platform}.tar.gz" "$binary"
  built+=("$platform")
  printf 'packaged %s -> npm/%s/bin/%s and artifacts/%s.tar.gz\n' \
    "$platform" "$platform" "$binary" "$platform"
done

printf '\n'
if ((${#built[@]})); then
  printf 'built: %s\n' "${built[*]}"
else
  printf 'built: (none)\n'
fi
if ((${#skipped[@]})); then
  printf 'skipped:\n'
  printf '  %s\n' "${skipped[@]}"
fi
if ((${#failed[@]})); then
  printf 'failed:\n' >&2
  printf '  %s\n' "${failed[@]}" >&2
fi

if ((${#built[@]} == 0)); then
  die "no platforms were packaged"
fi

if [[ "${PACKAGE_STRICT:-0}" == 1 ]]; then
  if ((${#skipped[@]} + ${#failed[@]} > 0)); then
    die "PACKAGE_STRICT=1 and some platforms were skipped or failed"
  fi
fi

if ((${#failed[@]} > 0)); then
  exit 2
fi
