#!/usr/bin/env bash
# amssh — encrypted SSH login manager
# Storage : ~/.amssh  (AES-256-CBC · PBKDF2-SHA256 · 600 000 iterations)
# Modes   : --tui (default, fzf TUI)  |  --drun (wofi launcher mode)
# Auth    : master passphrase; FIDO2/PAM layer if pam_u2f + pamtester present

set -euo pipefail

# ── constants ────────────────────────────────────────────────────────────────
STORE="${AMSSH_STORE:-$HOME/.amssh}"
CONF_DIR="${HOME}/.config/amssh"
AUTH_CONF="${CONF_DIR}/auth"
ITERS=600000
TERM_CMD="${AMSSH_TERM:-kitty}"
MODE="tui"
VERSION="#amssh:v1"

_TMP=$(mktemp -d /tmp/amssh.XXXXXX)
trap 'rm -rf "$_TMP"' EXIT

# ── usage ────────────────────────────────────────────────────────────────────
_usage() {
    cat >&2 <<'EOF'
amssh — encrypted SSH login manager

Usage:
  amssh [--tui]   Interactive fzf TUI (default)
  amssh --drun    Wofi launcher mode

TUI keys:
  Enter    Connect to selected host
  Ctrl-A   Add new entry
  Ctrl-E   Edit selected entry
  Ctrl-D   Delete selected entry
  Esc      Quit

Store: ~/.amssh (AES-256-CBC, PBKDF2-SHA256)
EOF
}

# ── argument parsing ─────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
    case "$1" in
        --tui|-t)  MODE="tui"  ;;
        --drun|-d) MODE="drun" ;;
        --help|-h) _usage; exit 0 ;;
        *) printf '[amssh] Unknown option: %s\n' "$1" >&2; _usage; exit 1 ;;
    esac
    shift
done

# ── crypto ───────────────────────────────────────────────────────────────────
_decrypt() {
    AMSSH_PASS="$1" openssl enc -aes-256-cbc -pbkdf2 -iter "$ITERS" -d \
        -in "$STORE" -pass env:AMSSH_PASS 2>/dev/null
}

_encrypt_stdin() {
    AMSSH_PASS="$1" openssl enc -aes-256-cbc -pbkdf2 -iter "$ITERS" \
        -out "$STORE" -pass env:AMSSH_PASS
}

# ── passphrase helpers ────────────────────────────────────────────────────────
_ask_tty() {
    local p
    printf '%s' "${1:-Passphrase: }" >&2
    IFS= read -rs p </dev/tty
    printf '\n' >&2
    printf '%s' "$p"
}

_ask_pinentry() {
    local cmd
    cmd=$(command -v pinentry-qt 2>/dev/null \
        || command -v pinentry-gtk-2 2>/dev/null \
        || command -v pinentry 2>/dev/null) || return 1

    local out
    out=$(printf 'SETPROMPT amssh master passphrase\nSETDESC Enter master passphrase for the encrypted SSH store\nGETPIN\n' \
        | "$cmd" 2>/dev/null) || return 1

    printf '%s\n' "$out" | grep -q '^ERR' && return 1
    printf '%s\n' "$out" | awk '/^D /{sub(/^D /,""); print; exit}'
}

# ── FIDO2 / PAM detection ────────────────────────────────────────────────────
PAM_SVC=amssh
PAM_SVC_FILE="/etc/pam.d/${PAM_SVC}"

_find_pam_u2f() {
    local f
    for f in /lib/security/pam_u2f.so \
              /usr/lib/security/pam_u2f.so \
              /lib/x86_64-linux-gnu/security/pam_u2f.so \
              /usr/lib/x86_64-linux-gnu/security/pam_u2f.so \
              /usr/lib/aarch64-linux-gnu/security/pam_u2f.so; do
        [[ -f "$f" ]] && printf '%s' "$f" && return 0
    done
    return 1
}

_fido_keys_file() {
    local f
    for f in "$HOME/.config/Yubico/u2f_keys" "$HOME/.config/pam-u2f/keys"; do
        [[ -f "$f" ]] && printf '%s' "$f" && return 0
    done
    return 1
}

# Hardware + tool present — enough to offer FIDO setup in the dialog.
_fido_hardware_available() {
    _find_pam_u2f >/dev/null && command -v pamtester &>/dev/null
}

# Fully configured — all four prerequisites ready to authenticate.
_fido_pam_available() {
    _fido_hardware_available \
        && _fido_keys_file >/dev/null \
        && [[ -f "$PAM_SVC_FILE" ]]
}

# Creates /etc/pam.d/amssh — requires sudo once.
_ensure_pam_service() {
    [[ -f "$PAM_SVC_FILE" ]] && return 0
    printf '[amssh] Creating PAM service %s (requires sudo)\n' "$PAM_SVC_FILE" >&2
    sudo tee "$PAM_SVC_FILE" >/dev/null <<'PAM'
#%PAM-1.0
auth required pam_u2f.so cue
PAM
}

# Registers a FIDO key into ~/.config/Yubico/u2f_keys if not already present.
_register_fido_key() {
    local keys_file="$HOME/.config/Yubico/u2f_keys"
    if [[ -f "$keys_file" ]]; then
        printf '[amssh] Key file already exists at %s\n' "$keys_file" >&2
        printf '[amssh] To add another key run: pamu2fcfg -n >> %s\n' "$keys_file" >&2
        return 0
    fi
    mkdir -p "$(dirname "$keys_file")"
    printf '[amssh] Insert your FIDO key, then press Enter...\n' >&2
    read -r -s < /dev/tty
    printf '[amssh] Touch your FIDO key when it blinks...\n' >&2
    if pamu2fcfg > "$keys_file" 2>/dev/tty; then
        printf '[amssh] Key registered at %s\n' "$keys_file" >&2
    else
        rm -f "$keys_file"
        printf '[amssh] Registration failed — check key and retry\n' >&2
        return 1
    fi
}

_pam_authenticate() {
    # Both stdout and stderr must go to /dev/tty: pamtester prints its success
    # message to stdout, which would contaminate pass=$(_get_passphrase) if left
    # uncaptured. stderr carries the pam_u2f tap prompt.
    pamtester "$PAM_SVC" "$USER" authenticate >/dev/tty 2>&1
}

# ── master auth (passphrase + optional FIDO2 PAM layer) ─────────────────────
_get_passphrase() {
    # FIDO2/PAM second-factor: opt-in via AMSSH_PAM=1
    if [[ "${AMSSH_PAM:-}" == "1" ]] && _fido_pam_available; then
        printf '[amssh] FIDO2/PAM authentication required\n' >&2
        _pam_authenticate || { printf '[amssh] PAM auth failed\n' >&2; exit 1; }
        printf '[amssh] PAM OK\n' >&2
    fi

    local pass
    if [[ "$MODE" == "drun" ]]; then
        pass=$(_ask_pinentry) || pass=$(_ask_tty "amssh passphrase: ")
    else
        pass=$(_ask_tty "amssh passphrase: ")
    fi
    [[ -z "$pass" ]] && { printf '[amssh] No passphrase provided\n' >&2; exit 1; }
    printf '%s' "$pass"
}

# ── store helpers ─────────────────────────────────────────────────────────────
# Entry format:  alias|user|host|port|identity|description
# (port defaults to 22; identity and description may be empty)

_init_store() {
    printf '%s\n' "$VERSION" | _encrypt_stdin "$1"
}

_verify_pass() {
    _decrypt "$1" 2>/dev/null | grep -q "^${VERSION}" || return 1
}

_load_entries() {
    _decrypt "$1" | grep -v '^#' | grep -v '^[[:space:]]*$' || true
}

_save_entries() {
    # reads entry lines from stdin, prepends version header, encrypts
    local pass="$1"
    { printf '%s\n' "$VERSION"; cat; } | grep -v '^[[:space:]]*$' | _encrypt_stdin "$pass"
}

# ── display formatting ────────────────────────────────────────────────────────
# Entry format: alias|user|host|port|identity|description|password  (7 fields)
# password is stored in plaintext inside the AES-256 encrypted store.

_entry_to_display() {
    awk -F'|' '{
        addr = $2 "@" $3
        if ($4 != "" && $4 != "22") addr = addr ":" $4
        line = $1 " \342\200\224 " addr
        if ($7 != "") line = line " [*]"
        if ($6 != "") line = line " \342\200\224 " $6
        print line
    }'
}

# ── SSH connector ─────────────────────────────────────────────────────────────
_connect() {
    local entry="$1"
    local alias user host port identity desc password
    IFS='|' read -r alias user host port identity desc password <<< "$entry"

    local args=()
    [[ -n "$port" && "$port" != "22" ]] && args+=(-p "$port")
    [[ -n "$identity" ]]               && args+=(-i "$identity")
    args+=("${user}@${host}")

    printf '[amssh] Connecting: ssh %s\n' "${args[*]}" >&2

    if [[ -n "$password" ]]; then
        # Use SSH_ASKPASS_REQUIRE=force so SSH calls our script for the password
        # instead of prompting interactively — works with OpenSSH 8.4+
        local askpass="$_TMP/askpass"
        printf '#!/bin/sh\nprintf "%%s" "$AMSSH_CONN_PW"\n' > "$askpass"
        chmod 700 "$askpass"
        AMSSH_CONN_PW="$password" SSH_ASKPASS="$askpass" \
            SSH_ASKPASS_REQUIRE=force ssh "${args[@]}"
    else
        ssh "${args[@]}"
    fi
}

# ── prompt for entry (drun / zenity) ─────────────────────────────────────────
_prompt_entry_zenity() {
    local result
    result=$(zenity --forms \
        --title="amssh — Add Entry" \
        --text="SSH login details" \
        --add-entry="user@host[:port]" \
        --add-entry="Alias" \
        --add-password="SSH password (optional)" \
        --separator='|' 2>/dev/null) || return 1

    local target alias password
    IFS='|' read -r target alias password <<< "$result"

    local user hostport host port
    user="${target%%@*}"; hostport="${target#*@}"
    if [[ "$hostport" == *:* ]]; then
        host="${hostport%%:*}"; port="${hostport##*:}"
    else
        host="$hostport"; port="22"
    fi

    [[ -z "$alias" || -z "$user" || -z "$host" ]] && {
        zenity --error --text="user@host and alias are required" 2>/dev/null || true
        return 1
    }
    printf '%s|%s|%s|%s||%s|%s' "$alias" "$user" "$host" "$port" "$alias" "$password"
}

# ── TUI mode (fzf with execute+reload — stays inside fzf on add/delete) ──────
_tui_mode() {
    local pass="$1"
    local passfile="$_TMP/.pass"
    local list_sh="$_TMP/list.sh"
    local add_sh="$_TMP/add.sh"
    local del_sh="$_TMP/del.sh"
    local prev_sh="$_TMP/prev.sh"

    # Secure passphrase file — readable only by owner
    printf '%s' "$pass" > "$passfile"
    chmod 600 "$passfile"

    # Snapshot constants into local vars for script generation
    local S="$STORE" I="$ITERS" V="$VERSION"

    # ── list.sh: decrypt + format for fzf display ────────────────────────────
    {   printf '#!/usr/bin/env bash\n'
        printf '_P=$(cat %q); _S=%q; _I=%q\n' "$passfile" "$S" "$I"
        cat << 'LIST'
AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" -d \
    -in "$_S" -pass env:AMSSH_PASS 2>/dev/null \
| awk -F'|' '!/^#/ && NF>0 {
    addr=$2"@"$3; if($4!=""&&$4!="22") addr=addr":"$4
    line=$1" \342\200\224 "addr
    if($7!="") line=line" [*]"
    if($6!="") line=line" \342\200\224 "$6
    print line
}' 2>/dev/null || true
LIST
    } > "$list_sh"; chmod +x "$list_sh"

    # ── add.sh: full-screen TUI form (alternate screen, hidden password) ────────
    {   printf '#!/usr/bin/env bash\n'
        printf '_P=$(cat %q); _S=%q; _I=%q; _V=%q\n' "$passfile" "$S" "$I" "$V"
        cat << 'ADD'
# ── terminal setup ────────────────────────────────────────────────────────────
_stty=$(stty -g 2>/dev/null)
_cleanup() { stty "$_stty" 2>/dev/null; tput rmcup 2>/dev/null; tput cnorm 2>/dev/null; }
trap '_cleanup; exit 1' INT TERM
trap '_cleanup'          EXIT

tput smcup 2>/dev/null   # switch to alternate screen
tput civis               # hide cursor while drawing
tput clear

cols=$(tput cols  2>/dev/null || printf '80')
rows=$(tput lines 2>/dev/null || printf '24')

BW=66; BH=14
BX=$(( (cols - BW) / 2 )); [[ $BX -lt 0 ]] && BX=0
BY=$(( (rows - BH) / 2 )); [[ $BY -lt 0 ]] && BY=0

# colour / style shortcuts
_b=$(tput bold); _r=$(tput sgr0); _di=$(tput dim)
_HL=$'\033[38;2;228;0;70m'    # Highlights  #E40046
_DK=$'\033[38;2;80;24;221m'   # Dark        #5018DD
_QW=$'\033[38;2;214;171;171m' # Quasi-White #D6ABAB
_RH=$'\033[38;2;245;5;5m'     # Red-Hivis   #F50505

# ── draw box ──────────────────────────────────────────────────────────────────
tput cup $BY $BX
printf "${_b}${_HL}╔$(printf '═%.0s' $(seq 1 $((BW-2))))╗${_r}"
for (( i=1; i<BH-1; i++ )); do
    tput cup $((BY+i)) $BX;           printf "${_HL}║${_r}"
    tput cup $((BY+i)) $((BX+BW-1)); printf "${_HL}║${_r}"
done
tput cup $((BY+BH-1)) $BX
printf "${_b}${_HL}╚$(printf '═%.0s' $(seq 1 $((BW-2))))╝${_r}"

_title=" Add SSH Server "
tput cup $BY $(( BX + (BW - ${#_title}) / 2 ))
printf "${_b}${_HL}${_title}${_r}"

# ── layout constants ──────────────────────────────────────────────────────────
# Columns
_LC=$((BX+3));  _LF=$((BX+13))   # left  label col / field col
_RC=$((BX+37)); _RF=$((BX+43))   # right label col / field col
_LFW=22;        _RFW=18          # field widths

# Rows
_R1=$((BY+3))   # Username | Host
_R2=$((BY+4))   # Password | Port
_R3=$((BY+6))   # Alias
_AFW=48         # alias field width

# draw one field slot: "▸ ·····"
_slot() {
    local row=$1 col=$2 w=$3
    tput cup "$row" "$col"
    printf "${_DK}▸ ${_di}$(printf '·%.0s' $(seq 1 "$w"))${_r}"
}

# draw labels
tput cup $_R1 $_LC; printf "${_QW}${_b}Username${_r}"
tput cup $_R2 $_LC; printf "${_QW}${_b}Password${_r}"
tput cup $_R3 $_LC; printf "${_QW}${_b}Alias${_r}"
tput cup $_R1 $_RC; printf "${_QW}${_b}Host${_r}"
tput cup $_R2 $_RC; printf "${_QW}${_b}Port${_r}"

# draw field slots
_slot $_R1 $_LF $_LFW
_slot $_R1 $_RF $_RFW
_slot $_R2 $_LF $_LFW
_slot $_R2 $_RF 5
_slot $_R3 $_LF $_AFW

# hint line
tput cup $((BY+BH-3)) $((BX+3))
printf "${_QW}${_di}Tab/Enter to advance · Ctrl-C to cancel${_r}"

# ── field reader helpers ──────────────────────────────────────────────────────
# Reads chars until Tab or Enter; Backspace edits in place; sets named var.
_readfield() {
    local _v="$1" _r="" _ch
    while IFS= read -r -s -n1 _ch; do
        case "$_ch" in
            $'\t'|$'\n'|'') break ;;
            $'\x7f'|$'\x08') [[ -n "$_r" ]] && { _r="${_r%?}"; printf '\b \b'; } ;;
            *) _r+="$_ch"; printf '%s' "$_ch" ;;
        esac
    done
    printf -v "$_v" '%s' "$_r"
}
_readpw() {
    local _v="$1" _r="" _ch
    while IFS= read -r -s -n1 _ch; do
        case "$_ch" in
            $'\t'|$'\n'|'') break ;;
            $'\x7f'|$'\x08') [[ -n "$_r" ]] && { _r="${_r%?}"; printf '\b \b'; } ;;
            *) _r+="$_ch"; printf '*' ;;
        esac
    done
    printf -v "$_v" '%s' "$_r"
}

# ── read fields ───────────────────────────────────────────────────────────────
tput cnorm
_user="" _host="" _pw="" _port="" _alias=""

tput cup $_R1 $((_LF+2)); _readfield _user
tput cup $_R1 $((_RF+2)); _readfield _host
tput cup $_R2 $((_LF+2)); _readpw    _pw
tput cup $_R2 $((_RF+2)); printf "22"; tput cup $_R2 $((_RF+2)); _readfield _port
tput cup $_R3 $((_LF+2)); _readfield _alias

[[ -z "$_port" ]] && _port="22"
tput civis

# ── validate ──────────────────────────────────────────────────────────────────
_err() {
    tput cup $((BY+BH-2)) $((BX+3))
    printf "${_RH}${_b}✗ %s${_r}" "$1"
    sleep 1.5; exit 1
}

[[ -z "$_user"  ]] && _err "username is required"
[[ -z "$_host"  ]] && _err "host is required"
[[ -z "$_alias" ]] && _err "alias is required"

_ex=$(AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" -d \
    -in "$_S" -pass env:AMSSH_PASS 2>/dev/null \
    | grep -v '^#' | grep -v '^[[:space:]]*$' || true)

if printf '%s\n' "$_ex" | grep -q "^${_alias}|" 2>/dev/null; then
    _err "alias \"${_alias}\" already exists"
fi

# ── save ──────────────────────────────────────────────────────────────────────
{ printf '%s\n' "$_V"
  printf '%s\n' "$_ex"
  printf '%s|%s|%s|%s||%s|%s\n' "$_alias" "$_user" "$_host" "$_port" "$_alias" "$_pw"
} | grep -v '^[[:space:]]*$' \
  | AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" \
      -out "$_S" -pass env:AMSSH_PASS

tput cup $((BY+BH-2)) $((BX+3))
printf "${_QW}${_b}✓ Added:${_r} %s → %s@%s:%s" "$_alias" "$_user" "$_host" "$_port"
sleep 0.8
ADD
    } > "$add_sh"; chmod +x "$add_sh"

    # ── del.sh: delete entry by alias ────────────────────────────────────────
    {   printf '#!/usr/bin/env bash\n'
        printf '_P=$(cat %q); _S=%q; _I=%q; _V=%q\n' "$passfile" "$S" "$I" "$V"
        cat << 'DEL'
_alias="$1"; [[ -z "$_alias" ]] && exit 0
_ex=$(AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" -d \
    -in "$_S" -pass env:AMSSH_PASS 2>/dev/null \
    | grep -v '^#' | grep -v '^[[:space:]]*$' || true)
{ printf '%s\n' "$_V"
  printf '%s\n' "$_ex" | grep -v "^${_alias}|"
} | grep -v '^[[:space:]]*$' \
  | AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" \
      -out "$_S" -pass env:AMSSH_PASS
printf '  Deleted: %s\n' "$_alias" >&2
DEL
    } > "$del_sh"; chmod +x "$del_sh"

    # ── prev.sh: preview panel ────────────────────────────────────────────────
    {   printf '#!/usr/bin/env bash\n'
        printf '_P=$(cat %q); _S=%q; _I=%q\n' "$passfile" "$S" "$I"
        cat << 'PREV'
_line="$1"
_a=$(printf '%s' "$_line" | awk -F' \342\200\224 ' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$1);print $1}')
[[ -z "$_a" ]] && exit 0
AMSSH_PASS="$_P" openssl enc -aes-256-cbc -pbkdf2 -iter "$_I" -d \
    -in "$_S" -pass env:AMSSH_PASS 2>/dev/null \
| awk -F'|' -v a="$_a" '$1==a {
    addr=$3; if($4!=""&&$4!="22") addr=addr":"$4
    printf "  Alias     : %s\n  User      : %s\n  Host      : %s\n  Port      : %s\n  Identity  : %s\n  Password  : %s\n  Note      : %s\n",
        $1,$2,addr,($4?$4:"22"),($5?$5:"(default)"),($7?"stored":"none"),($6?$6:"-")
}' 2>/dev/null || true
PREV
    } > "$prev_sh"; chmod +x "$prev_sh"

    # ── fzf loop: stay open after SSH session ends ────────────────────────────
    while true; do
        local sel
        sel=$(bash "$list_sh" \
            | fzf \
                --prompt="SSH › " \
                --color='fg:#D6ABAB,hl:#E40046,fg+:#D6ABAB,bg+:#1A1A1A,hl+:#F50505,info:#5018DD,prompt:#E40046,pointer:#E40046,marker:#5018DD,spinner:#E40046,header:#D6ABAB' \
                --header=$'  \e[1m\e[38;2;228;0;70mEnter\e[0m connect   \e[1m\e[38;2;228;0;70ma\e[0m add   \e[1m\e[38;2;228;0;70md\e[0m delete   \e[1m\e[38;2;228;0;70mq\e[0m quit\n  \e[2m\e[38;2;80;24;221mj/k\e[0m down/up   \e[2m\e[38;2;80;24;221mg/G\e[0m top/bottom   \e[2m\e[38;2;80;24;221m/\e[0m filter' \
                --bind="a:execute(bash '$add_sh')+reload(bash '$list_sh')" \
                --bind="d:execute(bash '$del_sh' {1})+reload(bash '$list_sh')" \
                --preview="bash '$prev_sh' {}" \
                --preview-window="right:50%:wrap" \
                --no-sort --ansi \
                --bind="j:down,k:up,g:first,G:last,/:toggle-search,q:abort" \
            2>/dev/null || true)

        [[ -z "$sel" ]] && break

        local alias
        alias=$(printf '%s' "$sel" \
            | awk -F' \342\200\224 ' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$1); print $1}')
        [[ -z "$alias" ]] && break

        local entry
        entry=$(AMSSH_PASS="$pass" openssl enc -aes-256-cbc -pbkdf2 -iter "$ITERS" -d \
            -in "$STORE" -pass env:AMSSH_PASS 2>/dev/null \
            | awk -F'|' -v a="$alias" '$1==a' || true)
        [[ -z "$entry" ]] && { printf '[amssh] Entry not found: %s\n' "$alias" >&2; continue; }

        _connect "$entry"
        # fzf reopens after SSH session ends
    done
}

# ── drun mode (wofi) ──────────────────────────────────────────────────────────
_drun_mode() {
    local pass="$1"

    local raw_entries display_list
    raw_entries=$(_load_entries "$pass")

    if [[ -z "$raw_entries" ]]; then
        display_list="(no entries — add via amssh --tui)"
    else
        display_list=$(printf '%s\n' "$raw_entries" | _entry_to_display)
    fi

    local selected
    selected=$(printf '%s\n' "$display_list" \
        | wofi --show=dmenu --prompt="SSH: " --dmenu \
        2>/dev/null) || exit 0

    [[ -z "$selected" || "$selected" == "(no entries"* ]] && exit 0

    local alias
    alias=$(printf '%s' "$selected" | awk -F' — ' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",\$1); print $1}')

    local entry
    entry=$(printf '%s\n' "$raw_entries" | awk -F'|' -v a="$alias" '$1==a')
    [[ -z "$entry" ]] && exit 1

    local e_alias e_user e_host e_port e_identity e_desc
    IFS='|' read -r e_alias e_user e_host e_port e_identity e_desc <<< "$entry"

    local args=()
    [[ -n "$e_port" && "$e_port" != "22" ]] && args+=(-p "$e_port")
    [[ -n "$e_identity" ]]                  && args+=(-i "$e_identity")
    args+=("${e_user}@${e_host}")

    # Launch SSH in a terminal window
    "$TERM_CMD" -e ssh "${args[@]}"
}

# ── FIDO2 info on startup (TUI only) ─────────────────────────────────────────
_print_fido_status() {
    [[ "$MODE" != "tui" ]] && return
    if _fido_pam_available; then
        if [[ "${AMSSH_PAM:-}" == "1" ]]; then
            printf '[amssh] FIDO2/PAM: active (second factor enabled)\n' >&2
        else
            printf '[amssh] FIDO2/PAM: available — set AMSSH_PAM=1 to require as second factor\n' >&2
        fi
    fi
}

# ── auth config (persisted choice from first-launch dialog) ──────────────────
_load_auth_config() {
    [[ ! -f "$AUTH_CONF" ]] && return
    local method
    method=$(cat "$AUTH_CONF")
    if [[ "$method" == "fido" ]] && _fido_pam_available; then
        export AMSSH_PAM=1
    fi
}

_first_launch_dialog() {
    mkdir -p "$CONF_DIR"

    # FIDO2 hardware unavailable — silently default to passphrase
    if ! _fido_hardware_available; then
        printf 'passphrase' > "$AUTH_CONF"
        return
    fi

    local choice
    if [[ "$MODE" == "drun" ]]; then
        choice=$(zenity --list \
            --radiolist \
            --title="amssh — Authentication Setup" \
            --text="Choose how to unlock your SSH store:" \
            --column="" --column="Method" --column="Description" \
            TRUE  "passphrase" "Master passphrase (recommended)" \
            FALSE "fido"       "FIDO2 hardware key (second factor)" \
            --height=210 --width=500 2>/dev/null) || choice="passphrase"
    else
        # whiptail writes selection to stderr; swap fds so $() captures it
        choice=$(whiptail \
            --title "amssh — Authentication Setup" \
            --menu "Choose how to unlock your SSH store:" \
            13 62 2 \
            "passphrase" "Master passphrase" \
            "fido"       "FIDO2 hardware key (second factor)" \
            3>&1 1>&2 2>&3) || choice="passphrase"
    fi

    [[ -z "$choice" ]] && choice="passphrase"
    printf '%s' "$choice" > "$AUTH_CONF"

    if [[ "$choice" == "fido" ]]; then
        printf '\n[amssh] FIDO2 selected — setting up...\n' >&2
        _register_fido_key || { printf 'passphrase' > "$AUTH_CONF"; return; }
        _ensure_pam_service || { printf 'passphrase' > "$AUTH_CONF"; return; }
        printf '[amssh] FIDO2 ready — your key will be required on each unlock.\n' >&2
    fi
}

# ── main ─────────────────────────────────────────────────────────────────────
main() {
    _load_auth_config

    if [[ ! -f "$STORE" ]]; then
        _first_launch_dialog
        _load_auth_config  # apply choice made in dialog
    fi

    _print_fido_status

    local pass
    pass=$(_get_passphrase)

    if [[ ! -f "$STORE" ]]; then
        printf '[amssh] Creating new store at %s\n' "$STORE" >&2
        local confirm
        if [[ "$MODE" == "drun" ]]; then
            confirm=$(_ask_pinentry) || { printf '[amssh] Cancelled\n' >&2; exit 1; }
        else
            confirm=$(_ask_tty "Confirm passphrase: ")
        fi
        [[ "$pass" != "$confirm" ]] && { printf '[amssh] Passphrases do not match\n' >&2; exit 1; }
        _init_store "$pass"
        printf '[amssh] Store initialised\n' >&2
        [[ "$MODE" == "drun" ]] && exit 0
    else
        _verify_pass "$pass" || { printf '[amssh] Wrong passphrase or corrupted store\n' >&2; exit 1; }
    fi

    case "$MODE" in
        tui)  _tui_mode  "$pass" ;;
        drun) _drun_mode "$pass" ;;
    esac
}

main
