#!/bin/bash
#
# Purpose : Install and manage CoreDesktop sibling projects
#
# A sibling is a git repository cloned into ~/.local/share/siblings/<name>.
# If it has an install.sh, that runs once when the sibling is added.
# Every `update` pulls each sibling and runs its update.sh, if it has one
# (see updates.d/71-siblings.sh).
#
# A sibling's version is its latest git tag, e.g. v1.4.0, or v1.4.0+3 for three
# commits after it. A sibling without tags gets CoreDesktop's kind of version:
# the commit's date and short hash, e.g. 2026.11.02 (a1b2c3d).

SIBLINGS="$HOME/.local/share/siblings"

usage() {
  cat << EOF
Usage: sibling [command]

  add [url]        Install a sibling project from a git repository (default)
  list             Show installed sibling projects and their versions
  check            Show which sibling projects have a newer version
  update [name...] Update the named sibling projects, or all of them
  remove <name>    Uninstall a sibling project
EOF
}

# Version of the sibling in $1 at commit $2 (HEAD by default)
sibling_version() {
  local dir="$1" rev="${2:-HEAD}" tag ahead
  if tag="$(git -C "$dir" describe --tags --abbrev=0 "$rev" 2> /dev/null)"; then
    ahead="$(git -C "$dir" rev-list --count "$tag..$rev" 2> /dev/null)"
    if [[ "$ahead" =~ ^[0-9]+$ ]] && (( ahead > 0 )); then
      echo "$tag+$ahead"
    else
      echo "$tag"
    fi
  else
    git -C "$dir" log -1 --format='%cd (%h)' --date=format:%Y.%m.%d "$rev"
  fi
}

# Installed siblings' directories, one per line, in alphabetical order
sibling_dirs() {
  local dir
  for dir in "$SIBLINGS"/*/; do
    [ -d "$dir/.git" ] && echo "${dir%/}"
  done
}

# Fetch the sibling in $1 and print "name<TAB>installed<TAB>available", where
# available is the version update would install, empty if it's up to date, or
# ? if the sibling's remote couldn't be checked.
check_one() {
  local dir="$1" installed available="" behind
  installed="$(sibling_version "$dir")"
  if [ -n "$(git -C "$dir" status --porcelain --untracked-files=no 2> /dev/null)" ]; then
    installed="$installed, modified"
  fi
  if ! GIT_TERMINAL_PROMPT=0 timeout 30 git -C "$dir" fetch --quiet &> /dev/null \
    || ! behind="$(git -C "$dir" rev-list --count 'HEAD..@{upstream}' 2> /dev/null)"; then
    available="?"
  elif (( behind > 0 )); then
    available="$(sibling_version "$dir" '@{upstream}')"
  fi
  printf '%s\t%s\t%s\n' "$(basename "$dir")" "$installed" "$available"
}

# check_one for every sibling, all at once so a slow remote doesn't hold up the
# rest, printed in alphabetical order. Prints nothing if none are installed.
check_all() {
  local dirs dir i=0 tmp
  mapfile -t dirs < <(sibling_dirs)
  (( ${#dirs[@]} > 0 )) || return 0
  tmp="$(mktemp -d)"
  for dir in "${dirs[@]}"; do
    check_one "$dir" > "$tmp/$i" &
    i=$(( i + 1 ))
  done
  wait
  for (( i = 0; i < ${#dirs[@]}; i++ )); do
    cat "$tmp/$i"
  done
  rm -rf "$tmp"
}

# Print check_all's lines as a table. Returns 0 if any sibling has a newer
# version, 1 if none does.
show_checked() {
  local name installed available newer=1 width=0 vwidth=0 lines=()
  mapfile -t lines
  for line in "${lines[@]}"; do
    IFS=$'\t' read -r name installed _ <<< "$line"
    (( ${#name} > width )) && width=${#name}
    (( ${#installed} > vwidth )) && vwidth=${#installed}
  done
  for line in "${lines[@]}"; do
    IFS=$'\t' read -r name installed available <<< "$line"
    case "$available" in
      "") available="up to date" ;;
      "?") available="couldn't check" ;;
      *) available="$available is available"; newer=0 ;;
    esac
    printf '  %-*s  %-*s  %s\n' "$width" "$name" "$vwidth" "$installed" "$available"
  done
  return $newer
}

check_siblings() {
  if [ "$1" = "--porcelain" ]; then
    check_all
    return
  fi
  local checked
  checked="$(check_all)"
  if [ -z "$checked" ]; then
    echo "No sibling projects installed."
    return 1
  fi
  show_checked <<< "$checked"
}

# Pull each named sibling (all of them if none are named) and run its update.sh
update_siblings() {
  local dirs=() dir name failed=0
  if (( $# == 0 )); then
    mapfile -t dirs < <(sibling_dirs)
  fi
  for name in "$@"; do
    if [[ -z "$name" || "$name" == */* || "$name" == .* || ! -d "$SIBLINGS/$name/.git" ]]; then
      echo "No sibling named '$name' is installed."
      failed=1
      continue
    fi
    dirs+=("$SIBLINGS/$name")
  done
  for dir in "${dirs[@]}"; do
    name="$(basename "$dir")"
    echo "Updating $name..."
    if ! git -C "$dir" pull --ff-only --quiet; then
      echo "  Could not pull $name."
      failed=1
      continue
    fi
    if [ -f "$dir/update.sh" ]; then
      (cd "$dir" && bash update.sh) || { echo "  $name's update.sh failed."; failed=1; continue; }
    fi
    echo "  $name is at $(sibling_version "$dir")."
  done
  return $failed
}

# Derive a directory name from a git URL, e.g.
# https://host/user/my-project.git -> my-project
sibling_name() {
  local name="${1%/}"
  name="${name##*/}"
  name="${name##*:}"
  echo "${name%.git}"
}

add_sibling() {
  local url="$1" name dir

  if [ -z "$url" ]; then
    read -rp "Git repository URL for the sibling project: " url
  fi
  if [ -z "$url" ]; then
    echo "No URL given."
    return 1
  fi

  name="$(sibling_name "$url")"
  if [[ -z "$name" || "$name" == .* ]]; then
    echo "Could not work out a project name from: $url"
    return 1
  fi
  dir="$SIBLINGS/$name"

  mkdir -p "$SIBLINGS"
  if [ -d "$dir/.git" ]; then
    local existing
    existing="$(git -C "$dir" remote get-url origin 2>/dev/null)"
    if [ "$existing" != "$url" ]; then
      echo "A different sibling named '$name' is already installed from: $existing"
      return 1
    fi
    echo "$name is already installed; pulling the latest version..."
    git -C "$dir" pull --ff-only || return 1
  else
    echo "Cloning $name..."
    git clone "$url" "$dir" || return 1
  fi

  if [ -f "$dir/install.sh" ]; then
    echo "Running $name's install.sh..."
    if ! (cd "$dir" && bash install.sh); then
      echo "$name's install.sh failed. Fix the problem and run 'sibling add $url' again."
      return 1
    fi
  fi

  # Confirm the install
  if [ -d "$dir/.git" ] && git -C "$dir" rev-parse --verify --quiet HEAD > /dev/null; then
    echo "Installed $name $(sibling_version "$dir") at $dir."
    echo "'update' will keep it up to date."
  else
    echo "$name does not look installed correctly at $dir."
    return 1
  fi
}

list_siblings() {
  local dirs dir width=0 vwidth=0 versions=()
  mapfile -t dirs < <(sibling_dirs)
  if (( ${#dirs[@]} == 0 )); then
    echo "No sibling projects installed."
    return
  fi
  for dir in "${dirs[@]}"; do
    versions+=("$(sibling_version "$dir")")
    dir="$(basename "$dir")"
    (( ${#dir} > width )) && width=${#dir}
  done
  for version in "${versions[@]}"; do
    (( ${#version} > vwidth )) && vwidth=${#version}
  done
  for i in "${!dirs[@]}"; do
    printf '%-*s  %-*s  %s
' "$width" "$(basename "${dirs[$i]}")" "$vwidth" "${versions[$i]}" \
      "$(git -C "${dirs[$i]}" remote get-url origin 2> /dev/null)"
  done
}

remove_sibling() {
  local name="$1"
  if [[ -z "$name" || "$name" == */* || "$name" == .* ]]; then
    usage
    return 1
  fi
  if [ ! -d "$SIBLINGS/$name" ]; then
    echo "No sibling named '$name' is installed."
    return 1
  fi
  read -rp "Remove $SIBLINGS/$name? [y/N] " response
  case "$response" in
    [yY][eE][sS]|[yY])
      rm -rf "${SIBLINGS:?}/$name"
      echo "Removed $name. Anything its install.sh set up outside that folder is still there."
      ;;
    *)
      echo "Nothing removed."
      ;;
  esac
}

case "$1" in
  ""|add)        add_sibling "$2" ;;
  list|ls)       list_siblings ;;
  check)         check_siblings "$2" ;;
  update)        shift; update_siblings "$@" ;;
  remove|rm)     remove_sibling "$2" ;;
  -h|--help|help) usage ;;
  *)             usage; exit 1 ;;
esac
