#!/bin/bash
# ============================================================
# freeipa-client.sh — FreeIPA client enrolment installer
# ============================================================
# Installs the packages needed to join this machine to a
# FreeIPA domain and then guides the user through three
# enrolment paths via a dialog(1) TUI:
#
#   answerfile — reads a pre-filled JSON file produced by the
#                freeipa-server.sh installer (recommended for
#                scripted/repeatable deployments)
#   manual     — interactive dialog prompts for every option
#   skip       — installs packages only; enrol later manually
#
# Packages installed:
#   sssd              — System Security Services Daemon; provides
#                       PAM/NSS integration for IPA-managed users
#   cyrus-sasl-gssapi — SASL GSSAPI mechanism for Kerberos auth
#   openldap          — LDAP utilities (ldapsearch, etc.)
#   krb5              — Kerberos 5 libraries and kinit/klist tools
#   oddjob            — D-Bus service that auto-creates home dirs
#                       on first login (via pam_oddjob_mkhomedir)
#   freeipa-client    — AUR package providing ipa-client-install
#
# This is an optional module because FreeIPA client enrolment is
# only relevant on managed/enterprise machines that are members
# of a FreeIPA domain.
# ============================================================

set -euo pipefail
# Load shared logging helpers from the dotfiles lib
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"

log "Starting FreeIPA client installer..."

# Resolve the absolute path of this script's directory so all relative
# references (to FreeipaAnsible scripts and answerfiles) remain correct
# regardless of where the installer TUI calls this module from.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FREEIPA_DIR="$SCRIPT_DIR/../../FreeipaAnsible"
# Optional server-generated enrolment wrapper script
CLIENT_SCRIPT="$FREEIPA_DIR/freeipa-client.sh"
# Default answerfile path — the server installer writes this file with
# the domain/realm/server already filled in, so clients only need to
# add the admin password.
DEFAULT_AF="$FREEIPA_DIR/freeipa-client-answerfile.json"

# Default values matching the home lab domain; the server installer
# sed-replaces these in the distributed client scripts so clients
# always get the correct values without any manual editing.
DEF_DOMAIN="freeipa.abdelbaki.eu"
DEF_REALM="FREEIPA.ABDELBAKI.EU"
DEF_SERVER="freeipa.abdelbaki.eu"

# ── Packages ──────────────────────────────────────────────────────────────────
echo "[+] Installing FreeIPA client packages..."
# pacman (without sudo) because this script may be run as root by the TUI.
# Only sssd + Kerberos/LDAP tooling are in the official repos:
# sssd          : provides NSS/PAM lookups against IPA LDAP + Kerberos
# cyrus-sasl-gssapi : GSSAPI mechanism; needed for LDAP binds with Kerberos tickets
# openldap      : ldapsearch and other LDAP client utilities
# krb5          : Kerberos 5 runtime; kinit, klist, kdestroy
pacman -S --noconfirm --needed sssd cyrus-sasl-gssapi openldap krb5

# oddjob (pam_oddjob_mkhomedir for on-login home creation) is AUR-only on Arch —
# `pacman -S oddjob` fails with "target not found". It is optional: pam_mkhomedir
# provides the same on-login home creation, so degrade gracefully if the AUR
# build is unavailable.
if command -v yay &>/dev/null; then
    yay -S --noconfirm --needed oddjob 2>/dev/null \
        || echo "[!] oddjob AUR build failed — mkhomedir will fall back to pam_mkhomedir."
else
    echo "[!] yay not found — skipping oddjob (mkhomedir falls back to pam_mkhomedir)."
fi

# freeipa-client provides ipa-client-install (the official FreeIPA enrolment
# tool).  It is AUR-only on Arch; gracefully degrade if yay is absent.
if command -v yay &>/dev/null; then
    echo "[+] Installing freeipa-client (AUR)..."
    yay -S --noconfirm --needed freeipa-client
else
    echo "[!] yay not found — skipping freeipa-client AUR package."
    echo "    Install yay then run: yay -S --needed freeipa-client"
fi

# Enable SSSD now so it starts at boot after enrolment.
# "|| true" suppresses the error if the service unit doesn't exist yet
# (it will exist after freeipa-client is installed).
systemctl enable sssd.service 2>/dev/null || true

# ── run_enroll helper ─────────────────────────────────────────────────────────
# Runs the enrolment either via the server-generated freeipa-client.sh wrapper
# (preferred, because it already has the correct domain/realm baked in) or by
# falling back to calling ipa-client-install directly with the same arguments.
run_enroll() {
    local args=("$@")
    if [[ -x "$CLIENT_SCRIPT" ]]; then
        # Preferred path: use the pre-generated client script from the server
        exec "$CLIENT_SCRIPT" "${args[@]}"
    else
        # Fall back to ipa-client-install directly
        local cmd=(ipa-client-install --unattended)
        local dom="" rlm="" srv="" hst="" pri="admin" pwd="" ntp=""
        local mkhomedir=true sudo_=true dns=true fido2=false
        declare -a fido2_users=()

        # Parse our internal argument format and translate to ipa-client-install flags
        for ((i=0; i<${#args[@]}; i++)); do
            case "${args[$i]}" in
                --domain)      dom="${args[$((i+1))]}";  ((i++)) ;;
                --realm)       rlm="${args[$((i+1))]}";  ((i++)) ;;
                --server)      srv="${args[$((i+1))]}";  ((i++)) ;;
                --hostname)    hst="${args[$((i+1))]}";  ((i++)) ;;
                --principal)   pri="${args[$((i+1))]}";  ((i++)) ;;
                --password)    pwd="${args[$((i+1))]}";  ((i++)) ;;
                --ntp-server)  ntp="${args[$((i+1))]}";  ((i++)) ;;
                --no-mkhomedir) mkhomedir=false ;;
                --no-sudo)      sudo_=false ;;
                --no-dns-update) dns=false ;;
                --fido2)        fido2=true ;;
                --fido2-user)  fido2_users+=("${args[$((i+1))]}"); ((i++)) ;;
            esac
        done

        # Build the ipa-client-install command from parsed values
        [[ -n "$dom" ]] && cmd+=(--domain "$dom")
        [[ -n "$rlm" ]] && cmd+=(--realm  "$rlm")
        [[ -n "$srv" ]] && cmd+=(--server "$srv")
        [[ -n "$hst" ]] && cmd+=(--hostname "$hst")
        [[ -n "$ntp" ]] && cmd+=(--ntp-server "$ntp")
        cmd+=(--principal "$pri" --password "$pwd")
        $mkhomedir && cmd+=(--mkhomedir) || cmd+=(--no-mkhomedir)
        $sudo_     && cmd+=(--enable-dns-updates) || true
        ! $dns     && cmd+=(--no-dns-update) || true
        exec "${cmd[@]}"
    fi
}

# Unattended mode (answerfile-driven install, no TTY): the interactive dialog
# TUI below would hang forever with no one to answer it. Auto-enroll from the
# default answerfile if the server already provisioned one, otherwise skip
# cleanly — packages are installed and the module can be re-run manually later.
if [[ "${MARCHY_UNATTENDED:-0}" == "1" || ! -t 0 ]]; then
    if [[ -f "$DEFAULT_AF" ]]; then
        echo "[+] Unattended mode — enrolling from $DEFAULT_AF"
        run_enroll --answerfile "$DEFAULT_AF"
    else
        echo "[!] Unattended mode — no answerfile at $DEFAULT_AF, skipping enrollment."
        echo "    Enroll later with: $CLIENT_SCRIPT --answerfile $DEFAULT_AF"
    fi
    exit 0
fi

# Ensure dialog is available for the TUI; install it on-the-fly if missing.
command -v dialog &>/dev/null || pacman -S --noconfirm --needed dialog

# ── Dialog theme ──────────────────────────────────────────────────────────────
# Apply the same magenta-on-black cyberqueer colour scheme used throughout
# the dotfiles TUI installers so all dialog screens look consistent.
# We only write a temporary dialogrc if one isn't already set in the environment.
if [[ -z "${DIALOGRC:-}" ]] || [[ ! -f "${DIALOGRC:-/dev/null}" ]]; then
    _TMP_D=$(mktemp -d)
    trap 'rm -rf "$_TMP_D"' EXIT
    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)
tag_key_color         = (CYAN,BLACK,ON)
tag_key_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
fi

BT="FreeIPA Client Setup"
T=$(mktemp); trap 'rm -f "$T"' EXIT

# ── dialog helper wrappers ────────────────────────────────────────────────────
# These thin wrappers normalise the fd-swap trick needed to capture dialog
# output (dialog writes to stderr, so we swap stdout/stderr with 3>&1 1>&2 2>&3).
d()    { dialog --backtitle "$BT" "$@" 3>&1 1>&2 2>&3; }
input(){ d --title " $1 " --inputbox "$2" 10 64 "$3"; }
pass() { d --title " $1 " --passwordbox "$2" 10 64; }
yn()   { d --title " $1 " --yesno "$2" 7 64; }
msg()  { d --title " FreeIPA Client " --msgbox "$1" 10 64; }

# ── Enrollment choice ─────────────────────────────────────────────────────────
# Present three options: answerfile, manual, or skip.
# Default to "skip" if the user presses Escape or cancels.
CHOICE=$(d --title " FreeIPA Client Enrollment " \
    --menu "\n  Packages installed.\n  How would you like to enroll this host?\n" \
    13 64 3 \
    "answerfile" "Use a JSON answerfile" \
    "manual"     "Enter enrollment data manually" \
    "skip"       "Skip — enroll later") || CHOICE="skip"

# ── Answerfile mode ───────────────────────────────────────────────────────────
# The answerfile is a JSON file pre-generated by freeipa-server.sh with domain,
# realm, and server already filled in.  The user only needs to add the password.
if [[ "$CHOICE" == "answerfile" ]]; then
    AF_PATH=$(input "Answerfile path" \
        "Path to the JSON answerfile:" \
        "$DEFAULT_AF") || { msg "  Enrollment cancelled."; exit 0; }

    # Use the default path if the user pressed Enter without typing a path
    [[ -z "$AF_PATH" ]] && AF_PATH="$DEFAULT_AF"

    if [[ ! -f "$AF_PATH" ]]; then
        # Offer to create a template answerfile so the user can fill it in
        if yn "Create answerfile" \
            "  '$AF_PATH' does not exist.\n  Create it with default values?"; then
            mkdir -p "$(dirname "$AF_PATH")"
            cat > "$AF_PATH" <<AFEOF
{
  "domain":      "$DEF_DOMAIN",
  "realm":       "$DEF_REALM",
  "server":      "$DEF_SERVER",
  "hostname":    "",
  "principal":   "admin",
  "password":    "",
  "mkhomedir":   true,
  "sudo":        true,
  "dns_update":  true,
  "ntp_server":  "",
  "fido2":       false,
  "fido2_users": []
}
AFEOF
            msg "  Created: $AF_PATH\n\n  Edit it to match your environment,\n  then re-run the FreeIPA Client module to enroll."
            exit 0
        else
            msg "  Provide an existing answerfile and re-run."
            exit 0
        fi
    fi

    # Clear the dialog UI before running ipa-client-install (which prints to stdout)
    clear
    run_enroll --answerfile "$AF_PATH"
fi

# ── Manual mode ───────────────────────────────────────────────────────────────
# Collect all enrolment parameters interactively via dialog prompts.
if [[ "$CHOICE" == "manual" ]]; then
    DOMAIN=$(input "IPA Domain" "FreeIPA domain:" "$DEF_DOMAIN") \
        || { msg "  Enrollment cancelled."; exit 0; }
    DOMAIN="${DOMAIN:-$DEF_DOMAIN}"

    # Kerberos realm is conventionally the domain uppercased; pre-fill that guess
    DEF_REALM_CALC="${DOMAIN^^}"
    REALM=$(input "Kerberos Realm" "Kerberos realm (usually domain uppercased):" \
        "$DEF_REALM_CALC") || REALM="$DEF_REALM_CALC"
    REALM="${REALM:-$DEF_REALM_CALC}"

    SERVER=$(input "IPA Server" "FreeIPA server FQDN:" "$DOMAIN") \
        || { msg "  Enrollment cancelled."; exit 0; }
    SERVER="${SERVER:-$DOMAIN}"

    # Default to the current machine's FQDN so the user can just press Enter
    DEF_HOST=$(hostname -f 2>/dev/null || hostname)
    HOST=$(input "This Host FQDN" "Hostname to register (leave blank = current):" \
        "$DEF_HOST") || HOST="$DEF_HOST"

    PRINCIPAL=$(input "Admin Principal" "IPA admin principal:" "admin") || PRINCIPAL="admin"
    PRINCIPAL="${PRINCIPAL:-admin}"

    # Use passwordbox so the password is not echoed to the terminal
    PASSWORD=$(pass "Admin Password" "Password for $PRINCIPAL@$REALM:") \
        || { msg "  Enrollment cancelled."; exit 0; }
    [[ -z "$PASSWORD" ]] && { msg "  Password is required."; exit 1; }

    NTP=$(input "NTP Server" "NTP server (blank to skip):" "") || NTP=""

    ARGS=(
        --domain    "$DOMAIN"
        --realm     "$REALM"
        --server    "$SERVER"
        --hostname  "$HOST"
        --principal "$PRINCIPAL"
        --password  "$PASSWORD"
    )
    [[ -n "$NTP" ]] && ARGS+=(--ntp-server "$NTP")

    # Optional features: each yes/no dialog adds or omits the corresponding flag
    yn "Home Directories" "  Auto-create home directories on first login?" \
        && true || ARGS+=(--no-mkhomedir)

    yn "Sudo via SSSD"   "  Configure sudo rules via SSSD?" \
        && true || ARGS+=(--no-sudo)

    yn "DNS Update"      "  Register this host's IP in IPA DNS?" \
        && true || ARGS+=(--no-dns-update)

    # FIDO2/WebAuthn hardware token support (requires libfido2 + pamu2fcfg)
    if yn "FIDO2" "  Enable FIDO2/WebAuthn authentication?"; then
        ARGS+=(--fido2)
        FIDO2_USERS=$(input "FIDO2 Users" \
            "Usernames to enable FIDO2 for (comma-separated, blank = all):" "") \
            || FIDO2_USERS=""
        if [[ -n "$FIDO2_USERS" ]]; then
            # Split comma-separated list and strip whitespace from each username
            IFS=',' read -ra _U <<< "$FIDO2_USERS"
            for u in "${_U[@]}"; do
                u="${u// /}"
                [[ -n "$u" ]] && ARGS+=(--fido2-user "$u")
            done
        fi
    fi

    clear
    run_enroll "${ARGS[@]}"
fi

# ── Skip ──────────────────────────────────────────────────────────────────────
# Packages are installed; print instructions for later manual enrolment.
echo ""
echo "[✓] FreeIPA client packages installed."
echo ""
echo "    To enroll later, run one of:"
if [[ -x "$CLIENT_SCRIPT" ]]; then
    echo "      $CLIENT_SCRIPT --interactive"
    echo "      $CLIENT_SCRIPT --answerfile $DEFAULT_AF"
else
    echo "      ipa-client-install --domain=<domain> --server=<server> --principal=admin"
fi
echo ""
echo "    After enrollment:"
echo "      authselect select sssd with-mkhomedir --force"
