#!/bin/sh

set -eu

usage() {
  cat <<'EOF'
Usage: update-tribes-pin [options] [rev]

Pin guix-tribes to a Tribes commit and refresh all fixed-output hashes.

By default, REV is "master" resolved from the local Tribes checkout. Hashing and
Guix builds run with local `guix` unless --build-host is provided.

Options:
  --tribes-repo PATH      Local Tribes git checkout
  --guix-repo PATH        Local guix-tribes checkout
  --build-host HOST       SSH host used for Guix builds and hashing
  -h, --help              Show this help
EOF
}

script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
default_guix_repo=$(CDPATH= cd -- "$script_dir/.." && pwd)
default_tribes_repo=$(CDPATH= cd -- "$default_guix_repo/../tribes" && pwd)

tribes_repo=$default_tribes_repo
guix_repo=$default_guix_repo
build_host=
rev=master

while [ "$#" -gt 0 ]; do
  case "$1" in
    --tribes-repo)
      tribes_repo=$2
      shift 2
      ;;
    --guix-repo)
      guix_repo=$2
      shift 2
      ;;
    --build-host)
      build_host=$2
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    --)
      shift
      break
      ;;
    -*)
      printf 'Unknown option: %s\n' "$1" >&2
      usage >&2
      exit 1
      ;;
    *)
      rev=$1
      shift
      ;;
  esac
done

if [ "$#" -gt 0 ]; then
  printf 'Unexpected arguments: %s\n' "$*" >&2
  usage >&2
  exit 1
fi

source_file=$guix_repo/tribes/packages/source.scm

if [ ! -d "$tribes_repo/.git" ]; then
  printf 'Tribes repo not found: %s\n' "$tribes_repo" >&2
  exit 1
fi

if [ ! -f "$source_file" ]; then
  printf 'guix-tribes source file not found: %s\n' "$source_file" >&2
  exit 1
fi

require_tool() {
  if ! command -v "$1" >/dev/null 2>&1; then
    printf 'Missing required tool: %s\n' "$1" >&2
    exit 1
  fi
}

require_tool env
require_tool git
require_tool tar
require_tool perl
require_tool mktemp

if [ -n "$build_host" ]; then
  use_remote=true
else
  use_remote=false
  require_tool guix
fi

commit=$(git -C "$tribes_repo" rev-parse "$rev^{commit}")
version=$(env LC_ALL=C perl -0ne 'print $1 if /\(git-version "([^"]+)"/' "$source_file")

if [ -z "$version" ]; then
  printf 'Could not read Tribes version from %s\n' "$source_file" >&2
  exit 1
fi

local_tmp=$(mktemp -d "${TMPDIR:-/tmp}/tribes-pin.XXXXXX")
remote_tmp=

cleanup() {
  if [ -n "${remote_tmp:-}" ]; then
    ssh "$build_host" "rm -rf '$remote_tmp'" >/dev/null 2>&1 || true
  fi
  rm -rf "$local_tmp"
}

trap cleanup EXIT INT TERM

mkdir -p "$local_tmp/tribes-source"
git -C "$tribes_repo" archive "$commit" | tar -x -C "$local_tmp/tribes-source"

setup_remote() {
  require_tool rsync
  require_tool ssh

  if [ -z "${remote_tmp:-}" ]; then
    printf 'Using build host %s.\n' "$build_host" >&2
    remote_tmp=$(ssh "$build_host" 'mktemp -d /tmp/tribes-pin.XXXXXX')

    rsync -az --delete --exclude .git "$guix_repo/" "$build_host:$remote_tmp/guix-tribes/"
    rsync -az --delete "$local_tmp/tribes-source/" "$build_host:$remote_tmp/tribes-source/"
  fi

  source_root=$remote_tmp/tribes-source
  guix_load_path=$remote_tmp/guix-tribes
  source_hash=$(ssh "$build_host" "guix hash -rx '$source_root'" | tr -d '\r')
}

if [ "$use_remote" = true ]; then
  setup_remote
else
  source_root=$local_tmp/tribes-source
  guix_load_path=$guix_repo
  source_hash=$(guix hash -rx "$source_root" | tr -d '\r')
fi

run_scheme() {
  name=$1
  body=$2

  if [ "$use_remote" = true ]; then
    ssh "$build_host" "cat > '$remote_tmp/$name.scm' <<'EOF'
$body
EOF"

    ssh "$build_host" "guix build -L '$guix_load_path' -f '$remote_tmp/$name.scm' --no-grafts 2>&1" || true
  else
    scheme_file=$local_tmp/$name.scm
    cat > "$scheme_file" <<EOF
$body
EOF

    guix build -L "$guix_load_path" -f "$scheme_file" --no-grafts 2>&1 || true
  fi
}

extract_hash() {
  env LC_ALL=C perl -ne '
    if (s/.*\bgot:\s*([0-9a-z]{52}).*/$1/) {
      chomp;
      $hash = $_;
    } elsif (s/.*\bactual hash:\s*([0-9a-z]{52}).*/$1/) {
      chomp;
      $hash = $_;
    }
    END {
      if (defined $hash) {
        print $hash;
      } else {
        exit 1;
      }
    }
  '
}

dummy_hash=0000000000000000000000000000000000000000000000000000

raw_mix_deps_output() {
  run_scheme raw-mix-deps "(use-modules (guix gexp) (tribes packages source))
(fetch-mix-deps
  (local-file \"$source_root\" #:recursive? #t)
  #:name \"tribes-mix-deps-raw\"
  #:version \"$version\"
  #:sha256 \"$dummy_hash\")"
}

mix_deps_output() {
  run_scheme mix-deps "(use-modules (guix gexp) (tribes packages source))
(tribes-mix-deps
  (local-file \"$source_root\" #:recursive? #t)
  #:name \"tribes-mix-deps\"
  #:version \"$version\"
  #:raw-sha256 \"$raw_mix_hash\"
  #:sha256 \"$dummy_hash\")"
}

npm_deps_output() {
  run_scheme npm-deps "(use-modules (guix gexp) (tribes packages source))
(fetch-npm-deps
  (local-file \"$source_root\" #:recursive? #t)
  #:mix-fod-deps
  (tribes-mix-deps
    (local-file \"$source_root\" #:recursive? #t)
    #:name \"tribes-mix-deps\"
    #:version \"$version\"
    #:raw-sha256 \"$raw_mix_hash\"
    #:sha256 \"$mix_hash\")
  #:name \"tribes-npm-deps\"
  #:version \"$version\"
  #:sha256 \"$dummy_hash\")"
}

hash_failure_hint() {
  label=$1
  output=$2

  printf 'Failed to extract %s hash.\n' "$label" >&2
  if [ "$use_remote" = false ]; then
    printf 'Local Guix did not complete the hash refresh. To run this step on a build host, rerun with --build-host HOST.\n' >&2
  else
    printf 'Build host Guix did not complete the hash refresh.\n' >&2
  fi
  printf '%s\n' "$output" >&2
  exit 1
}

raw_output=$(raw_mix_deps_output)
raw_mix_hash=$(printf '%s\n' "$raw_output" | extract_hash) || hash_failure_hint 'raw mix deps' "$raw_output"

mix_output=$(mix_deps_output)
mix_hash=$(printf '%s\n' "$mix_output" | extract_hash) || hash_failure_hint 'prepared mix deps' "$mix_output"

npm_output=$(npm_deps_output)
npm_hash=$(printf '%s\n' "$npm_output" | extract_hash) || hash_failure_hint 'npm deps' "$npm_output"

COMMIT=$commit \
SOURCE_HASH=$source_hash \
RAW_MIX_HASH=$raw_mix_hash \
MIX_HASH=$mix_hash \
NPM_HASH=$npm_hash \
env LC_ALL=C perl -0pi -e '
  s/\(define %tribes-raw-mix-deps-sha256\n  "[^"]+"\)/(define %tribes-raw-mix-deps-sha256\n  "$ENV{RAW_MIX_HASH}")/;
  s/\(define %tribes-mix-deps-sha256\n  "[^"]+"\)/(define %tribes-mix-deps-sha256\n  "$ENV{MIX_HASH}")/;
  s/\(define %tribes-npm-deps-sha256\n  "[^"]+"\)/(define %tribes-npm-deps-sha256\n  "$ENV{NPM_HASH}")/;
  s/\(define %tribes-commit\n  "[^"]+"\)/(define %tribes-commit\n  "$ENV{COMMIT}")/;
  s/\(define %tribes-source-sha256\n  "[^"]+"\)/(define %tribes-source-sha256\n  "$ENV{SOURCE_HASH}")/;
' "$source_file"

printf 'Updated %s\n' "$source_file"
printf 'commit: %s\n' "$commit"
printf 'source sha256: %s\n' "$source_hash"
printf 'raw mix deps sha256: %s\n' "$raw_mix_hash"
printf 'mix deps sha256: %s\n' "$mix_hash"
printf 'npm deps sha256: %s\n' "$npm_hash"
