#!/usr/bin/env bash
#
# gopass_bernina_setup — one-shot onboarding for a *human* PSI account
# joining the shared "bernina" gopass secrets store. See gopass.md (same
# directory) for the full picture; this automates the "Onboarding someone
# new" section.
#
# Storage backend: prefers ~/network_drives (SMB, Partially Shared Home) -
# the account's GPG keyring, gopass config, and the store clone all end up
# there, so they're already the same on every host without any separate
# provisioning step. Falls back to /sf/bernina/config/personal/<you> (NFS4)
# if network_drives isn't reachable on this host - see gopass.md
# "Store location" for the trade-off.
#
# THE WORKAROUND THIS SCRIPT USES for network_drives (confirmed by hand -
# see gopass.md): network_drives (SMB) rejects the chmod calls that
# `git init`/`git clone`/`git config`/`gpg --gen-key` make internally, so
# those must never run directly there ("Operation not supported"). An
# ALREADY-initialized repo or keyring, copied on with `cp -r` (rsync also
# works on some shares but confirmed to fail outright on at least one -
# see copy_tree() below), keeps working normally afterward:
# add/commit/push/pull, encrypt/decrypt, and `gopass insert`/`show`/`sync`
# all confirmed. So: do the actual init/clone/gen-key/config in a local
# scratch directory first, then copy the result onto network_drives and
# use it from there.
#
# What this script CANNOT do for you — gitea has no API for these, and
# they're each an action only another human/account can take:
#   1. Registering your SSH public key with gitea.psi.ch
#      (Settings -> SSH/GPG Keys -> Add Key)
#   2. Getting added as a Collaborator (Write access) on the
#      Bernina/bernina-secrets repo
#   3. Getting your GPG key authorized as a recipient - an existing
#      recipient (currently: lemke_h) has to run `gopass recipients add`
#      for you (see gopass.md) after you send them your exported
#      public key
# The script detects each of these and tells you exactly what's missing
# and what to do, then stops - re-run it after fixing that one thing.
#
# Usage: gopass_bernina_setup [your-psi-email]
#   Defaults to <your-username>@psi.ch.
#
# Safe to re-run: every step checks current state first and skips work
# that's already done. If you already have a working GPG key/store in an
# old location (e.g. $HOME/.gnupg, or a previous /sf/bernina-only setup),
# this OFFERS to copy it into the new location rather than generating a
# fresh one - keeps the same recipient registration, no re-authorization
# needed.
#
set -euo pipefail

GITEA_REMOTE="git@gitea.psi.ch:Bernina/bernina-secrets.git"
STORE_ALIAS="bernina"
PERSONAL_ROOT="/sf/bernina/config/personal"
BIN_DIR="/sf/bernina/bin"

log()  { echo "==> $*"; }
warn() { echo "!!  $*" >&2; }
ask() {
    local reply
    read -r -p "$1 [y/N] " reply
    [[ "$reply" =~ ^[Yy]$ ]]
}
copy_tree() {
    # `rsync -rlt` was the first choice here (no -p/-a - permission
    # preservation doesn't work on network_drives) and works fine on some
    # network_drives/SMB shares, but confirmed to fail on at least one
    # (gac-bernina's: exit 3, "Errors selecting input/output files, dirs",
    # with -v producing zero output even with stderr combined - looks
    # like an rsync/gvfs-fuse incompatibility specific to that share).
    # `cp -r` confirmed to work where that rsync failed, so use it
    # everywhere instead for consistency.
    mkdir -p "$2"
    cp -r "$1/." "$2/"
}

if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
    cat <<'USAGE'
gopass_bernina_setup [your-psi-email]

For: any HUMAN PSI account that needs to read/write the shared "bernina"
gopass secrets store - this is the SAME script and procedure whether
you're the admin (the one who also runs `gopass recipients add` for
others) or a normal group user. There is no separate "admin mode".

NOT for the gac-bernina service account - see
gopass_bernina_service_setup for that (different: passphrase-less key,
deploy key instead of a personal SSH key, non-interactive).

Defaults to <your-username>@psi.ch. Safe to re-run.
USAGE
    exit 0
fi

USERNAME="$(whoami)"
EMAIL="${1:-${USERNAME}@psi.ch}"

# --- 0. pick a storage backend ----------------------------------------------
if [[ -d "${HOME}/network_drives/home" ]]; then
    BACKEND="network_drives"
    ROOT_DIR="${HOME}/network_drives/home/gopass"
    NEEDS_SCRATCH=1
else
    BACKEND="sf_bernina"
    ROOT_DIR="${PERSONAL_ROOT}/${USERNAME}"
    NEEDS_SCRATCH=0
    log "~/network_drives/home not reachable on this host - falling back to ${ROOT_DIR}"
fi
GNUPG_DIR="${ROOT_DIR}/gnupg"
GOPASS_HOME_DIR="${ROOT_DIR}/gopass-home"
STORE_DIR="${ROOT_DIR}/gopass-${STORE_ALIAS}"
ENV_FILE="${ROOT_DIR}/env.sh"

SCRATCH="$(mktemp -d)"
cleanup() { rm -rf "${SCRATCH}"; }
trap cleanup EXIT

echo "Setting up gopass access to the '${STORE_ALIAS}' store for ${EMAIL}"
echo "Backend: ${BACKEND}  (everything will live under ${ROOT_DIR})"
echo

# --- 1. target directory -----------------------------------------------------
log "Step 1/6: target directory (${ROOT_DIR})"
if [[ "${BACKEND}" == "sf_bernina" ]]; then
    "${BIN_DIR}/create_personal_dir"
fi
mkdir -p "${GOPASS_HOME_DIR}"
mkdir -m 700 -p "${GNUPG_DIR}"

# --- 2. SSH key + gitea reachability -----------------------------------------
log "Step 2/6: SSH key for gitea.psi.ch"
if [[ ! -f ~/.ssh/id_ed25519 && ! -f ~/.ssh/id_rsa ]]; then
    if ask "  No SSH key found. Generate one now (ssh-keygen -t ed25519)?"; then
        ssh-keygen -t ed25519 -C "${EMAIL}"
    else
        warn "  Can't continue without an SSH key - see gopass.md"
        exit 1
    fi
fi

if ssh -T -o BatchMode=yes -o ConnectTimeout=8 git@gitea.psi.ch </dev/null 2>&1 | grep -qi "successfully authenticated"; then
    log "  SSH to gitea.psi.ch: OK"
else
    warn "  SSH to gitea.psi.ch did not authenticate."
    warn "  Add your public key (~/.ssh/id_ed25519.pub or ~/.ssh/id_rsa.pub) at:"
    warn "    https://gitea.psi.ch -> Settings -> SSH/GPG Keys -> Add Key"
    warn "  Then re-run this script."
    exit 1
fi

# --- 3. GPG key: already in place / migrate from an old location / generate -
# NOTE: we detect "is there a usable key" by presence, not by matching
# ${EMAIL} - your real PSI email often isn't <username>@psi.ch (e.g.
# lemke_h's is henrik.lemke@psi.ch), so a literal-match check would miss
# an existing key and offer to generate a redundant, unauthorized one.
# Once a key is found/generated, EMAIL is corrected to that key's own
# uid email for every step from here on (gopass init, the final
# instructions, etc).
log "Step 3/6: GPG key"

key_email_in() {
    GNUPGHOME="$1" gpg --list-secret-keys --with-colons 2>/dev/null \
        | awk -F: '/^uid/{print $10; exit}' | sed -n 's/.*<\(.*\)>.*/\1/p'
}
has_secret_key_in() {
    GNUPGHOME="$1" gpg --list-secret-keys --with-colons 2>/dev/null | grep -q '^sec'
}

if has_secret_key_in "${GNUPG_DIR}"; then
    EMAIL="$(key_email_in "${GNUPG_DIR}")"
    log "  already present under ${GNUPG_DIR} (${EMAIL})"
else
    OLD_GNUPGHOME=""
    for candidate in "${GNUPGHOME:-}" "${HOME}/.gnupg" "${PERSONAL_ROOT}/${USERNAME}/gnupg"; do
        [[ -z "${candidate}" || "${candidate}" == "${GNUPG_DIR}" ]] && continue
        if has_secret_key_in "${candidate}"; then
            OLD_GNUPGHOME="${candidate}"
            break
        fi
    done
    if [[ -n "${OLD_GNUPGHOME}" ]]; then
        FOUND_EMAIL="$(key_email_in "${OLD_GNUPGHOME}")"
        log "  found an existing key (${FOUND_EMAIL}) under ${OLD_GNUPGHOME}"
        if ask "  Copy it to ${GNUPG_DIR} (keeps the SAME key/recipient - no re-authorization needed)?"; then
            copy_tree "${OLD_GNUPGHOME}" "${GNUPG_DIR}"
            EMAIL="${FOUND_EMAIL}"
            log "  copied - using ${EMAIL} as your identity from here on"
        else
            warn "  without this, decrypt calls will fail until a key exists under ${GNUPG_DIR}"
        fi
    else
        warn "  no GPG key found anywhere (checked ${GNUPG_DIR}, \$GNUPGHOME, ~/.gnupg, ${PERSONAL_ROOT}/${USERNAME}/gnupg)"
        if ask "  Generate one now (gpg --full-generate-key - use your REAL psi.ch email, set a real passphrase)?"; then
            if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
                # gpg --gen-key does chmods internally that fail directly on
                # network_drives - generate locally, then copy the result.
                mkdir -m 700 -p "${SCRATCH}/gnupg"
                GNUPGHOME="${SCRATCH}/gnupg" gpg --full-generate-key
                copy_tree "${SCRATCH}/gnupg" "${GNUPG_DIR}"
            else
                GNUPGHOME="${GNUPG_DIR}" gpg --full-generate-key
                # safety net in case this gpg version generated into the
                # default homedir despite the GNUPGHOME override above
                if ! has_secret_key_in "${GNUPG_DIR}" && has_secret_key_in "${HOME}/.gnupg"; then
                    copy_tree "${HOME}/.gnupg" "${GNUPG_DIR}"
                fi
            fi
            if has_secret_key_in "${GNUPG_DIR}"; then
                EMAIL="$(key_email_in "${GNUPG_DIR}")"
                log "  generated - using ${EMAIL} as your identity from here on"
            fi
        else
            warn "  Can't continue without a GPG key"
            exit 1
        fi
    fi
fi

if ! has_secret_key_in "${GNUPG_DIR}"; then
    warn "  no usable key under ${GNUPG_DIR} - can't continue. Re-run and"
    warn "  agree to the copy/generate step above, or place a key there by hand first."
    exit 1
fi
log "  using GPG identity: ${EMAIL}"
export GNUPGHOME="${GNUPG_DIR}"

# --- 4. root store -------------------------------------------------------------
log "Step 4/6: gopass root store"
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}"
if gopass mounts >/dev/null 2>&1; then
    log "  already initialized"
elif [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
    GNUPGHOME="${GNUPG_DIR}" GOPASS_HOMEDIR="${SCRATCH}/gopass-home" gopass init "${EMAIL}"
    (cd "${SCRATCH}/gopass-home/.local/share/gopass/stores/root" && git config core.fileMode false)
    copy_tree "${SCRATCH}/gopass-home" "${GOPASS_HOME_DIR}"
    sed -i "s|${SCRATCH}/gopass-home|${GOPASS_HOME_DIR}|g" "${GOPASS_HOME_DIR}/.config/gopass/config"
else
    gopass init "${EMAIL}"
fi

# --- 5. clone/mount the bernina store ----------------------------------------
log "Step 5/6: '${STORE_ALIAS}' store at ${STORE_DIR}"
if gopass mounts 2>/dev/null | grep -q "${STORE_ALIAS} ("; then
    log "  already mounted"
elif [[ -d "${STORE_DIR}/.git" ]]; then
    log "  directory already looks cloned, mounting it"
    gopass mounts add "${STORE_ALIAS}" "${STORE_DIR}"
elif [[ -d "${STORE_DIR}" ]]; then
    warn "  ${STORE_DIR} exists but has no .git - a previous attempt likely"
    warn "  failed partway. See gopass.md Troubleshooting before removing"
    warn "  it by hand (confirm it really lacks .git first)."
    exit 1
else
    # Plain `git clone` + `gopass mounts add`, deliberately NOT `gopass
    # clone`: confirmed by hand that `gopass clone` can hang indefinitely
    # (Go-level deadlock, wchan futex_do_wait, zero subprocesses/network
    # activity - not a slow/big-repo thing, reproduced even against the
    # tiny real bernina-secrets repo) at its post-clone "Gathering
    # information for the git repository" bookkeeping step. A plain git
    # clone plus `gopass mounts add` (the same call already used just
    # above when a store directory is pre-existing) reaches the same end
    # state without going through that codepath - confirmed instant
    # against the real repo.
    CLONE_DEST="${STORE_DIR}"
    if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
        CLONE_DEST="${SCRATCH}/${STORE_ALIAS}"
    fi
    CLONE_ERR="$(mktemp)"
    if ! git clone "${GITEA_REMOTE}" "${CLONE_DEST}" 2>"${CLONE_ERR}"; then
        cat "${CLONE_ERR}" >&2
        if grep -qi "not authorized\|access rights\|could not read from remote\|permission denied" "${CLONE_ERR}"; then
            warn "  Repo access not granted yet. Ask the admin (lemke_h) to add you"
            warn "  as a Collaborator (Write access) on Bernina/bernina-secrets:"
            warn "    https://gitea.psi.ch/Bernina/bernina-secrets/settings/collaboration"
            warn "  Then re-run this script."
        fi
        rm -f "${CLONE_ERR}"
        exit 1
    fi
    rm -f "${CLONE_ERR}"
    (cd "${CLONE_DEST}" && git config core.fileMode false)
    if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
        copy_tree "${CLONE_DEST}" "${STORE_DIR}"
    fi
    gopass mounts add "${STORE_ALIAS}" "${STORE_DIR}"
fi

# verify (don't skip this, a broken mount fails confusingly later)
if ! gopass mounts 2>/dev/null | grep -q "${STORE_ALIAS} ("; then
    warn "  mount verification failed - see gopass.md Troubleshooting"
    exit 1
fi
if [[ ! -d "${STORE_DIR}/.git" ]]; then
    warn "  ${STORE_DIR} has no .git - clone did not fully succeed"
    exit 1
fi
log "  mounted and verified OK"

# --- 6. env.sh + export pubkey + tell them what's left -----------------------
log "Step 6/6: writing ${ENV_FILE} and exporting your GPG public key"
if [[ "${BACKEND}" == "network_drives" ]]; then
    cat > "${ENV_FILE}" <<EOF
# Source this from ~/.bashrc (LOCAL to each host) to make your gopass
# access to '${STORE_ALIAS}' work the same on every host where ${ROOT_DIR}
# is reachable. Generated by gopass_bernina_setup - see gopass.md.
#
# ~/network_drives (SMB, "Partially Shared Home") is mounted per login
# session, asynchronously - on a freshly opened terminal/session it can
# take a moment to appear. Without this wait, the first gopass command of
# a new session can race the automount and silently fall back to an
# empty LOCAL store (this host's own \$HOME, not the shared one) - looks
# like "not a recipient" or "password-store is not initialized" for no
# obvious reason, and typically "fixes itself" on retry a moment later.
# Wait briefly (up to 5s) here instead, once per shell, so the first real
# command doesn't lose that race. See gopass.md Troubleshooting.
__gopass_wait_n=0
while [ ! -d "${GNUPG_DIR}" ] && [ "\${__gopass_wait_n}" -lt 20 ]; do
    sleep 0.25
    __gopass_wait_n=\$((__gopass_wait_n + 1))
done
unset __gopass_wait_n

export GNUPGHOME="${GNUPG_DIR}"
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}"
EOF
else
    cat > "${ENV_FILE}" <<EOF
# Source this from ~/.bashrc (LOCAL to each host) to make your gopass
# access to '${STORE_ALIAS}' work the same on every host where ${ROOT_DIR}
# is reachable. Generated by gopass_bernina_setup - see gopass.md.
export GNUPGHOME="${GNUPG_DIR}"
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}"
EOF
fi

PUBKEY_FILE="${ROOT_DIR}/${USERNAME}-gopass-pubkey.asc"
gpg --armor --export "${EMAIL}" > "${PUBKEY_FILE}"
log "  wrote ${PUBKEY_FILE}"

# is this identity already an authorized recipient? (true for anyone
# re-running this script after migrating an already-authorized key to a
# new location - e.g. the admin's own re-run - not just for brand-new
# people)
ALREADY_RECIPIENT=0
if gopass recipients 2>/dev/null | grep -qi "<${EMAIL}>"; then
    ALREADY_RECIPIENT=1
fi

echo
echo "======================================================================"
# ~/.bashrc itself may be force-copied (not symlinked) from this file on
# every login, with no diff check, per PSI's Partially Shared Home
# 'copy:' mechanism (~/network_drives/home/linux_home_links.yaml) -
# confirmed 2026-09-18. If so, an edit to ~/.bashrc directly would be
# silently lost at the next login, so point at the real source instead.
BASHRC_TEMPLATE="${HOME}/network_drives/home/linux_config/bashrc"
if [[ -f "${BASHRC_TEMPLATE}" ]]; then
    echo "Add ONE line to ${BASHRC_TEMPLATE} - NOT to ~/.bashrc"
    echo "directly: your ~/.bashrc here gets force-copied from that file on"
    echo "every login with no diff check (see the 'copy:' section of"
    echo "~/network_drives/home/linux_home_links.yaml), so a direct edit to"
    echo "~/.bashrc would silently vanish at your next login."
else
    echo "Add ONE line to this host's LOCAL ~/.bashrc (not shared - do this once"
    echo "per host, or replace any older GNUPGHOME/GOPASS_HOMEDIR exports there)."
fi
echo "Put it NEAR THE TOP - before any '[are we interactive?] || return'"
echo "guard - so it also reaches non-interactive shells (VS Code Remote-SSH,"
echo "cron, some Jupyter kernels), which would otherwise silently fall back"
echo "to this host's own empty local gopass store instead of erroring:"
echo
echo "  source ${ENV_FILE}"
echo
if [[ "${ALREADY_RECIPIENT}" -eq 1 ]]; then
    echo "You're already an authorized recipient of '${STORE_ALIAS}' (${EMAIL}) -"
    echo "nothing else to do. Open a fresh shell (so the line above takes"
    echo "effect), then 'gopass show ${STORE_ALIAS}/...' should work (it'll"
    echo "ask for your GPG passphrase once, then cache it)."
else
    echo "One step only an existing recipient can do for you:"
    echo
    echo "  Send ${PUBKEY_FILE} to the store admin (lemke_h) - internal email,"
    echo "  Mattermost/Slack DM, or in person. They will run:"
    echo
    echo "    gpg --import ${USERNAME}-gopass-pubkey.asc"
    echo "    gopass recipients add --store ${STORE_ALIAS} ${EMAIL}"
    echo
    echo "Until then, 'gopass show ${STORE_ALIAS}/...' will fail with a"
    echo "'not a recipient' error - that's expected, not a bug (see gopass.md)."
fi
echo "======================================================================"
