#!/bin/bash
# enroll-biometrics.sh — TUI for face biometric setup.
#
# Two subsystems:
#   1. Presence detection — configure/test the webcam used by presence-detect.sh
#   2. Howdy face auth    — enroll/manage/test face models for PAM authentication

BACKTITLE="Biometric Enrollment"
PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py"

# howdy keeps its own camera setting in this INI, entirely separate from the
# presence-detect camera. Enrollment aborts in VideoCapture before touching the
# lens if `device_path` is left at the package default of `none`, so the howdy
# menu has to write a real /dev/video* node in here.
HOWDY_CONFIG="/usr/lib/security/howdy/config.ini"

# ── PAM 2FA (howdy face + FIDO U2F) ────────────────────────────────────────────
# Both factors required, with the normal password stack kept as a fallback so a
# dead camera or absent key can never lock you out.
PAM_TARGETS=(/etc/pam.d/sudo /etc/pam.d/hyprlock /etc/pam.d/login)
# howdy 2.x has no pam_howdy.so — it authenticates through pam_python.so loading
# its own entrypoint script. pam_python.so ships in the AUR package `pam-python`.
PAM_PYTHON_SO="/usr/lib/security/pam_python.so"
HOWDY_PAM_PY="/usr/lib/security/howdy/pam.py"
PAM_U2F_SO="/usr/lib/security/pam_u2f.so"
U2F_MAP="/etc/u2f_mappings"
PAM_MARK_BEGIN="# enroll-biometrics:begin howdy+u2f 2fa"
PAM_MARK_END="# enroll-biometrics:end howdy+u2f 2fa"

# ── Dialog theme (Cyberqueer) ─────────────────────────────────────────────────
TMP_D=$(mktemp -d)
trap 'rm -rf "$TMP_D"' EXIT INT TERM

export DIALOGRC="$TMP_D/dialogrc"
cat > "$DIALOGRC" << 'RCEOF'
use_shadow = ON
use_colors = ON
screen_color              = (BLACK,BLACK,ON)
shadow_color              = (BLACK,BLACK,ON)
title_color               = (MAGENTA,BLACK,ON)
border_color              = (MAGENTA,BLACK,ON)
button_active_color       = (BLACK,MAGENTA,ON)
button_inactive_color     = (WHITE,BLACK,OFF)
button_key_active_color   = (BLACK,CYAN,ON)
button_key_inactive_color = (CYAN,BLACK,ON)
button_label_active_color   = (BLACK,MAGENTA,ON)
button_label_inactive_color = (WHITE,BLACK,OFF)
inputbox_color        = (WHITE,BLACK,OFF)
inputbox_border_color = (MAGENTA,BLACK,ON)
menubox_color         = (WHITE,BLACK,OFF)
menubox_border_color  = (MAGENTA,BLACK,ON)
item_color            = (WHITE,BLACK,OFF)
item_selected_color   = (BLACK,MAGENTA,ON)
tag_color             = (CYAN,BLACK,ON)
tag_selected_color    = (BLACK,CYAN,ON)
check_color           = (WHITE,BLACK,OFF)
check_selected_color  = (BLACK,MAGENTA,ON)
uarrow_color          = (MAGENTA,BLACK,ON)
darrow_color          = (MAGENTA,BLACK,ON)
RCEOF

# ── Helpers ───────────────────────────────────────────────────────────────────
die() { clear; printf "\nError: %s\n\n" "$1" >&2; exit 1; }

msg() {
    dialog --backtitle "$BACKTITLE" --title " $1 " --msgbox "\n$2\n" "$3" "$4"
}

require_dialog() {
    command -v dialog &>/dev/null && return
    sudo pacman -S --noconfirm dialog || die "dialog not found"
}

# ── Camera helpers ─────────────────────────────────────────────────────────────
# True only for nodes that actually deliver video frames. UVC webcams expose
# extra /dev/video* nodes for metadata capture (no pixel formats); pointing
# howdy at one of those makes it fail to read a frame and abort with exit 1.
is_capture_device() {
    v4l2-ctl --device="$1" --list-formats 2>/dev/null \
        | grep -qE '^[[:space:]]*\[[0-9]+\]:'
}

list_cameras() {
    for dev in /dev/video*; do
        [[ -c "$dev" ]] || continue
        is_capture_device "$dev" || continue
        local id="${dev#/dev/video}"
        local name
        name=$(v4l2-ctl --device="$dev" --info 2>/dev/null \
               | awk -F': ' '/Card type/{print $2}' | head -1)
        [[ -z "$name" ]] && name="Camera $id"
        printf '%s\n%s\n' "$id" "$name"
    done
}

get_camera_id() {
    if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then
        echo "$PRESENCE_DETECT_CAMERA"
    elif [[ -f "$PRESENCE_CFG" ]]; then
        grep -oP 'CAMERA=\K[0-9]+' "$PRESENCE_CFG" 2>/dev/null || echo 0
    else
        echo 0
    fi
}

set_camera_id() {
    mkdir -p "$(dirname "$PRESENCE_CFG")"
    printf 'CAMERA=%s\n' "$1" > "$PRESENCE_CFG"
}

# Grab a single still from a v4l2 node into $2. No sudo: /dev/video* is acl/group
# readable by the user. The first frame off a cold sensor is often black, so pull
# a few and keep the last.
camera_capture_frame() {
    local dev="$1" out="$2"
    command -v ffmpeg &>/dev/null || return 2
    ffmpeg -hide_banner -loglevel error -f v4l2 -i "$dev" \
           -frames:v 5 -update 1 -y "$out" </dev/null 2>/dev/null
    [[ -s "$out" ]]
}

# Render an image inline via kitty's graphics protocol. KITTY_WINDOW_ID is the
# reliable signal — $TERM is frequently xterm-256color even under kitty.
preview_frame() {
    local img="$1"
    [[ -s "$img" ]] || return 1
    command -v kitty &>/dev/null || return 1
    [[ -n "$KITTY_WINDOW_ID" || "$TERM" == *kitty* ]] || return 1
    kitty +kitten icat --align left --clear "$img"
}

# IR sensors deliver near-zero chroma; colour webcams don't. Returns 0 when the
# still looks monochrome (likely IR), 1 otherwise. Assumes colour when it can't
# measure — that's the case that needs the extra non-IR tuning anyway.
camera_looks_ir() {
    local img="$1" sat
    [[ -s "$img" ]] || return 1
    command -v ffmpeg &>/dev/null || return 1
    sat=$(ffmpeg -hide_banner -loglevel error -i "$img" \
            -vf signalstats,metadata=mode=print:file=- -f null - 2>/dev/null \
          | awk -F= '/SATAVG/{print $2; exit}')
    [[ -z "$sat" ]] && return 1
    awk -v s="$sat" 'BEGIN{exit !(s < 12)}'
}

# Shared camera chooser: menu → live still preview in kitty → confirm. The pick
# is returned in the global PICKED_CAM (numeric id); returns 1 if cancelled.
PICKED_CAM=""
PICKED_CAM_FRAME=""
pick_camera_with_preview() {
    local title="$1" current="$2"
    local -a cam_items
    mapfile -t cam_items < <(list_cameras)

    if [[ ${#cam_items[@]} -eq 0 ]]; then
        msg "No Cameras Found" \
            "No video devices found at /dev/video*.\n\nMake sure your webcam is connected." \
            10 55
        return 1
    fi

    while true; do
        local choice
        choice=$(dialog --backtitle "$BACKTITLE" \
            --title " $title " \
            --menu "\nCurrent: ${current}\n\nSelect a camera:" \
            16 62 6 \
            "${cam_items[@]}" \
            3>&1 1>&2 2>&3) || return 1

        local dev="/dev/video${choice}"
        clear
        printf "\n\033[1;35m  Preview — %s\033[0m\n" "$dev"
        printf "\033[35m  ─────────────────────────────────────────\033[0m\n\n"

        local frame="$TMP_D/cam_preview.png"
        camera_capture_frame "$dev" "$frame"
        case $? in
            0) preview_frame "$frame" \
                   || printf "  (kitty preview unavailable; frame saved to %s)\n" "$frame" ;;
            2) printf "  ffmpeg is not installed — skipping preview.\n" ;;
            *) printf "  Could not capture a frame from %s.\n  The device may be busy or not a capture node.\n" "$dev" ;;
        esac

        printf "\n"
        local ans
        read -rp "  Use ${dev}? [Y]es / [n]o, pick another / [c]ancel: " ans
        case "${ans,,}" in
            ""|y|yes) PICKED_CAM="$choice"; PICKED_CAM_FRAME="$frame"; return 0 ;;
            c|cancel) return 1 ;;
            *)        continue ;;
        esac
    done
}

# ── Presence detection ────────────────────────────────────────────────────────
presence_configure_camera() {
    local current; current=$(get_camera_id)
    pick_camera_with_preview "Select Camera (Presence)" "/dev/video${current}" || return

    set_camera_id "$PICKED_CAM"
    msg "Camera Set" \
        "Presence detection will use /dev/video${PICKED_CAM}.\n\nRestart presence-detect.sh for the change to take effect." \
        9 62
}

presence_test_camera() {
    local cam; cam=$(get_camera_id)

    clear
    printf "\n\033[1;35m  Testing presence detection on /dev/video%s...\033[0m\n" "$cam"
    printf "\033[35m  ─────────────────────────────────────────\033[0m\n\n"
    printf "  Look at the camera and stay still.\n\n"

    python3 "$PYTHON_DETECT" "$cam" 2>/dev/null
    local rc=$?

    case $rc in
        0) msg "Test Result" "Face detected!\n\nPresence detection is working correctly." 8 52 ;;
        1) msg "Test Result" \
               "No face detected.\n\nMake sure you are in front of the camera\nand there is adequate lighting." \
               10 56 ;;
        2) msg "Camera Error" \
               "Could not open /dev/video${cam}.\n\nTry configuring a different camera first." \
               9 56 ;;
        *) msg "Error" "Unexpected exit code ($rc) from detection script." 7 52 ;;
    esac
}

# ── Howdy helpers ─────────────────────────────────────────────────────────────
# Detection must survive howdy 2.x's layout: its CLI lives under root-only
# /usr/lib/security/howdy/ with /usr/bin/howdy symlinked into it, so a normal
# user can neither resolve nor run the link — `command -v howdy` reports "not
# found" even when it is installed. Every op here invokes it via `sudo howdy`
# anyway, so fall back to the symlink itself and the package DB.
howdy_installed() {
    command -v howdy &>/dev/null && return 0
    [[ -L /usr/bin/howdy || -e /usr/bin/howdy ]] && return 0
    command -v pacman &>/dev/null && pacman -Qq howdy &>/dev/null
}

howdy_require() {
    howdy_installed && return 0

    # howdy lives only in the AUR — it is not in the official repos, so a
    # plain `pacman -S howdy` always fails. Require an AUR helper.
    local helper
    for h in yay paru; do
        command -v "$h" &>/dev/null && { helper="$h"; break; }
    done
    if [[ -z "$helper" ]]; then
        msg "No AUR Helper" \
            "howdy is an AUR package and needs an AUR helper (yay or paru) to install.\n\nInstall yay first, then re-run this menu." \
            10 60
        return 1
    fi

    dialog --backtitle "$BACKTITLE" --title " Howdy Not Found " \
        --yesno "\nhowdy is not installed.\n\nInstall it now via ${helper}?" 8 48 || return 1
    clear
    printf "\nInstalling howdy via %s...\n\n" "$helper"
    "$helper" -S --noconfirm --needed howdy
    howdy_installed || { msg "Install Failed" "howdy installation failed.\nInstall it manually: ${helper} -S howdy" 8 52; return 1; }
}

# ── Howdy camera (device_path) ────────────────────────────────────────────────
# config.ini is root-only, so reads and writes both go through sudo. Returns the
# bare value (e.g. /dev/video1, or `none` when unconfigured).
howdy_get_device_path() {
    sudo grep -oP '^\s*device_path\s*=\s*\K.*' "$HOWDY_CONFIG" 2>/dev/null | head -1
}

# Generic [video] key writer: replaces the key in place, or inserts it under the
# [video] header if absent. Every key we touch (device_path, recording_plugin,
# dark_threshold) lives only in that section, so a global replace is safe.
howdy_set_cfg() {
    local key="$1" val="$2"
    if sudo grep -qE "^\s*${key}\s*=" "$HOWDY_CONFIG"; then
        sudo sed -i -E "s|^(\s*${key}\s*=).*|\1 ${val}|" "$HOWDY_CONFIG"
    else
        sudo sed -i -E "/^\[video\]/a ${key} = ${val}" "$HOWDY_CONFIG"
    fi
}

howdy_set_device_path() { howdy_set_cfg device_path "$1"; }

# True when howdy points at a node that exists AND can actually capture video
# (not a metadata-only node, which would make enrollment fail with exit 1).
howdy_camera_ok() {
    local dp; dp=$(howdy_get_device_path)
    [[ -n "$dp" && "$dp" != none && -e "$dp" ]] || return 1
    is_capture_device "$dp"
}

# howdy's stock tuning assumes a Windows-Hello IR sensor. A regular colour
# webcam works too — dlib doesn't care about IR — but needs two tweaks:
#   • recording_plugin = ffmpeg  (opencv mis-decodes many YUYV colour streams to
#     grey garbage; ffmpeg reads them cleanly — the same path the preview used)
#   • dark_threshold   = 80      (IR's flashing emitter wants a low value; a
#     colour feed in normal light gets rejected as "too dark" at the default 50)
# The camera-type question defaults from the preview's measured colourfulness.
howdy_apply_camera_profile() {
    local frame="$1" dev="$2"
    local dn=""
    camera_looks_ir "$frame" && dn="--defaultno"   # looks IR ⇒ default the answer to "No"

    dialog --backtitle "$BACKTITLE" --title " Camera Type " $dn --yesno \
"\nIs this a regular (non-IR / colour) webcam?\n\n  Yes — tune howdy for a colour camera\n        (ffmpeg recorder, dark_threshold 80).\n        Works in normal light; less secure.\n\n  No  — IR sensor (Windows Hello style).\n        Keep howdy's default IR tuning." 16 62
    case $? in
        0)  howdy_set_cfg recording_plugin ffmpeg
            howdy_set_cfg dark_threshold   80
            msg "Howdy Camera Set" \
"howdy will use ${dev}.\n\nTuned for a NON-IR colour webcam:\n  recorder        = ffmpeg\n  dark_threshold  = 80\n\nRun 'add face model' next. If frames come out\ngrey or fail, try the other /dev/video node." 14 62 ;;
        1)  howdy_set_cfg recording_plugin opencv
            howdy_set_cfg dark_threshold   50
            msg "Howdy Camera Set" \
"howdy will use ${dev}.\n\nTuned for an IR sensor (howdy defaults):\n  recorder        = opencv\n  dark_threshold  = 50" 12 62 ;;
        *)  msg "Howdy Camera Set" \
"howdy will use ${dev}.\n\nCamera-type tuning left unchanged." 9 56 ;;
    esac
}

howdy_configure_camera() {
    howdy_require || return
    local current; current=$(howdy_get_device_path)
    [[ -z "$current" ]] && current="(unset)"

    pick_camera_with_preview "Select Camera (Howdy)" "$current" || return

    local dev="/dev/video${PICKED_CAM}"
    howdy_set_device_path "$dev"
    howdy_apply_camera_profile "$PICKED_CAM_FRAME" "$dev"
}

# Howdy aborts in VideoCapture when device_path is unset/wrong, surfacing only a
# generic exit code 1. Catch that here and offer to fix it before enrolling.
howdy_ensure_camera() {
    howdy_camera_ok && return 0
    local dp; dp=$(howdy_get_device_path)
    dialog --backtitle "$BACKTITLE" --title " Camera Not Configured " --yesno \
        "\nhowdy has no usable camera (device_path = ${dp:-none}).\n\nEnrollment will fail until this is set.\nConfigure the howdy camera now?" 11 60 \
        || return 1
    howdy_configure_camera
    howdy_camera_ok || {
        msg "Still Not Configured" \
            "howdy's camera is still unset, aborting enrollment." 7 52
        return 1
    }
}

# ── Howdy operations ──────────────────────────────────────────────────────────
howdy_add() {
    howdy_require || return
    howdy_ensure_camera || return

    local name
    name=$(dialog --backtitle "$BACKTITLE" \
        --title " Add Face Model " \
        --inputbox "\nEnter a label for this face model:" 8 52 "$USER" \
        3>&1 1>&2 2>&3) || return
    [[ -z "$name" ]] && return

    clear
    printf "\n\033[1;35m  Enrolling face model '%s'...\033[0m\n" "$name"
    printf "\033[35m  ─────────────────────────────────────────\033[0m\n\n"
    printf "  Look at the camera and hold still.\n\n"
    # howdy has no label flag; the label is read from stdin during `add`.
    # Tee the output so the actual reason (dark frames, no face, busy camera…)
    # can be shown on failure instead of a bare exit code.
    local log="$TMP_D/howdy_add.log"
    printf '%s\n' "$name" | sudo howdy -U "$USER" add 2>&1 | tee "$log"
    local rc=${PIPESTATUS[1]}

    if [[ $rc -eq 0 ]]; then
        msg "Enrolled" \
            "Face model '$name' added.\n\nYou may need to relogin for PAM changes to take effect." \
            9 58
    else
        local why; why=$(grep -vE '^\s*$' "$log" 2>/dev/null | tail -n 4)
        msg "Failed" \
            "Enrollment failed (code $rc).\n\nhowdy said:\n${why:-(no output captured)}\n\nIf it's a camera/frame error, re-run\n'configure camera' and pick another node." \
            16 64
    fi
}

howdy_list() {
    howdy_require || return
    local out; out=$(sudo howdy -U "$USER" list 2>&1)
    msg "Enrolled Face Models" "$out" 20 64
}

howdy_remove() {
    howdy_require || return

    local models; models=$(sudo howdy -U "$USER" list 2>&1)
    if [[ -z "$models" || "$models" == *"no models"* ]]; then
        msg "No Models" "No face models are currently enrolled." 7 44
        return
    fi

    local id
    id=$(dialog --backtitle "$BACKTITLE" \
        --title " Remove Face Model " \
        --inputbox "\nCurrent models:\n\n${models}\n\nEnter model ID to remove:" 20 64 \
        3>&1 1>&2 2>&3) || return
    [[ -z "$id" || ! "$id" =~ ^[0-9]+$ ]] && {
        msg "Invalid ID" "Please enter a numeric model ID." 6 36; return
    }

    dialog --backtitle "$BACKTITLE" --title " Confirm " \
        --yesno "\nRemove face model ID $id?" 7 40 || return

    sudo howdy -U "$USER" remove "$id"
    msg "Done" "Face model $id removed." 6 38
}

howdy_test() {
    howdy_require || return
    clear
    printf "\n\033[1;35m  Testing howdy authentication...\033[0m\n"
    printf "\033[35m  ─────────────────────────────────────────\033[0m\n\n"
    printf "  Look at the camera.\n\n"
    sudo howdy -U "$USER" test
    local rc=$?
    if [[ $rc -eq 0 ]]; then
        msg "Result" "Authentication successful." 6 38
    else
        msg "Result" "Authentication failed (code $rc).\n\nCheck enrolled models and camera setup." 9 52
    fi
}

# ── howdy PAM module (pam_python.so) ──────────────────────────────────────────
# howdy's PAM entrypoint is a Python script loaded by pam_python.so. The module
# is /usr/lib/security/pam_python.so directly (world-traversable, unlike howdy's
# own root-only dir), so a plain existence test is reliable here; pacman -Qq is
# the fallback in case it lands elsewhere.
pam_python_installed() {
    [[ -e "$PAM_PYTHON_SO" ]] && return 0
    command -v pacman &>/dev/null && pacman -Qq pam-python &>/dev/null
}

pam_python_require() {
    pam_python_installed && return 0

    # pam-python is AUR-only, so it needs an AUR helper just like howdy.
    local helper
    for h in yay paru; do
        command -v "$h" &>/dev/null && { helper="$h"; break; }
    done
    if [[ -z "$helper" ]]; then
        msg "No AUR Helper" \
            "howdy's PAM module needs the AUR package 'pam-python', which requires an AUR helper (yay or paru).\n\nInstall yay first, then re-run." \
            11 62
        return 1
    fi

    dialog --backtitle "$BACKTITLE" --title " pam-python Needed " \
        --yesno "\nhowdy authenticates via pam_python.so, provided by the AUR package 'pam-python'.\n\nInstall it now via ${helper}?" 10 62 || return 1
    clear
    printf "\nInstalling pam-python via %s...\n\n" "$helper"
    "$helper" -S --noconfirm --needed pam-python
    pam_python_installed || {
        msg "Install Failed" "pam-python installation failed.\nInstall it manually: ${helper} -S pam-python" 8 58
        return 1
    }
}

# ── FIDO U2F helpers ────────────────────────────────────────────────────────────
fido_installed() { [[ -e "$PAM_U2F_SO" ]] && command -v pamu2fcfg &>/dev/null; }

fido_require() {
    fido_installed && return 0
    dialog --backtitle "$BACKTITLE" --title " FIDO U2F Not Found " \
        --yesno "\npam-u2f is not installed.\n\nInstall it now via pacman?" 8 50 || return 1
    clear
    printf "\nInstalling pam-u2f...\n\n"
    sudo pacman -S --noconfirm --needed pam-u2f
    fido_installed || { msg "Install Failed" "pam-u2f installation failed.\nInstall it manually: sudo pacman -S pam-u2f" 8 56; return 1; }
}

# Ensure $USER has a registered key in the central mapping; register one if not.
fido_register() {
    if sudo grep -q "^${USER}:" "$U2F_MAP" 2>/dev/null; then
        dialog --backtitle "$BACKTITLE" --title " FIDO Key " \
            --yesno "\nA FIDO key is already registered for $USER.\n\nRegister an additional key?" 9 56 \
            || return 0
        clear
        printf "\n\033[1;35m  Insert your FIDO key and touch it when it blinks...\033[0m\n\n"
        local extra
        extra=$(pamu2fcfg -n -u "$USER") || { msg "Registration Failed" "Could not read FIDO key." 7 44; return 1; }
        [[ -z "$extra" ]] && { msg "No Key" "No FIDO key detected." 7 36; return 1; }
        # pamu2fcfg -n prints just the credential; append it to the user's line.
        sudo sed -i "s|^\(${USER}:.*\)\$|\1:${extra}|" "$U2F_MAP"
        return 0
    fi

    clear
    printf "\n\033[1;35m  Registering FIDO key for %s...\033[0m\n" "$USER"
    printf "\033[35m  ─────────────────────────────────────────\033[0m\n\n"
    printf "  Insert your FIDO key and touch it when it blinks.\n\n"
    local line
    line=$(pamu2fcfg -u "$USER") || { msg "Registration Failed" "Could not read FIDO key.\n\nMake sure it is plugged in." 9 48; return 1; }
    [[ -z "$line" ]] && { msg "No Key" "No FIDO key detected." 7 36; return 1; }
    printf '%s\n' "$line" | sudo tee -a "$U2F_MAP" >/dev/null
    sudo chmod 600 "$U2F_MAP"
    sudo chown root:root "$U2F_MAP"
    return 0
}

# ── PAM file editing ──────────────────────────────────────────────────────────
# The inserted block: try howdy, then U2F. Both must pass to satisfy auth and
# skip the password stack; if either fails, control falls through to the
# existing (password) auth lines below.
pam_emit_block() {
    printf '%s\n' "$PAM_MARK_BEGIN"
    printf 'auth      [success=ignore default=1] pam_python.so %s\n' "$HOWDY_PAM_PY"
    printf 'auth      sufficient                 pam_u2f.so authfile=%s cue\n' "$U2F_MAP"
    printf '%s\n' "$PAM_MARK_END"
}

pam_apply_file() {
    local f="$1"
    [[ -f "$f" ]] || { printf '  skip  %s (not present)\n' "$f"; return 0; }
    if sudo grep -qF "$PAM_MARK_BEGIN" "$f"; then
        printf '  ok    %s (already configured)\n' "$f"; return 0
    fi
    sudo cp -a "$f" "${f}.bak.$(date +%s)"
    local tmp first; tmp=$(mktemp); first=$(sudo head -n1 "$f")
    if [[ "$first" == "#%PAM"* ]]; then
        { printf '%s\n' "$first"; pam_emit_block; sudo tail -n +2 "$f"; } > "$tmp"
    else
        { pam_emit_block; sudo cat "$f"; } > "$tmp"
    fi
    sudo cp "$tmp" "$f"; rm -f "$tmp"
    printf '  added %s\n' "$f"
}

pam_remove_file() {
    local f="$1"
    [[ -f "$f" ]] || return 0
    sudo grep -qF "$PAM_MARK_BEGIN" "$f" || { printf '  skip  %s (not configured)\n' "$f"; return 0; }
    sudo cp -a "$f" "${f}.bak.$(date +%s)"
    local tmp; tmp=$(mktemp)
    sudo awk -v b="$PAM_MARK_BEGIN" -v e="$PAM_MARK_END" \
        '$0==b{s=1} !s{print} $0==e{s=0}' "$f" > "$tmp"
    sudo cp "$tmp" "$f"; rm -f "$tmp"
    printf '  removed from %s\n' "$f"
}

# hyprlock ships as `auth include login`. Since the 2FA block also lands in
# /etc/pam.d/login, a plain include would re-run the block on the failure path.
# Give hyprlock its own clean fallback by pointing it at system-auth instead.
HYPRLOCK_REDIRECT='s/^(auth[[:space:]]+include[[:space:]]+)login[[:space:]]*$/\1system-auth/'
HYPRLOCK_RESTORE='s/^(auth[[:space:]]+include[[:space:]]+)system-auth[[:space:]]*$/\1login/'

pam_apply_hyprlock() {
    local f="/etc/pam.d/hyprlock"
    [[ -f "$f" ]] || { printf '  skip  %s (not present)\n' "$f"; return 0; }
    if sudo grep -qF "$PAM_MARK_BEGIN" "$f"; then
        printf '  ok    %s (already configured)\n' "$f"; return 0
    fi
    sudo cp -a "$f" "${f}.bak.$(date +%s)"
    local tmp first; tmp=$(mktemp); first=$(sudo head -n1 "$f")
    if [[ "$first" == "#%PAM"* ]]; then
        { printf '%s\n' "$first"; pam_emit_block
          sudo tail -n +2 "$f" | sed -E "$HYPRLOCK_REDIRECT"; } > "$tmp"
    else
        { pam_emit_block; sudo cat "$f" | sed -E "$HYPRLOCK_REDIRECT"; } > "$tmp"
    fi
    sudo cp "$tmp" "$f"; rm -f "$tmp"
    printf '  added %s (fallback → system-auth)\n' "$f"
}

pam_remove_hyprlock() {
    local f="/etc/pam.d/hyprlock"
    [[ -f "$f" ]] || return 0
    sudo grep -qF "$PAM_MARK_BEGIN" "$f" || { printf '  skip  %s (not configured)\n' "$f"; return 0; }
    sudo cp -a "$f" "${f}.bak.$(date +%s)"
    local tmp; tmp=$(mktemp)
    sudo awk -v b="$PAM_MARK_BEGIN" -v e="$PAM_MARK_END" \
        '$0==b{s=1} !s{print} $0==e{s=0}' "$f" | sed -E "$HYPRLOCK_RESTORE" > "$tmp"
    sudo cp "$tmp" "$f"; rm -f "$tmp"
    printf '  removed from %s (fallback → login)\n' "$f"
}

pam_setup() {
    howdy_require       || return
    pam_python_require  || return
    fido_require        || return

    dialog --backtitle "$BACKTITLE" --title " Set Up PAM 2FA " --yesno \
"\nThis will require BOTH face (howdy) and a FIDO key for:\n\n  ${PAM_TARGETS[*]}\n\nYour password still works if a factor is unavailable.\nBackups of each file are saved as *.bak.<timestamp>.\n\nContinue?" 15 64 || return

    fido_register || return

    clear
    printf "\n\033[1;35m  Configuring PAM...\033[0m\n"
    printf "\033[35m  ─────────────────────────────────────────\033[0m\n\n"
    local f
    for f in "${PAM_TARGETS[@]}"; do
        if [[ "$f" == */hyprlock ]]; then pam_apply_hyprlock; else pam_apply_file "$f"; fi
    done
    printf "\n"
    msg "PAM Configured" \
"Face + FIDO 2FA is active (password fallback kept).\n\n*** KEEP THIS TERMINAL OPEN. ***\nIn a NEW terminal, run:  sudo -k; sudo true\nand confirm you can authenticate.\n\nIf locked out, restore a *.bak.<timestamp> file." 15 64
}

pam_teardown() {
    dialog --backtitle "$BACKTITLE" --title " Remove PAM 2FA " --yesno \
"\nRemove the howdy + FIDO 2FA block from:\n\n  ${PAM_TARGETS[*]}\n\nContinue?" 12 64 || return
    clear
    printf "\n\033[1;35m  Removing PAM 2FA...\033[0m\n\n"
    local f
    for f in "${PAM_TARGETS[@]}"; do
        if [[ "$f" == */hyprlock ]]; then pam_remove_hyprlock; else pam_remove_file "$f"; fi
    done
    printf "\n"
    msg "PAM Restored" "The 2FA block was removed.\nBackups remain as *.bak.<timestamp>." 8 56
}

# ── Main menu ─────────────────────────────────────────────────────────────────
main_menu() {
    local choice
    choice=$(dialog --backtitle "$BACKTITLE" \
        --title " Biometric Enrollment " \
        --menu "\nSelect an option:" 23 70 10 \
        "1" "Presence detection  — configure camera" \
        "2" "Presence detection  — test detection" \
        "3" "Howdy face auth     — configure camera" \
        "4" "Howdy face auth     — add face model" \
        "5" "Howdy face auth     — list enrolled models" \
        "6" "Howdy face auth     — remove face model" \
        "7" "Howdy face auth     — test authentication" \
        "8" "PAM 2FA            — set up howdy + FIDO key" \
        "9" "PAM 2FA            — remove howdy + FIDO key" \
        3>&1 1>&2 2>&3) || { clear; exit 0; }

    case "$choice" in
        1) presence_configure_camera ;;
        2) presence_test_camera       ;;
        3) howdy_configure_camera     ;;
        4) howdy_add                  ;;
        5) howdy_list                 ;;
        6) howdy_remove               ;;
        7) howdy_test                 ;;
        8) pam_setup                  ;;
        9) pam_teardown               ;;
    esac
    main_menu
}

require_dialog
main_menu
