#!/usr/bin/env bash
#
# gcfs — a minimal gocryptfs vault manager
#
# Config-driven, monochrome, dependency-light. Lets gocryptfs handle the
# password prompt itself, so no secret ever touches this script.
#
# Config:  $XDG_CONFIG_HOME/gcfs/vaults.conf
# Format:  name|cipherdir|mountpoint      (# comments and blank lines ignored)
#
# Usage:   gcfs                 interactive menu
#          gcfs status          show all vaults and their state (default)
#          gcfs mount   <name>  mount a vault (name or number)
#          gcfs umount  <name>  unmount a vault (name or number)
#          gcfs mount-all       mount every vault
#          gcfs umount-all      unmount every mounted vault
#          gcfs add             register a new vault interactively
#          gcfs init            create a fresh gocryptfs cipherdir
#          gcfs edit            open the config in $EDITOR
#          gcfs help            this text

set -euo pipefail

CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/gcfs"
CONFIG_FILE="$CONFIG_DIR/vaults.conf"

# ---- styling (degrades gracefully with no tty) --------------------------
if [[ -t 1 ]]; then
  B=$(tput bold    2>/dev/null || true)
  D=$(tput dim     2>/dev/null || true)
  U=$(tput smul    2>/dev/null || true)
  R=$(tput sgr0    2>/dev/null || true)
else
  B=""; D=""; U=""; R=""
fi

ON="●"    # mounted
OFF="○"   # unmounted
BAD="✗"   # broken (missing cipherdir)

# ---- output helpers -----------------------------------------------------
msg()  { printf '%s\n' "$*"; }
info() { printf '%s\n' "${D}$*${R}"; }
warn() { printf '%s\n' "${B}!${R} $*" >&2; }
die()  { printf '%s\n' "${B}✗${R} $*" >&2; exit 1; }

# ---- dependency + config bootstrap --------------------------------------
need() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }
need gocryptfs
need fusermount

ensure_config() {
  [[ -f "$CONFIG_FILE" ]] && return
  mkdir -p "$CONFIG_DIR"
  cat > "$CONFIG_FILE" <<'EOF'
# gcfs vaults — one per line:  name|cipherdir|mountpoint
# leading ~/ is expanded to your home. lines starting with # are ignored.
#
# example:
# docs|~/.vaults/docs|~/vault/docs
EOF
  info "created $CONFIG_FILE — add a vault with 'gcfs add'"
}

# expand only a leading ~/ (safe, no eval)
expand() { case "$1" in "~/"*) printf '%s\n' "$HOME/${1#\~/}";; *) printf '%s\n' "$1";; esac; }

# ---- load vaults into parallel arrays -----------------------------------
declare -a NAMES CIPHERS MOUNTS
load_vaults() {
  NAMES=(); CIPHERS=(); MOUNTS=()
  local line name cipher mount
  while IFS='|' read -r name cipher mount || [[ -n "$name" ]]; do
    [[ -z "${name// }" ]] && continue
    [[ "${name#\#}" != "$name" ]] && continue
    name="${name## }"; name="${name%% }"
    NAMES+=("$name")
    CIPHERS+=("$(expand "${cipher## }")")
    MOUNTS+=("$(expand "${mount## }")")
  done < "$CONFIG_FILE"
}

is_mounted() { mountpoint -q -- "$1" 2>/dev/null; }

# resolve a user token (name or 1-based index) to an array index
resolve() {
  local tok="$1" i
  if [[ "$tok" =~ ^[0-9]+$ ]]; then
    (( tok >= 1 && tok <= ${#NAMES[@]} )) || die "no vault #$tok"
    printf '%s\n' "$(( tok - 1 ))"; return
  fi
  for i in "${!NAMES[@]}"; do
    [[ "${NAMES[$i]}" == "$tok" ]] && { printf '%s\n' "$i"; return; }
  done
  die "no vault named '$tok'"
}

# ---- commands -----------------------------------------------------------
cmd_status() {
  load_vaults
  (( ${#NAMES[@]} )) || { info "no vaults configured — 'gcfs add'"; return; }

  local w=4 i
  for i in "${!NAMES[@]}"; do (( ${#NAMES[$i]} > w )) && w=${#NAMES[$i]}; done

  printf '%s\n' "${B}  # $(printf "%-${w}s" NAME)  MOUNTPOINT${R}"
  for i in "${!NAMES[@]}"; do
    local mark clr
    if [[ ! -f "${CIPHERS[$i]}/gocryptfs.conf" ]]; then
      mark="$BAD"; clr="$D"
    elif is_mounted "${MOUNTS[$i]}"; then
      mark="$ON"; clr="$B"
    else
      mark="$OFF"; clr="$D"
    fi
    printf '%s %2d %s %s  %s%s\n' \
      "$clr$mark$R" "$((i+1))" "$B" \
      "$(printf "%-${w}s" "${NAMES[$i]}")" \
      "${D}${MOUNTS[$i]}${R}" "$R"
  done
}

cmd_mount() {
  local i; i=$(resolve "$1"); load_vaults
  local cipher="${CIPHERS[$i]}" mount="${MOUNTS[$i]}" name="${NAMES[$i]}"
  [[ -f "$cipher/gocryptfs.conf" ]] || die "'$name': no gocryptfs.conf in $cipher"
  if is_mounted "$mount"; then info "'$name' already mounted"; return; fi
  mkdir -p -- "$mount"
  msg "${B}unlocking${R} $name → ${D}$mount${R}"
  if gocryptfs -- "$cipher" "$mount"; then
    msg "$ON mounted $name"
  else
    die "failed to mount $name"
  fi
}

cmd_umount() {
  local i; i=$(resolve "$1"); load_vaults
  local mount="${MOUNTS[$i]}" name="${NAMES[$i]}"
  is_mounted "$mount" || { info "'$name' not mounted"; return; }
  if fusermount -u -- "$mount" 2>/dev/null; then
    msg "$OFF unmounted $name"
  else
    warn "'$name' is busy — close open files, or force with: fusermount -uz -- '$mount'"
    return 1
  fi
}

cmd_mount_all()  { load_vaults; local i; for i in "${!NAMES[@]}"; do cmd_mount  "$((i+1))" || true; done; }
cmd_umount_all() { load_vaults; local i; for i in "${!NAMES[@]}"; do cmd_umount "$((i+1))" || true; done; }

cmd_add() {
  local name cipher mount
  read -rp "name:        " name
  read -rp "cipherdir:   " cipher
  read -rp "mountpoint:  " mount
  [[ -n "$name" && -n "$cipher" && -n "$mount" ]] || die "all fields required"
  printf '%s|%s|%s\n' "$name" "$cipher" "$mount" >> "$CONFIG_FILE"
  msg "$OFF added $name"
}

cmd_init() {
  local cipher="${1:-}"
  [[ -n "$cipher" ]] || read -rp "new cipherdir: " cipher
  cipher="$(expand "$cipher")"
  mkdir -p -- "$cipher"
  gocryptfs -init -- "$cipher"
  info "initialised — register it with 'gcfs add' (cipherdir: $cipher)"
}

cmd_edit() { "${EDITOR:-vi}" "$CONFIG_FILE"; }

cmd_help() { sed -n '3,29p' "$0" | sed 's/^# \{0,1\}//'; }

# ---- interactive menu ---------------------------------------------------
menu() {
  while :; do
    printf '\n'
    cmd_status
    printf '\n%s[number]%s toggle  %s[a]%sdd  %s[i]%snit  %s[q]%suit  ' \
      "$B" "$R" "$B" "$R" "$B" "$R" "$B" "$R"
    read -r choice || { printf '\n'; break; }
    case "$choice" in
      q|Q|"") break ;;
      a|A)    cmd_add ;;
      i|I)    cmd_init ;;
      *)
        load_vaults
        idx=$(resolve "$choice") || continue
        if is_mounted "${MOUNTS[$idx]}"; then
          cmd_umount "$choice" || true
        else
          cmd_mount "$choice" || true
        fi
        ;;
    esac
  done
}

# ---- dispatch -----------------------------------------------------------
ensure_config
case "${1:-menu}" in
  menu)               menu ;;
  status|st|ls)       cmd_status ;;
  mount|m)            shift; [[ $# -ge 1 ]] || die "usage: gcfs mount <name|#>"; cmd_mount "$1" ;;
  umount|unmount|u)   shift; [[ $# -ge 1 ]] || die "usage: gcfs umount <name|#>"; cmd_umount "$1" ;;
  mount-all)          cmd_mount_all ;;
  umount-all)         cmd_umount_all ;;
  add)                cmd_add ;;
  init)               shift; cmd_init "${1:-}" ;;
  edit)               cmd_edit ;;
  help|-h|--help)     cmd_help ;;
  *)                  die "unknown command '$1' — try 'gcfs help'" ;;
esac
