#!/usr/bin/env bash

: <<'DOC'
Language-agnostic release artifact packaging.

Produces canonically-named tarballs from files or directories.
Callers handle any pre-packaging steps (stripping, signing, etc.).
DOC

TARBALL_DEFAULT_FORMAT="tar.gz"
TARBALL_ARTIFACTS_SCHEMA="${SHELLOPS_DIR}/lib/tarball/artifacts.schema.json"

: <<'DOC'
Validates an artifacts JSON array: checks schema conformance and verifies
that every listed file path exists on disk.

  tarball_validate_artifacts "$json"
  tarball_validate_artifacts "$(cat artifacts.json)"
DOC
function tarball_validate_artifacts() {
  local json="$1"

  if ! jq_validate "$json" "$TARBALL_ARTIFACTS_SCHEMA"; then
    return 1
  fi

  local paths
  paths="$(jq_get "$json" '.[]')"

  while IFS= read -r path; do
    [[ -n "$path" ]] || continue
    if [[ ! -f "$path" && ! -d "$path" ]]; then
      error "artifact not found: $path"
      return 1
    fi
  done <<< "$paths"
}

: <<'DOC'
Validates variadic file paths and writes an artifacts.json to the given output
directory. Prints the validated JSON array to stdout.

  tarball_save_artifacts "./dist" path/to/a.tar.gz path/to/b.tar.gz
DOC
function tarball_save_artifacts() {
  local outdir="$1"
  shift

  if [[ $# -eq 0 ]]; then
    error "no artifact paths provided"
    return 1
  fi

  local json
  json="$(jq_arr "$@")"

  if ! tarball_validate_artifacts "$json"; then
    return 1
  fi

  mkdir -p "$outdir"
  echo "$json" | tee "${outdir}/artifacts.json"
}

: <<'DOC'
Reads an artifacts JSON file, validates it, and prints each path line-by-line.

  mapfile -t paths < <(tarball_load_artifacts "./dist/artifacts.json")
DOC
function tarball_load_artifacts() {
  local file="$1"

  if [[ ! -f "$file" ]]; then
    error "artifacts file not found: $file"
    return 1
  fi

  local json
  json="$(<"$file")"

  if ! tarball_validate_artifacts "$json"; then
    error "invalid artifacts file: $file"
    return 1
  fi

  jq_get "$json" '.[]'
}

: <<'DOC'
Returns the canonical artifact filename for a release tarball.

  tarball_artifact_name "insights" "1.2.0" "x86_64-apple-darwin"
  # => insights-1.2.0-x86_64-apple-darwin.tar.gz

  tarball_artifact_name "violet" "0.3.1" "aarch64-unknown-linux-gnu" "tar.xz"
  # => violet-0.3.1-aarch64-unknown-linux-gnu.tar.xz
DOC
function tarball_artifact_name() {
  local name="$1" version="$2" triple="$3"
  local format="${4:-$TARBALL_DEFAULT_FORMAT}"
  echo "${name}-${version}-${triple}.${format}"
}

: <<'DOC'
Stages a file or directory into the build directory layout expected by
tarball_create_all.

  tarball_stage "/path/to/binary" "$build_dir" "x86_64-apple-darwin"
  tarball_stage "/path/to/dir" "$build_dir" "x86_64-apple-darwin"
DOC
function tarball_stage() {
  local source="$1" build_dir="$2" triple="$3"
  mkdir -p "${build_dir}/${triple}"

  if [[ -d "$source" ]]; then
    cp -r "$source" "${build_dir}/${triple}/"
  else
    cp "$source" "${build_dir}/${triple}/"
  fi
}

: <<'DOC'
Stages a file or directory for every triple in a JSON array. The
source_callback receives each triple as its first argument, followed by any
extra args, and should echo the path to the artifact to stage.

  tarball_stage_all "$build_dir" "$triples" my_source_callback [extra_args...]
  # callback signature: my_source_callback "x86_64-apple-darwin" [extra_args...] -> echoes path
DOC
function tarball_stage_all() {
  local build_dir="$1" triples="$2" source_callback="$3"
  shift 3
  jq_foreach __tarball__stage_one "$triples" "$build_dir" "$source_callback" "$@"
}

function __tarball__stage_one() {
  local triple build_dir="$2" source_callback="$3"
  triple="$(jq_unwrap "$1")"
  shift 3
  local source
  source="$("$source_callback" "$triple" "$@")"
  tarball_stage "$source" "$build_dir" "$triple"
}

: <<'DOC'
Packages a file or directory into a canonically-named tarball.

The tarball is created in outdir. The archived content is placed
under a top-level directory matching the artifact stem (name-version-triple)
so extraction produces a single predictable folder.

  tarball_create "/path/to/binary" "insights" "1.2.0" "x86_64-apple-darwin" "./dist"

Returns the path to the created tarball.
DOC
function tarball_create() {
  local source="$1" name="$2" version="$3" triple="$4" outdir="$5"
  local format="${6:-$TARBALL_DEFAULT_FORMAT}"

  local artifact_name stem
  artifact_name="$(tarball_artifact_name "$name" "$version" "$triple" "$format")"
  stem="${artifact_name%."${format}"}"

  local staging
  staging="$(mktemp -d)"
  mkdir -p "${staging}/${stem}"

  if [[ -d "$source" ]]; then
    cp -r "$source"/. "${staging}/${stem}/"
  else
    cp "$source" "${staging}/${stem}/"
  fi

  mkdir -p "$outdir"

  local -a tar_cmd=(tar -C "$staging")
  local tar_flag
  tar_flag="$(__tarball__compression_flag "$format")"
  [[ -n "$tar_flag" ]] && tar_cmd+=("$tar_flag")
  tar_cmd+=(-cf "${outdir}/${artifact_name}" "$stem")
  "${tar_cmd[@]}"

  rm -rf "$staging"

  local dest="${outdir}/${artifact_name}"
  local size
  size="$(__tarball__human_size "$dest")"
  log "packaged ${dest} (${size})"
  echo "$dest"
}

function __tarball__compression_flag() {
  local format="$1"
  case "$format" in
    tar.gz)  echo "-z" ;;
    tar.xz)  echo "-J" ;;
    tar.bz2) echo "-j" ;;
    tar)     echo "" ;;
    *)
      error "tarball: unsupported format '$format'"
      return 1
      ;;
  esac
}

: <<'DOC'
Packages every triple's artifact from a build output directory.

Expects a directory layout where each triple has its own subdirectory
containing the file(s) to package:

  build_output/
    x86_64-apple-darwin/
      mybin
    aarch64-unknown-linux-gnu/
      mybin

  tarball_create_all "./build_output" "mybin" "1.0.0" "./dist"

Creates one tarball per subdirectory. Returns a JSON array of output paths.
DOC
function tarball_create_all() {
  local build_dir="$1" name="$2" version="$3" outdir="$4"
  local format="${5:-$TARBALL_DEFAULT_FORMAT}"

  local results="[]"

  for triple_dir in "$build_dir"/*/; do
    [[ -d "$triple_dir" ]] || continue

    local triple
    triple="$(basename "$triple_dir")"

    local dest
    dest="$(tarball_create "$triple_dir" "$name" "$version" "$triple" "$outdir" "$format")"
    results="$(jq_push "$results" '.' "$(jq_wrap "$dest")")"
  done

  echo "$results"
}

function __tarball__human_size() {
  local file="$1"
  local bytes_per_kb=1024
  local bytes_per_mb=$(( 1024 * 1024 ))
  local bytes_per_gb=$(( 1024 * 1024 * 1024 ))

  if [[ ! -f "$file" ]]; then
    error "not a file: $file"
    return 1
  fi

  local bytes
  bytes="$(stat --printf='%s' "$file" 2>/dev/null || stat -f '%z' "$file" 2>/dev/null)"

  if (( bytes >= bytes_per_gb )); then
    __tarball__fmt_size "$bytes" "$bytes_per_gb" "G"
  elif (( bytes >= bytes_per_mb )); then
    __tarball__fmt_size "$bytes" "$bytes_per_mb" "M"
  elif (( bytes >= bytes_per_kb )); then
    __tarball__fmt_size "$bytes" "$bytes_per_kb" "K"
  else
    echo "${bytes}B"
  fi
}

function __tarball__fmt_size() {
  local bytes="$1" divisor="$2" suffix="$3"
  local whole=$(( bytes / divisor ))
  local remainder=$(( (bytes % divisor) * 10 / divisor ))
  echo "${whole}.${remainder}${suffix}"
}
