#!/usr/bin/env bash
# Manage homelab SSH host keys and sops age keys in the Proton Pass HomeLab vault.
#
# Vault naming (HomeLab vault; override with HOMELAB_PASS_VAULT):
#
#   ssh_host#<config>   ssh-key item  ↔  ~/.config/homelab/<config>/ssh_host_ed25519_key{,.pub}
#   age#<config>        note item     ↔  ~/.config/homelab/<config>/age.txt
#   age#admin           note item     ↔  ~/.config/sops/age/keys.txt
#
# Usage:
#   ./scripts/keys store   [--force] [<config>...]
#   ./scripts/keys restore [<config>...]
#
# store:   upload local keys to the vault. Skips items that already exist
#          unless --force is given (deletes the existing item first).
# restore: download vault items to local files with correct permissions.
#
# With no <config> args both subcommands operate on every hosts/ directory.
# The admin age key is always included regardless of <config> args.
set -euo pipefail

SCRIPT_DIR="$(dirname "$(realpath "$0")")"
REPO="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || dirname "$SCRIPT_DIR")"
cd "$REPO"

KEYDIR="${HOMELAB_KEY_DIR:-${HOME:-/root}/.config/homelab}"
ADMIN_AGE="${HOME:-/root}/.config/sops/age/keys.txt"
VAULT="${HOMELAB_PASS_VAULT:-HomeLab}"

die()  { echo "error: $*" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1"; }

need pass-cli
need jq

# Resolve an active vault item by title → item ID, or empty string.
# Active-only filter avoids the trashed-item-shadows-active bug (see deploy).
resolve_item() {
  local title="$1"
  pass-cli item list --vault-name "$VAULT" --filter-state active --output human 2>/dev/null \
    | awk -v t="$title" '
        { i = index($0, "]: "); if (i == 0) next
          id   = substr($0, 4, i - 4)
          rest = substr($0, i + 3)
          sub(/ \(state=[^)]*\)$/, "", rest)
          if (rest == t) { print id; exit } }' || true
}

# All configs: every hosts/<dir> that has a configuration.nix.
all_configs() {
  for d in hosts/*/; do
    [ -f "${d}configuration.nix" ] && basename "$d"
  done
}

# ---------------------------------------------------------------------------
# store helpers
# ---------------------------------------------------------------------------

# Returns 0 (skip) if item exists and --force was not given; 1 (proceed) otherwise.
# Deletes the existing item when --force is set.
should_store() {
  local title="$1" id
  id="$(resolve_item "$title")"
  if [ -n "$id" ]; then
    if [ -n "$FORCE" ]; then
      echo ">> $title: deleting existing item (--force)"
      pass-cli item delete --vault-name "$VAULT" --item-id "$id"
    else
      echo ">> $title: already in vault — skipping (use --force to overwrite)"
      return 0
    fi
  fi
  return 1
}

store_ssh_key() {
  local config="$1" keyfile="$KEYDIR/$1/ssh_host_ed25519_key" title="ssh_host#$1"
  [ -f "$keyfile" ] || { echo ">> $title: $keyfile not found — skipping"; return; }
  should_store "$title" && return
  echo ">> $title: uploading"
  pass-cli item create ssh-key import \
    --from-private-key "$keyfile" \
    --title "$title" \
    --vault-name "$VAULT"
}

store_age_key() {
  local title="$1" src="$2"
  [ -f "$src" ] || { echo ">> $title: $src not found — skipping"; return; }
  should_store "$title" && return
  echo ">> $title: uploading"
  pass-cli item create note \
    --title "$title" \
    --note "$(cat "$src")" \
    --vault-name "$VAULT"
}

# ---------------------------------------------------------------------------
# restore helpers
# ---------------------------------------------------------------------------

restore_ssh_key() {
  local config="$1" title="ssh_host#$1" id json
  id="$(resolve_item "$title")"
  if [ -z "$id" ]; then
    echo ">> $title: not in vault — skipping"
    return
  fi
  echo ">> $title: restoring"
  json="$(pass-cli item view --vault-name "$VAULT" --item-id "$id" --output json)"
  private_key="$(jq -r '.item.content.content.SshKey.private_key' <<< "$json")"
  public_key="$(jq  -r '.item.content.content.SshKey.public_key'  <<< "$json")"
  [ -n "$private_key" ] || die "$title: private_key missing from vault item JSON"
  [ -n "$public_key"  ] || die "$title: public_key missing from vault item JSON"
  mkdir -p "$KEYDIR/$config"
  printf '%s' "$private_key" > "$KEYDIR/$config/ssh_host_ed25519_key"
  chmod 600 "$KEYDIR/$config/ssh_host_ed25519_key"
  printf '%s\n' "$public_key" > "$KEYDIR/$config/ssh_host_ed25519_key.pub"
  chmod 644 "$KEYDIR/$config/ssh_host_ed25519_key.pub"
  echo "   → $KEYDIR/$config/ssh_host_ed25519_key{,.pub}"
}

restore_age_key() {
  local title="$1" dest="$2" id content
  id="$(resolve_item "$title")"
  if [ -z "$id" ]; then
    echo ">> $title: not in vault — skipping"
    return
  fi
  echo ">> $title: restoring"
  content="$(pass-cli item view --vault-name "$VAULT" --item-id "$id" \
    --output json | jq -r '.item.content.note')"
  [ -n "$content" ] || die "$title: note field is empty in vault item"
  mkdir -p "$(dirname "$dest")"
  printf '%s\n' "$content" > "$dest"
  chmod 600 "$dest"
  echo "   → $dest"
}

# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------

cmd="${1:-}"
[ -n "$cmd" ] || die "usage: ./scripts/keys <store|restore> [--force] [<config>...]"
shift

FORCE=""
[ "${1:-}" = "--force" ] && { FORCE=1; shift; }

configs=("$@")
[ "${#configs[@]}" -gt 0 ] || mapfile -t configs < <(all_configs)

case "$cmd" in
  store)
    for config in "${configs[@]}"; do
      store_ssh_key "$config"
      store_age_key "age#$config" "$KEYDIR/$config/age.txt"
    done
    store_age_key "age#admin" "$ADMIN_AGE"
    ;;

  restore)
    for config in "${configs[@]}"; do
      restore_ssh_key "$config"
      restore_age_key "age#$config" "$KEYDIR/$config/age.txt"
    done
    restore_age_key "age#admin" "$ADMIN_AGE"
    ;;

  *)
    die "unknown command '$cmd' — usage: ./scripts/keys <store|restore> [--force] [<config>...]"
    ;;
esac
