#!/bin/bash
# services/vpn-data-mount.sh — mount existing SMB shares from a
# NetBird-connected home box, with SSH-key bootstrap automated.
# Part of the modular post-install system (sourced by setup.sh).
#
# Can also be run standalone on any machine:
#   sudo bash vpn-data-mount.sh
# (No Docker needed — this only touches SSH and /etc/fstab)
#
# Unlike most services here, this is repeatable by design: different
# services can have data on different home boxes, so this asks for a home
# box IP every time and can be re-run any number of times, once per
# home-box you want to pull shares from. It's the multi-instance pattern
# from CLAUDE.md generalized from "N instances of one app" to "N
# independent mounts" — there's no single install directory to gate on, so
# state lives in /etc/fstab itself (tagged entries), same as
# tools/mount-network-drive.sh.
#
# Deliberately READ-ONLY on the home box's Samba config — this tool never
# writes to smb.conf, never installs Samba, never creates or resets a
# Samba account there. It only (a) bootstraps passwordless SSH if needed,
# then (b) reads the home box's existing smb.conf over that SSH connection
# to list whatever shares are already configured there, so you can pick
# one or more to mount. Set up the actual share(s) on the home box
# yourself, the normal way (or with tools/mount-network-drive.sh's own
# guided flow, run there). An earlier version of this tried to fully
# provision Samba remotely too — reversed per direct request, and it had
# also caused real damage in practice (a section-removal bug that deleted
# unrelated shares on a real box) that a read-only tool can't repeat.
#
# Assumes the home box is Linux and reachable over a NetBird IP — this repo
# doesn't set up the home box's side of NetBird (that's a separate machine,
# possibly not running this repo at all).
#
# SMB chosen over NFS/SSHFS deliberately: NFS is marginally faster for
# Linux-to-Linux but SMB isn't a "huge" difference for normal use (media,
# docs, moderate datasets — the gap shows up mainly on many-small-files
# workloads). SSHFS was ruled out because the VPN tunnel already encrypts
# everything — SSHFS's own SSH-layer encryption on top of that is pure
# redundant overhead for no added security, and it's the slowest and least
# robust (FUSE reconnect quirks) of the three for an always-on mount.
#
# Mounts use real Samba credentials (a username/password you provide for
# an account that already exists on the home box), stored locally in a
# root-only credentials file, same convention tools/mount-network-drive.sh
# already uses — never guest access.
#
# "mount error(79): Can not access a needed shared library" is NOT a
# credentials problem, guest-vs-authenticated problem, or a mislabeled
# ENOKEY — errno 79 is literally ELIBACC, and mount.cifs prints glibc's
# literal strerror() text for it. Confirmed live: this recurred identically
# with a real Samba account and a verified, correctly-captured password
# (see the read()/IFS note above — that was a real bug too, just not this
# one), and again after keyutils was already installed — so it's not that
# either, at least not on every host. Two independent real causes share
# this exact errno/message, both fixed defensively below:
#   1. `keyutils` missing on the client. cifs-utils hard-depends on the
#      libkeyutils1 *library* but only Recommends the keyutils *package*
#      (/sbin/request-key + /etc/request-key.d/*.conf, what the kernel's
#      upcall actually invokes) — minimal cloud images that disable
#      install-recommends silently skip it.
#   2. The hardcoded `iocharset=utf8` mount option needs the kernel's
#      nls_utf8 module. Confirmed live on a stock Ubuntu 6.8.0-137-generic
#      VPS kernel: `modprobe nls_utf8` → "FATAL: Module nls_utf8 not found"
#      — not loadable, not built in, just absent from that kernel build.
#      Every mount asking for that codepage fails with errno 79 regardless
#      of credentials. `_vdm_mount_local` probes for it with a harmless
#      `modprobe` and only adds `iocharset=utf8` if it actually loads;
#      otherwise it warns and mounts without it (kernel falls back to its
#      build's nls_default — fine for ASCII-heavy filenames, the common
#      case for a home-data share; non-ASCII filenames may not round-trip
#      perfectly on a kernel missing this module, which is a kernel
#      limitation this script can't paper over further).
#
# Optional: a gocryptfs decrypt layer on top of the plain CIFS mount, for
# when "available to the VPS" and "opaque to the VPS's operator/anyone with
# disk access to it" both matter. Set up the encrypted store on the home
# box first with tools/gocryptfs-setup-home.sh (that tool's header explains
# the actual threat model this buys you and, just as importantly, doesn't).
# The short version: the home box encrypts before anything ever crosses
# SMB, so the VPS's CIFS mount only ever holds ciphertext; this script's
# decrypt layer then fetches the passphrase fresh over the same SSH trust
# already used for share discovery — piped straight into gocryptfs, never
# written to this VPS's own disk — and mounts a decrypted view alongside
# the raw ciphertext mount. Whatever's actively reading through that
# decrypted view still sees plaintext live, same as any mount anywhere;
# nothing changes that. What changes is everything else: a snapshot,
# backup, or disk-level look at this VPS while the passphrase isn't
# actively loaded shows only ciphertext.

# ── Standalone bootstrap ──────────────────────────────────────────────────────
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    [[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }

    _SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    _COMMON="$_SELF_DIR/../lib/common.sh"

    if [[ -f "$_COMMON" ]]; then
        # shellcheck source=../lib/common.sh
        source "$_COMMON"
    else
        log_info()    { echo -e "\033[0;34m[INFO]\033[0m $*"; }
        log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
        log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
        log_error()   { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }

        prompt_text() {
            local _q="$1" _def="$2" _var="$3" _r
            [[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
            read -r -p "  $_q " _r
            eval "$_var='${_r:-$_def}'"
        }

        prompt_yn() {
            local _q="$1" _def="$2" _var="$3" _r
            [[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
            read -r -p "  $_q " _r
            eval "$_var='${_r:-$_def}'"
        }

        register_service() { :; }
    fi

    ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
    ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
    DRY_RUN="${DRY_RUN:-false}"
    UNATTENDED="${UNATTENDED:-false}"

    _RUN_STANDALONE=1
fi
# ─────────────────────────────────────────────────────────────────────────────

register_service vpn-data-mount homelab "Mount existing SMB shares from a NetBird-connected home box (read-only discovery, SSH-automated key setup)"

# ── fstab tagging — the durable record of what this tool has set up ────────
# Same philosophy as tools/mount-network-drive.sh: /etc/fstab is the single
# source of truth, no separate state file to drift out of sync with it.
_VDM_TAG_PREFIX="# vpn-data-mount:"

_vdm_list_existing() {
    local entries
    entries="$(grep "^${_VDM_TAG_PREFIX}" /etc/fstab 2>/dev/null || true)"
    if [ -n "$entries" ]; then
        echo ""
        log_info "Already-configured VPN data mounts:"
        echo "$entries" | sed "s|^${_VDM_TAG_PREFIX}|  •|"
        echo ""
    fi

    local units
    units="$(systemctl list-unit-files 'vpn-data-mount-decrypt-*.service' --no-legend 2>/dev/null | awk '{print $1}')"
    if [ -n "$units" ]; then
        log_info "Decrypt layers configured:"
        echo "$units" | sed 's/^/  • /'
        echo ""
    fi
}

# ── Name a raw IP so it can be used everywhere instead of typing it again ──
# Deliberately /etc/hosts, not ~/.ssh/config: an SSH Host alias only helps
# the `ssh` command itself resolve a name — mount.cifs (and everything
# else) never consults ~/.ssh/config at all, so an alias alone wouldn't let
# the actual CIFS mount address use a name. /etc/hosts is the one mechanism
# that makes a name resolve for both, which is what "use a name instead of
# the IP" actually needs end to end.
# Sets RESOLVED_HOST (not local — same out-param convention as elsewhere).
_vdm_resolve_host() {
    local input="$1"
    RESOLVED_HOST="$input"

    # Not a raw IP (already a name, whether from /etc/hosts, real DNS, or
    # just typed that way) — nothing to do.
    [[ "$input" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 0

    # Already named from an earlier vpn-data-mount run against this same IP?
    local existing
    existing="$(awk -v ip="$input" '$1==ip && /# vpn-data-mount/ {print $2; exit}' /etc/hosts 2>/dev/null)"
    if [ -n "$existing" ]; then
        log_info "Already named '$existing' in /etc/hosts from an earlier mount — using that."
        RESOLVED_HOST="$existing"
        return 0
    fi

    local NAME=""
    prompt_text "  Name this home box (blank to keep using the IP):" "" NAME
    [ -z "$NAME" ] && return 0
    NAME="$(echo "$NAME" | tr -cs 'a-zA-Z0-9-' '-' | sed 's/^-*//;s/-*$//')"
    [ -z "$NAME" ] && return 0

    if grep -qE "^\S+[[:space:]]+${NAME}([[:space:]]|\$)" /etc/hosts 2>/dev/null; then
        log_warning "'$NAME' is already used for a different address in /etc/hosts — keeping the IP instead."
        return 0
    fi

    echo "${input}    ${NAME}    # vpn-data-mount" >> /etc/hosts
    log_success "Added to /etc/hosts: $NAME -> $input (works for SSH, this mount, and anything else on this box)"
    RESOLVED_HOST="$NAME"
}

# ── SSH trust: test first, only bootstrap if actually needed ──────────────
# Covers "the home box and VPS already share a key via GitHub import (or any
# other means)" for free — if it already works, nothing below runs at all.
_vdm_ssh_works() {
    local user="$1" host="$2"
    # Runs as $ACTUAL_USER, not root (this whole script runs as root) — the
    # SSH key lives in $ACTUAL_HOME/.ssh, so root's own bare `ssh` would look
    # in the wrong home directory entirely and never find it.
    sudo -u "$ACTUAL_USER" ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
        "${user}@${host}" true 2>/dev/null
}

_vdm_ensure_ssh_trust() {
    local user="$1" host="$2"

    if _vdm_ssh_works "$user" "$host"; then
        log_success "Passwordless SSH to ${user}@${host} already works — nothing to set up."
        return 0
    fi

    log_info "No passwordless SSH to ${user}@${host} yet — setting it up."

    local keyfile="$ACTUAL_HOME/.ssh/id_ed25519"
    if [ ! -f "$keyfile" ]; then
        log_info "No SSH key found at $keyfile — generating one."
        sudo -u "$ACTUAL_USER" mkdir -p "$ACTUAL_HOME/.ssh"
        sudo -u "$ACTUAL_USER" ssh-keygen -t ed25519 -N "" -f "$keyfile" -C "${ACTUAL_USER}@$(hostname)-vpn-data-mount" \
            || { log_error "Key generation failed."; return 1; }
        chmod 700 "$ACTUAL_HOME/.ssh"
        chmod 600 "$keyfile"
        chmod 644 "${keyfile}.pub"
    fi

    echo ""
    echo "  This box's public key (needs to end up in ${user}'s authorized_keys"
    echo "  on the home box, one way or another):"
    echo ""
    sed 's/^/    /' "${keyfile}.pub"
    echo ""

    while true; do
        echo "  How do you want to get it there?"
        echo "    1) Try now with ssh-copy-id (needs password login enabled on the home box)"
        echo "    2) I'll add it myself — paste it into ~/.ssh/authorized_keys there, or add it"
        echo "       to your GitHub account and run 'ssh-import-id gh:<user>' on the home box"
        echo "       (same mechanism this repo's own base.sh setup uses)"
        echo "    3) Cancel this mount"
        echo ""
        local CHOICE=""
        prompt_text "  Choice [1/2/3]:" "1" CHOICE
        case "$CHOICE" in
            1)
                sudo -u "$ACTUAL_USER" ssh-copy-id -i "${keyfile}.pub" "${user}@${host}" \
                    || log_warning "ssh-copy-id failed — password auth may be disabled on the home box. Try option 2."
                ;;
            2)
                echo ""
                read -r -p "  Press Enter once the key is in place on the home box: " _
                ;;
            3|c|C)
                log_info "Cancelled."
                return 1
                ;;
            *)
                log_warning "Invalid choice."
                continue
                ;;
        esac

        if _vdm_ssh_works "$user" "$host"; then
            log_success "Passwordless SSH to ${user}@${host} confirmed."
            return 0
        fi
        log_warning "Still can't connect without a password — try again, or cancel."
    done
}

# ── Read-only share discovery ───────────────────────────────────────────────
# Prints "share_name|path" one per line for every real data share found in
# the home box's smb.conf (skips [global]/[homes]/[printers]/[print$] —
# not actual browsable directories). Never writes anything, on either
# side — see the file header. Tries a plain read first (smb.conf is
# world-readable on a stock Samba install); falls back to a sudo'd read
# only if that comes back empty, still read-only either way.
_vdm_list_remote_shares() {
    local user="$1" host="$2"
    local conf
    conf="$(sudo -u "$ACTUAL_USER" ssh "${user}@${host}" 'cat /etc/samba/smb.conf 2>/dev/null')"
    if [ -z "$conf" ]; then
        conf="$(sudo -u "$ACTUAL_USER" ssh -t "${user}@${host}" 'sudo cat /etc/samba/smb.conf 2>/dev/null' 2>/dev/null)"
    fi
    [ -z "$conf" ] && return 1

    echo "$conf" | awk '
        function flush() {
            if (sect != "" && path != "" && sect != "global" && sect != "printers" && sect != "print$" && sect != "homes") {
                print sect "|" path
            }
        }
        /^\[/ {
            flush()
            sect = $0
            gsub(/[][]/, "", sect)
            path = ""
            next
        }
        /^[[:space:]]*path[[:space:]]*=/ {
            path = $0
            sub(/^[[:space:]]*path[[:space:]]*=[[:space:]]*/, "", path)
            gsub(/[[:space:]]+$/, "", path)
        }
        END { flush() }
    '
}

# ── Reuse a password already entered for the same user+host ───────────────
# Prints the password if an earlier mount from this host used the same
# Samba username, nothing otherwise. No log_* calls in here — this runs
# inside a caller's $(...) capture, and log_info/log_warning/etc. all
# write to stdout, which would corrupt it.
_vdm_find_existing_smb_password() {
    local host="$1" user="$2" label creds_file found_user found_pass
    while IFS= read -r label; do
        [ -z "$label" ] && continue
        creds_file="/etc/samba/credentials.vpn-data-mount-${label}"
        [ -f "$creds_file" ] || continue
        found_user="$(grep '^username=' "$creds_file" | cut -d= -f2-)"
        [ "$found_user" = "$user" ] || continue
        found_pass="$(grep '^password=' "$creds_file" | cut -d= -f2-)"
        [ -n "$found_pass" ] && { printf '%s' "$found_pass"; return 0; }
    done < <(grep -E "^${_VDM_TAG_PREFIX} [^ ]+ — ${host}:" /etc/fstab 2>/dev/null \
        | sed -E "s/^${_VDM_TAG_PREFIX} ([^ ]+) .*/\1/")
    return 1
}

# ── Already mounted? Find its label+mount point instead of re-prompting ───
# Prints "label|mount_point" if this exact host+share is already tagged in
# /etc/fstab, nothing otherwise. No log_* calls — same $(...) capture
# reason as _vdm_find_existing_smb_password above.
_vdm_find_existing_mount() {
    local host="$1" share_name="$2"
    grep -E "^${_VDM_TAG_PREFIX} [^ ]+ — ${host}:${share_name} -> " /etc/fstab 2>/dev/null \
        | sed -E "s/^${_VDM_TAG_PREFIX} ([^ ]+) — [^ ]+ -> (.*)\$/\1|\2/" \
        | head -1
}

# ── Tear down an existing mount so it can be redone from scratch ──────────
# Decrypt layer first (it sits on top of the CIFS mount — systemctl stop
# runs the unit's own ExecStop, which unmounts it) if one exists for this
# label, then the CIFS mount, its credentials file, and its /etc/fstab
# tag+entry (backed up first, same as every other /etc/fstab write in this
# file — a fixed ",+1d" range: the tag line plus exactly the one mount line
# that always immediately follows it, never an open-ended range to the next
# blank line or EOF; see the file's own history for why that distinction
# matters).
_vdm_remove_mount() {
    local label="$1" mount_point="$2"
    local unit="vpn-data-mount-decrypt-${label}.service"

    if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q .; then
        systemctl disable --now "$unit" >/dev/null 2>&1
        rm -f "/etc/systemd/system/${unit}" "/usr/local/sbin/vpn-data-mount-decrypt-${label}.sh"
        systemctl daemon-reload
    fi

    umount "$mount_point" 2>/dev/null || true
    rmdir "$mount_point" 2>/dev/null || true
    rm -f "/etc/samba/credentials.vpn-data-mount-${label}"

    local bk="/etc/fstab.backup.$(date +%Y%m%d-%H%M%S)"
    cp /etc/fstab "$bk"
    sed -i "/^${_VDM_TAG_PREFIX} ${label} — /,+1d" /etc/fstab
    log_success "Removed the existing mount for '$label' (fstab backup: $(basename "$bk"))"
}

# ── Every configured mount, for the removal menu below ─────────────────────
# Prints "label|host:share|mount_point" one per line. Unlike
# _vdm_find_existing_mount, not scoped to a particular host+share.
_vdm_list_all_mounts() {
    grep -E "^${_VDM_TAG_PREFIX} " /etc/fstab 2>/dev/null \
        | sed -E "s/^${_VDM_TAG_PREFIX} ([^ ]+) — ([^ ]+) -> (.*)\$/\1|\2|\3/"
}

# ── Pick one or more configured mounts by number and remove them ──────────
_vdm_remove_mount_interactive() {
    local entries=()
    while IFS= read -r line; do
        [ -z "$line" ] && continue
        entries+=("$line")
    done < <(_vdm_list_all_mounts)

    if [ "${#entries[@]}" -eq 0 ]; then
        log_info "No vpn-data-mount mounts configured — nothing to remove."
        return 0
    fi

    while true; do
        echo ""
        echo "  Configured mounts:"
        local i label hostshare point
        for i in "${!entries[@]}"; do
            IFS='|' read -r label hostshare point <<< "${entries[$i]}"
            printf "    %d) %-15s %-28s -> %s\n" "$((i + 1))" "$label" "$hostshare" "$point"
        done
        echo ""
        local CHOICE=""
        prompt_text "  Remove which one? (number, or blank to stop):" "" CHOICE
        [ -z "$CHOICE" ] && break

        if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 1 ] || [ "$CHOICE" -gt "${#entries[@]}" ]; then
            log_warning "'$CHOICE' isn't one of the listed mounts."
            continue
        fi

        IFS='|' read -r label hostshare point <<< "${entries[$((CHOICE - 1))]}"
        local CONFIRM=""
        prompt_yn "  Remove '$label' ($point)? This unmounts it, removes its credentials, and removes it from /etc/fstab (backed up first). (y/n):" "n" CONFIRM
        if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
            _vdm_remove_mount "$label" "$point"
            unset "entries[$((CHOICE - 1))]"
            entries=("${entries[@]}")
        fi

        [ "${#entries[@]}" -eq 0 ] && break
        local AGAIN=""
        prompt_yn "  Remove another? (y/n):" "n" AGAIN
        [[ "$AGAIN" =~ ^[Yy]$ ]] || break
    done
}

# ── Prompt for a password twice, hidden, matching ──────────────────────────
# Prints the password on success. No log_* calls — same reason as above;
# uses plain stderr output instead so it's visible without corrupting a
# caller's $(...) capture.
_vdm_prompt_password() {
    local prompt="$1" pw1="" pw2=""
    while true; do
        # IFS= matters here, not just -s/-r: plain `read -r pw1` (no IFS=)
        # silently strips leading/trailing whitespace even into a single
        # variable — confirmed live, a password with a leading/trailing
        # space (copy-pasted from a password manager, a stray keystroke)
        # got quietly trimmed on the way in, so the credentials file ended
        # up holding a DIFFERENT password than the one actually set on the
        # Samba account. That surfaces as a cryptic mount failure, not an
        # obvious "wrong password" — nothing here could tell the two apart.
        echo -n "  ${prompt}: " >&2
        IFS= read -r -s pw1; echo "" >&2
        echo -n "  Confirm: " >&2
        IFS= read -r -s pw2; echo "" >&2
        if [ -n "$pw1" ] && [ "$pw1" = "$pw2" ]; then
            # Length only, never the password itself — lets you catch a
            # silently-stripped character (or a typo) yourself before the
            # mount attempt fails with an unhelpful error.
            echo "  Captured (${#pw1} characters)." >&2
            printf '%s' "$pw1"
            return 0
        fi
        echo "  Passwords didn't match or were empty — try again." >&2
    done
}

# ── Local mount + fstab ─────────────────────────────────────────────────────
_vdm_mount_local() {
    local host="$1" share_name="$2" mount_point="$3" label="$4" smb_user="$5" smb_pass="$6"

    command -v mount.cifs >/dev/null 2>&1 || apt-get install -y cifs-utils -qq
    # Explicit, not left to cifs-utils' Recommends — see file header on
    # errno 79/ELIBACC. dpkg -s (not command -v: keyutils ships no binary
    # this script calls directly, just the request-key handler files).
    dpkg -s keyutils >/dev/null 2>&1 || apt-get install -y keyutils -qq

    mkdir -p "$mount_point"

    # Credentials file, not guest/inline password — root-only, matching
    # tools/mount-network-drive.sh's existing convention for SMB creds.
    local creds_file="/etc/samba/credentials.vpn-data-mount-${label}"
    mkdir -p /etc/samba
    cat > "$creds_file" << CREDS
username=${smb_user}
password=${smb_pass}
CREDS
    chmod 600 "$creds_file"
    chown root:root "$creds_file"

    # sec=ntlmssp explicitly — see the file header on errno 79.
    local opts="credentials=${creds_file},sec=ntlmssp,uid=$(id -u "$ACTUAL_USER"),gid=$(id -g "$ACTUAL_USER"),nofail,_netdev"

    # iocharset=utf8 only if the kernel can actually load nls_utf8 — see
    # the file header. modprobe on an already-loaded/built-in module is a
    # harmless no-op, so this is safe to call unconditionally.
    if modprobe nls_utf8 >/dev/null 2>&1; then
        opts="${opts},iocharset=utf8"
    else
        log_warning "This kernel ($(uname -r)) has no nls_utf8 module — mounting without iocharset=utf8. Non-ASCII filenames may not display correctly; try 'sudo apt-get install --reinstall linux-modules-$(uname -r)' to see if it restores the module."
    fi

    local share="//${host}/${share_name}"

    log_info "Testing mount..."
    if mount -t cifs -o "$opts" "$share" "$mount_point"; then
        log_success "Mounted at $mount_point"
    else
        log_error "Mount failed for [$share_name] — check the username/password and that the home box's share actually allows this account."
        rmdir "$mount_point" 2>/dev/null || true
        rm -f "$creds_file"
        return 1
    fi

    if grep -qs "$mount_point" /etc/fstab; then
        log_warning "$mount_point already in /etc/fstab — skipping fstab entry."
        return 0
    fi
    local bk="/etc/fstab.backup.$(date +%Y%m%d-%H%M%S)"
    cp /etc/fstab "$bk"
    {
        echo ""
        echo "${_VDM_TAG_PREFIX} ${label} — ${host}:${share_name} -> ${mount_point}"
        printf '%-40s %-25s %-6s %s 0 0\n' "$share" "$mount_point" "cifs" "$opts"
    } >> /etc/fstab
    log_success "Added to /etc/fstab (backup: $(basename "$bk"))"
}

# ── Optional gocryptfs decrypt layer on top of an already-mounted share ────
# See the file header for the threat model. Fully opt-in, per-share, off by
# default — a plain unencrypted share works exactly as before.
_vdm_ensure_gocryptfs() {
    command -v gocryptfs >/dev/null 2>&1 || apt-get install -y gocryptfs -qq

    # -allow_other needs this uncommented for a non-root mount; harmless
    # (and unnecessary, since we always mount as root below) otherwise —
    # cheap to guarantee rather than leave as a silent gotcha if that ever
    # changes.
    local conf="/etc/fuse.conf"
    [ -f "$conf" ] || touch "$conf"
    if grep -q '^#user_allow_other' "$conf"; then
        sed -i 's/^#user_allow_other/user_allow_other/' "$conf"
    elif ! grep -q '^user_allow_other' "$conf"; then
        echo "user_allow_other" >> "$conf"
    fi
}

# Generates /etc/systemd/system/vpn-data-mount-decrypt-<label>.service so the
# decrypt layer comes back after a reboot the same way the fstab CIFS mount
# does. RequiresMountsFor sequences it after that mount (whether it's an
# fstab-generated mount unit or comes up some other way) instead of racing
# it. The passphrase fetch happens fresh on every start — nothing from it
# persists in the unit file itself or anywhere else on this disk.
_vdm_write_decrypt_unit() {
    local host="$1" ssh_user="$2" label="$3" passfile="$4" cifs_mount_point="$5" decrypted_point="$6"
    local wrapper="/usr/local/sbin/vpn-data-mount-decrypt-${label}.sh"
    local unit="/etc/systemd/system/vpn-data-mount-decrypt-${label}.service"

    # A wrapper script, not one long quoted ExecStart= one-liner — a single
    # ExecStart string embedding an SSH-piped-to-gocryptfs pipeline needs
    # two independent layers of quoting (systemd's own ExecStart= word
    # splitting, then bash -c's), which is exactly the kind of thing that
    # looks right and silently breaks on the one path/host with a space or
    # an unusual character in it. A plain script just uses normal bash
    # quoting once.
    cat > "$wrapper" << WRAPPER
#!/bin/bash
set -e
sudo -u "${ACTUAL_USER}" ssh -o BatchMode=yes -o ConnectTimeout=10 "${ssh_user}@${host}" "cat '${passfile}'" \\
    | gocryptfs -passfile /dev/stdin -allow_other "${cifs_mount_point}" "${decrypted_point}"
WRAPPER
    chmod 700 "$wrapper"
    chown root:root "$wrapper"

    cat > "$unit" << UNIT
[Unit]
Description=vpn-data-mount gocryptfs decrypt layer: ${label}
After=network-online.target
Wants=network-online.target
RequiresMountsFor=${cifs_mount_point}

[Service]
Type=forking
ExecStart=${wrapper}
ExecStop=/bin/fusermount -u ${decrypted_point}
Restart=on-failure
RestartSec=15
User=root

[Install]
WantedBy=multi-user.target
UNIT

    systemctl daemon-reload
    if systemctl enable --now "vpn-data-mount-decrypt-${label}.service" >/dev/null 2>&1; then
        log_success "Decrypt layer will remount automatically on boot (systemd unit: vpn-data-mount-decrypt-${label})"
    else
        log_warning "Couldn't enable the boot-time systemd unit — you'll need to re-run the decrypt step manually after a reboot."
    fi
}

_vdm_setup_decrypt_layer() {
    local host="$1" ssh_user="$2" label="$3" cifs_mount_point="$4"

    echo ""
    local WANT=""
    prompt_yn "  Is [$label] a gocryptfs-encrypted share (set up on the home box with tools/gocryptfs-setup-home.sh)? Layer decryption on top? (y/n):" "n" WANT
    [[ "$WANT" =~ ^[Yy]$ ]] || return 0

    _vdm_ensure_gocryptfs

    local passfile=""
    prompt_text "  Path to the passphrase file on the home box (printed at the end of gocryptfs-setup-home.sh):" "" passfile
    if [ -z "$passfile" ]; then
        log_warning "No path given — skipping the decrypt layer for [$label]."
        return 0
    fi

    if ! sudo -u "$ACTUAL_USER" ssh -o BatchMode=yes -o ConnectTimeout=5 "${ssh_user}@${host}" "test -f '$passfile'" 2>/dev/null; then
        log_warning "Can't see $passfile on ${ssh_user}@${host} — skipping the decrypt layer for [$label]. Run tools/gocryptfs-setup-home.sh there first, or check the path."
        return 0
    fi

    local decrypted_point=""
    prompt_text "  Local mount point for the decrypted view:" "${cifs_mount_point}-decrypted" decrypted_point
    mkdir -p "$decrypted_point"

    if mountpoint -q "$decrypted_point" 2>/dev/null; then
        log_info "$decrypted_point is already mounted — leaving it as-is, just (re)writing the boot-time unit."
    else
        if ! sudo -u "$ACTUAL_USER" ssh -o BatchMode=yes "${ssh_user}@${host}" "cat '$passfile'" \
            | gocryptfs -passfile /dev/stdin -allow_other "$cifs_mount_point" "$decrypted_point"; then
            log_error "gocryptfs mount failed for [$label] — check the passphrase file on the home box and that $cifs_mount_point actually holds a gocryptfs store (gocryptfs.conf present at its root)."
            rmdir "$decrypted_point" 2>/dev/null || true
            return 1
        fi
        log_success "Decrypted view mounted at $decrypted_point"
    fi

    _vdm_write_decrypt_unit "$host" "$ssh_user" "$label" "$passfile" "$cifs_mount_point" "$decrypted_point"

    # Point chained callers (filebrowser.sh etc.) at the decrypted view
    # instead of the raw ciphertext mount, same out-param as elsewhere.
    VDM_LAST_MOUNT_POINT="$decrypted_point"
}

# ── Parse a selection like "1", "1,3", "1-3", "1 3 5" into 1-based indices ──
# Prints one index per line. Silently drops anything that doesn't look like
# a number or a range — the caller validates indices against the actual
# list length.
_vdm_parse_selection() {
    local input="$1" token start end i
    for token in $(echo "$input" | tr ',' ' '); do
        if [[ "$token" =~ ^([0-9]+)-([0-9]+)$ ]]; then
            start="${BASH_REMATCH[1]}"; end="${BASH_REMATCH[2]}"
            for ((i = start; i <= end; i++)); do echo "$i"; done
        elif [[ "$token" =~ ^[0-9]+$ ]]; then
            echo "$token"
        fi
    done
}

# ── One home box, one or more shares from it ────────────────────────────────
_vdm_add_mount() {
    echo ""
    local HOST_INPUT="" HOST="" SSH_USER=""
    prompt_text "  Home box's NetBird IP or an already-named host (check 'netbird status' on that box):" "" HOST_INPUT
    if [ -z "$HOST_INPUT" ]; then
        log_warning "No host entered — cancelling."
        return 1
    fi
    _vdm_resolve_host "$HOST_INPUT"
    HOST="$RESOLVED_HOST"

    prompt_text "  SSH username on the home box:" "$ACTUAL_USER" SSH_USER

    _vdm_ensure_ssh_trust "$SSH_USER" "$HOST" || return 1

    # Pure convenience on top of the /etc/hosts naming above (which is what
    # actually makes the mount itself usable by name) — an SSH Host alias
    # additionally skips typing the username for interactive `ssh` use.
    if [[ ! "$HOST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] \
        && declare -F add_ssh_host_alias >/dev/null 2>&1 \
        && declare -F ssh_host_alias_exists >/dev/null 2>&1 \
        && ! ssh_host_alias_exists "$HOST"; then
        local ADD_ALIAS=""
        prompt_yn "  Also add '$HOST' as an SSH alias (ssh $HOST, no username needed)? (y/n):" "y" ADD_ALIAS
        [[ "$ADD_ALIAS" =~ ^[Yy]$ ]] && add_ssh_host_alias "$HOST" "$HOST_INPUT" "$SSH_USER" "22"
    fi

    log_info "Reading Samba shares already configured on $HOST (read-only)..."
    local shares_raw
    shares_raw="$(_vdm_list_remote_shares "$SSH_USER" "$HOST")"
    if [ -z "$shares_raw" ]; then
        log_warning "No Samba shares found on $HOST (or /etc/samba/smb.conf couldn't be read). Set up a share there first, the normal way, then re-run this."
        return 1
    fi

    local share_names=() share_paths=()
    while IFS='|' read -r sname spath; do
        [ -z "$sname" ] && continue
        share_names+=("$sname")
        share_paths+=("$spath")
    done <<< "$shares_raw"

    echo ""
    echo "  Samba shares found on $HOST:"
    local i
    for i in "${!share_names[@]}"; do
        printf "    %d) %-20s %s\n" "$((i + 1))" "${share_names[$i]}" "${share_paths[$i]}"
    done
    echo ""
    local SELECTION=""
    prompt_text "  Which one(s)? e.g. '1' or '1,3' or '1-3' or '1 3 5':" "" SELECTION
    if [ -z "$SELECTION" ]; then
        log_warning "Nothing selected — cancelling."
        return 1
    fi

    local indices=()
    while IFS= read -r i; do indices+=("$i"); done < <(_vdm_parse_selection "$SELECTION")
    if [ "${#indices[@]}" -eq 0 ]; then
        log_warning "Couldn't parse a selection from '$SELECTION' — cancelling."
        return 1
    fi

    # Credentials asked once, reused for every share picked here — the
    # common case is one personal account with access to several shares.
    # Re-run this for a share needing a different account.
    local SMB_USER=""
    prompt_text "  Samba username to connect with:" "$SSH_USER" SMB_USER
    local SMB_PASS=""
    SMB_PASS="$(_vdm_find_existing_smb_password "$HOST" "$SMB_USER")"
    if [ -n "$SMB_PASS" ]; then
        log_info "Reusing the Samba password already saved for '$SMB_USER' on $HOST from an earlier mount."
    else
        SMB_PASS="$(_vdm_prompt_password "Samba password for '$SMB_USER'")"
    fi

    local picked_any=false idx arr_i this_share this_path LABEL MOUNT_POINT
    for idx in "${indices[@]}"; do
        arr_i=$((idx - 1))
        if [ "$arr_i" -lt 0 ] || [ "$arr_i" -ge "${#share_names[@]}" ]; then
            log_warning "$idx isn't one of the listed shares — skipping."
            continue
        fi
        this_share="${share_names[$arr_i]}"
        this_path="${share_paths[$arr_i]}"

        echo ""
        log_info "Setting up: [$this_share] -> $this_path"

        # Already mounted from an earlier run? Offer to reconfigure in
        # place or fully redo it, instead of forcing a new label on a share
        # that already has one.
        local existing existing_label existing_point default_label="$this_share"
        existing="$(_vdm_find_existing_mount "$HOST" "$this_share")"
        if [ -n "$existing" ]; then
            existing_label="${existing%%|*}"
            existing_point="${existing#*|}"
            echo ""
            log_info "[$this_share] is already mounted as '$existing_label' at $existing_point."
            echo "    1) Set up/redo the gocryptfs decrypt layer for it (leaves the mount itself alone)"
            echo "    2) Fully redo this mount (new mount point and/or re-enter credentials)"
            echo "    3) Skip"
            local EXIST_CHOICE=""
            prompt_text "  Choice [1/2/3]:" "1" EXIST_CHOICE
            case "$EXIST_CHOICE" in
                2)
                    _vdm_remove_mount "$existing_label" "$existing_point"
                    default_label="$existing_label"
                    ;;
                3)
                    continue
                    ;;
                *)
                    VDM_LAST_MOUNT_POINT="$existing_point"
                    picked_any=true
                    _vdm_setup_decrypt_layer "$HOST" "$SSH_USER" "$existing_label" "$existing_point"
                    continue
                    ;;
            esac
        fi

        LABEL=""
        while true; do
            prompt_text "  Local label for this mount:" "$default_label" LABEL
            LABEL="$(echo "$LABEL" | tr -cs 'a-zA-Z0-9-' '-' | sed 's/^-*//;s/-*$//')"
            if [ -z "$LABEL" ]; then
                log_warning "Label can't be empty."; continue
            fi
            if grep -q "^${_VDM_TAG_PREFIX} ${LABEL} " /etc/fstab 2>/dev/null; then
                log_warning "Label '$LABEL' is already used — pick another."; continue
            fi
            break
        done

        MOUNT_POINT=""
        prompt_text "  Local mount point:" "/mnt/${LABEL}" MOUNT_POINT

        if _vdm_mount_local "$HOST" "$this_share" "$MOUNT_POINT" "$LABEL" "$SMB_USER" "$SMB_PASS"; then
            # Read by callers like services/filebrowser.sh/audiobookshelf.sh/
            # emby.sh that chain into this service and want to default
            # their own "which directory" prompt to whatever was just
            # mounted. Last one wins if several were picked in this run.
            # _vdm_setup_decrypt_layer overwrites this with the decrypted
            # view's path instead, if one gets set up for this share.
            VDM_LAST_MOUNT_POINT="$MOUNT_POINT"
            picked_any=true
            _vdm_setup_decrypt_layer "$HOST" "$SSH_USER" "$LABEL" "$MOUNT_POINT"
        fi
    done

    if [ "$picked_any" = true ]; then
        echo ""
        log_success "Done."
        echo "  Manage this and other network mounts anytime with:"
        echo "    sudo bash tools/mount-network-drive.sh"
    fi
}

install_vpn-data-mount() {
    echo ""
    echo "╔══════════════════════════════════════════════════════════╗"
    echo "║  VPN Data Mount — mount existing SMB shares from a home   ║"
    echo "║  box over NetBird (read-only — nothing changes there)     ║"
    echo "╚══════════════════════════════════════════════════════════╝"

    if [ "$DRY_RUN" = true ]; then
        echo "[DRY-RUN] Would offer to name a raw IP in /etc/hosts for reuse (SSH + this mount)"
        echo "[DRY-RUN] Would test/set up passwordless SSH to a home box over its NetBird IP"
        echo "[DRY-RUN] Would read-only list the home box's existing Samba shares (never writes there)"
        echo "[DRY-RUN] Would let you pick one or more by number and mount them locally over CIFS"
        echo "[DRY-RUN] Would add each to /etc/fstab with a root-only credentials file (not guest)"
        echo "[DRY-RUN] Would offer a gocryptfs decrypt layer per share (opt-in, requires the home box already set up via tools/gocryptfs-setup-home.sh)"
        echo "[DRY-RUN] Would offer to remove any existing mount (unmount, drop its credentials + fstab entry)"
        echo "[DRY-RUN] Repeatable — can be run again for additional home boxes"
        return 0
    fi

    # Every prompt below (home box IP, share selection, ...) has no sane
    # unattended default — unlike most services here, there's no reasonable
    # value to fall back to. Skip outright rather than let prompt_text's
    # always-blank UNATTENDED behavior spin something forever.
    if [ "$UNATTENDED" = true ]; then
        log_info "Skipping — needs interactive input (home box IP, share selection, ...). Run 'sudo ./setup.sh vpn-data-mount' without --unattended."
        return 0
    fi

    _vdm_list_existing

    local REMOVE=""
    prompt_yn "Remove any existing VPN data mounts? (y/n):" "n" REMOVE
    [[ "$REMOVE" =~ ^[Yy]$ ]] && _vdm_remove_mount_interactive

    while true; do
        local ADD=""
        prompt_yn "Connect to a home box and mount some of its shares now? (y/n):" "y" ADD
        [[ "$ADD" =~ ^[Yy]$ ]] || break

        _vdm_add_mount

        local AGAIN=""
        prompt_yn "Connect to another (different) home box? (y/n):" "n" AGAIN
        [[ "$AGAIN" =~ ^[Yy]$ ]] || break
    done
}

[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_vpn-data-mount
