#!/bin/bash
# simple-install.sh — TUI installer for the_miro's Arch dotfiles


# -u: error on unset variables; -o pipefail: a pipe fails if any stage fails.
# Intentionally omits -e so individual module failures can be handled explicitly.
set -uo pipefail

# ── Paths ─────────────────────────────────────────────────────────────────────
# Resolve the installer's own directory regardless of where it was invoked from.
# BASH_SOURCE[0] remains correct even when the script is sourced.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Dotfiles root is one level above setup/.
DOTFILES_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
MODULES="$DOTFILES_DIR/setup/modules"
APPS="$MODULES/optional-Modules/apps"
LOG="$HOME/dotfiles-install.log"
# Private scratch space for temporary files (e.g. a transient colors.conf).
# mktemp -d creates a unique directory visible only to this process.
TMP_D="$(mktemp -d)"
# Cleanup trap: remove scratch dir and restore terminal state on any exit signal.
# tput reset issues a full terminal reset; stty sane is the bare-console fallback.
# '|| true' ensures the trap never masks the real process exit code.
trap 'rm -rf "$TMP_D"; tput reset 2>/dev/null || stty sane 2>/dev/null || true' EXIT INT TERM HUP

# Allow callers to inject an alternative answerfile via the ANSWERFILE env var.
# /answerfile.json is a conventional location for CI/PXE unattended installs.
ANSWERFILE="${ANSWERFILE:-/answerfile.json}"
ANSWERFILE_MODE=false
# Activate unattended mode only if the answerfile is present on disk.
[[ -f "$ANSWERFILE" ]] && ANSWERFILE_MODE=true

TITLE="the_miro's Arch Dotfiles"

# ── Terminal dimensions ───────────────────────────────────────────────────────
# tput may fail on a bare TTY with no TERM set; fall back to safe 80×24 defaults.
TERM_H=$(tput lines 2>/dev/null || echo 24)
TERM_W=$(tput cols  2>/dev/null || echo 80)
# Cap the TUI width at 72 columns but shrink to fit narrow terminals.
# Leaving a 2-column margin (TERM_W - 2) prevents line wrapping on small screens.
TUI_W=$(( TERM_W < 74 ? TERM_W - 2 : 72 ))
# Build a horizontal rule string of exactly TUI_W '─' characters.
# printf -v assigns the result to TUI_BAR without printing it.
# '%.0s─' prints zero characters from the argument then appends '─', repeated
# once per number from seq, producing TUI_W copies of the character.
printf -v TUI_BAR '%.0s─' $(seq 1 "$TUI_W")

# ── ANSI color codes ──────────────────────────────────────────────────────────
# Short variable names to keep printf format strings readable.
# R=reset, B=bold, D=dim, M=magenta+bold, CY=cyan+bold.
# $'\e[...' is the ANSI CSI escape sequence; 0m resets all attributes.
R=$'\e[0m'; B=$'\e[1m'; D=$'\e[2m'; M=$'\e[1;35m'; CY=$'\e[1;36m'

# ── TUI primitives ────────────────────────────────────────────────────────────
# All display goes to /dev/tty so functions work correctly inside $() captures.
# If output went to stdout, the calling $() subshell would swallow the UI text
# along with the return value, making the screen appear blank.

_hdr() {
    # \e[2J clears the screen; \e[H moves the cursor to the top-left.
    # The box is drawn using TUI_BAR (pre-built ─ string) and box-drawing chars.
    # %-*s with width (TUI_W - 4) left-pads the title to fill the box exactly.
    printf '\e[2J\e[H' >/dev/tty
    printf "${M}  ┌%s┐${R}\n  ${M}│  ${B}%-*s${R}  ${M}│${R}\n  ${M}└%s┘${R}\n\n" \
        "$TUI_BAR" $(( TUI_W - 4 )) "$TITLE" "$TUI_BAR" >/dev/tty
}

_sep() { printf "  ${D}─────────────────────────────────────────────${R}\n\n" >/dev/tty; }

# tui_msg TITLE MSG
tui_msg() {
    _hdr
    printf "  ${M}${B}%s${R}\n" "$1" >/dev/tty; _sep
    # %b interprets escape sequences in MSG so callers can embed \n for newlines.
    printf "%b\n\n" "$2" >/dev/tty
    # Blocks until Enter; 'read -r </dev/tty' reads from the terminal even when
    # the function is called inside a $() subshell that has stdin redirected.
    printf "  ${D}Press Enter to continue...${R}" >/dev/tty; read -r </dev/tty
}

# tui_yesno TITLE MSG  → 0=yes 1=no
tui_yesno() {
    _hdr
    printf "  ${M}${B}%s${R}\n" "$1" >/dev/tty; _sep
    printf "%b\n\n" "$2" >/dev/tty
    local a
    while true; do
        printf "  ${CY}[Y/n]${R} > " >/dev/tty; read -r a </dev/tty
        # ${a,,} lowercases the input; empty input (bare Enter) defaults to "yes"
        # to follow the conventional [Y/n] prompt behaviour.
        case "${a,,}" in
            y|yes|"") return 0 ;;
            n|no)     return 1 ;;
            *)        printf "  Enter Y or N.\n" >/dev/tty ;;
        esac
    done
}

# tui_input TITLE PROMPT [DEFAULT]  → prints value to stdout
tui_input() {
    _hdr
    printf "  ${M}${B}%s${R}\n" "$1" >/dev/tty; _sep
    printf "  %b\n" "$2" >/dev/tty
    [[ -n "${3:-}" ]] && printf "  ${D}(default: %s)${R}\n" "$3" >/dev/tty
    printf "\n  > " >/dev/tty; local v; read -r v </dev/tty
    # If the user pressed Enter without typing, fall back to the DEFAULT argument.
    # The nested :- handles both an empty input and a missing DEFAULT gracefully.
    printf '%s' "${v:-${3:-}}"
}

# tui_checklist TITLE PROMPT  tag desc state  tag desc state ...
# state: "on" | "off" | "header"  (header = section label, tag is ignored)
# Prints space-separated selected tags to stdout.
# Arrow keys navigate · Space toggles · a selects all · Enter/n confirms
tui_checklist() {
    local title="$1" prompt="$2"; shift 2
    # Parse triplets (tag, description, state) into three parallel arrays.
    # Using parallel arrays avoids nested data structures (unavailable in bash 4).
    local -a _T _D _S
    while [[ $# -ge 3 ]]; do _T+=("$1"); _D+=("$2"); _S+=("$3"); shift 3; done
    local n=${#_T[@]}
    local _tv _vp _cur _scr _VIS _k _k2 _k3 _k4 _i _j _f _l _chk _res _toti _posi i

    # _vp = number of visible list rows; reserve 11 rows for header, prompt, and status bar.
    _tv=$(tput lines 2>/dev/null || echo 24)
    _vp=$(( _tv - 11 ))
    (( _vp < 4 )) && _vp=4

    # Pre-count selectable (non-header) items to display the [N/total] counter.
    _toti=0
    for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" != "header" ]] && (( _toti++ )); done

    # Initialise cursor to the first non-header item so the cursor is never
    # parked on an unselectable section label.
    _cur=0
    for (( i=0; i<n; i++ )); do
        [[ "${_S[$i]}" != "header" ]] && { _cur=$i; break; }
    done
    _scr=0; _VIS=0

    # Count visual lines from index _s up to (not including) _e; result in _VIS.
    # Headers take 1 line when at the top of the viewport, 2 lines otherwise
    # (a blank separator line is printed before each non-first header).
    __cl_vis() {
        local _s=$1 _e=$2; _VIS=0
        for (( i=_s; i<_e && i<n; i++ )); do
            if [[ "${_S[$i]}" == "header" ]]; then
                (( i == _s )) && (( _VIS++ )) || (( _VIS+=2 ))
            else
                (( _VIS++ ))
            fi
        done
    }

    # Ensure _cur is within the visible viewport; adjust _scr (scroll offset) if needed.
    # Scrolls _scr forward one step at a time until _cur fits within _vp visual lines.
    __cl_sync() {
        (( _cur < _scr )) && _scr=$_cur
        while true; do
            __cl_vis $_scr $(( _cur + 1 ))
            (( _VIS <= _vp )) && break
            (( _scr < n-1 )) || break
            (( _scr++ ))
        done
    }

    __cl_draw() {
        _hdr
        printf "  ${M}${B}%s${R}\n" "$title" >/dev/tty
        _sep
        printf "  ${D}%s${R}\n\n" "$prompt" >/dev/tty

        _l=0
        for (( i=_scr; i<n; i++ )); do
            if [[ "${_S[$i]}" == "header" ]]; then
                if (( i == _scr )); then
                    # First visible item is a header: print it without a leading blank line.
                    (( _l + 1 > _vp )) && break
                    printf "  ${M}── %s${R}\n" "${_D[$i]}" >/dev/tty
                    (( _l++ ))
                else
                    # Non-first header: add a blank separator line before it (costs 2 rows).
                    (( _l + 2 > _vp )) && break
                    printf "\n  ${M}── %s${R}\n" "${_D[$i]}" >/dev/tty
                    (( _l+=2 ))
                fi
            else
                (( _l + 1 > _vp )) && break
                # _chk is the checkbox fill: '*' when selected, ' ' when not.
                _chk=" "; [[ "${_S[$i]}" == "on" ]] && _chk="*"
                if (( i == _cur )); then
                    # Highlight the cursor row with cyan arrow and colour.
                    printf "  ${CY}▶ [%s] %-22s${R} %s\n" "$_chk" "${_T[$i]}" "${_D[$i]}" >/dev/tty
                else
                    printf "    [%s] %-22s %s\n" "$_chk" "${_T[$i]}" "${_D[$i]}" >/dev/tty
                fi
                (( _l++ ))
            fi
        done

        # Compute the 1-based position of the cursor among selectable items for
        # the status bar counter, e.g. "[7/42]".
        _posi=0
        for (( i=0; i<_cur; i++ )); do [[ "${_S[$i]}" != "header" ]] && (( _posi++ )); done
        (( _posi++ ))
        printf "\n  ${D}↑↓ navigate  Space toggle  a all  Enter/n confirm  [%d/%d]${R}\n" \
            "$_posi" "$_toti" >/dev/tty
    }

    while true; do
        __cl_sync
        __cl_draw

        # Read a keypress: -r disables backslash interpretation, -s suppresses echo,
        # -n1 reads exactly one byte at a time.  IFS= prevents whitespace trimming.
        IFS= read -rsn1 _k </dev/tty
        if [[ "$_k" == $'\e' ]]; then
            # Escape is the lead byte of multi-byte arrow/page key sequences (ESC [ X).
            # Read up to three more bytes with a short timeout (-t 0.05 s) so a bare
            # Escape key press doesn't hang waiting for bytes that will never arrive.
            IFS= read -rsn1 -t 0.05 _k2 </dev/tty
            if [[ "$_k2" == '[' ]]; then
                IFS= read -rsn1 -t 0.05 _k3 </dev/tty
                if [[ "$_k3" =~ [0-9] ]]; then
                    # Four-byte sequence (e.g. Page Up ESC[5~, Page Down ESC[6~).
                    IFS= read -rsn1 -t 0.05 _k4 </dev/tty
                    _k="${_k}${_k2}${_k3}${_k4}"
                else
                    # Three-byte sequence (e.g. Up ESC[A, Down ESC[B, Home ESC[H, End ESC[F).
                    _k="${_k}${_k2}${_k3}"
                fi
            else
                _k="${_k}${_k2}"
            fi
        fi

        case "$_k" in
            $'\e[A')  # Up arrow
                for (( _i=_cur-1; _i>=0; _i-- )); do
                    [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
                done ;;
            $'\e[B')  # Down arrow
                for (( _i=_cur+1; _i<n; _i++ )); do
                    [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
                done ;;
            $'\e[5~')  # Page Up — jump half a viewport upward
                for (( _j=0; _j < _vp/2; _j++ )); do
                    _f=0
                    for (( _i=_cur-1; _i>=0; _i-- )); do
                        [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; _f=1; break; }
                    done
                    # _f=0 means we hit the top; stop the outer loop early.
                    (( _f == 0 )) && break
                done ;;
            $'\e[6~')  # Page Down — jump half a viewport downward
                for (( _j=0; _j < _vp/2; _j++ )); do
                    _f=0
                    for (( _i=_cur+1; _i<n; _i++ )); do
                        [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; _f=1; break; }
                    done
                    (( _f == 0 )) && break
                done ;;
            $'\e[H')  # Home — jump to first selectable item; also reset scroll
                for (( _i=0; _i<n; _i++ )); do
                    [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
                done; _scr=0 ;;
            $'\e[F')  # End — jump to last selectable item
                for (( _i=n-1; _i>=0; _i-- )); do
                    [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; }
                done ;;
            ' ')  # Space — toggle current item between on/off
                [[ "${_S[$_cur]}" == "on" ]] && _S[$_cur]="off" || _S[$_cur]="on" ;;
            'a')  # Select all selectable items at once
                for (( _i=0; _i<n; _i++ )); do
                    [[ "${_S[$_i]}" != "header" ]] && _S[$_i]="on"
                done ;;
            ''|'n')  break ;;  # Enter or n — confirm and exit the loop
        esac
    done

    # Collect tags of all "on" items into a space-separated string.
    # "${_res% }" trims the trailing space added by the loop.
    _res=""
    for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" == "on" ]] && _res+="${_T[$i]} "; done
    printf '%s' "${_res% }"
}

# tui_menu TITLE PROMPT  tag desc  tag desc ...
# Prints selected tag to stdout; loops until a valid number is entered.
tui_menu() {
    local title="$1" prompt="$2"; shift 2
    local -a _T _D
    while [[ $# -ge 2 ]]; do _T+=("$1"); _D+=("$2"); shift 2; done
    local n=${#_T[@]}
    while true; do
        _hdr
        printf "  ${M}${B}%s${R}\n" "$title" >/dev/tty; _sep
        printf "  ${D}%s${R}\n\n" "$prompt" >/dev/tty
        local i
        for (( i=0; i<n; i++ )); do
            printf "  %3d) %-22s %s\n" $(( i+1 )) "${_T[$i]}" "${_D[$i]}" >/dev/tty
        done
        printf "\n  ${D}Enter number:${R}\n  > " >/dev/tty
        local inp; read -r inp </dev/tty
        if [[ "$inp" =~ ^[0-9]+$ ]] && (( inp >= 1 && inp <= n )); then
            printf '%s' "${_T[$(( inp-1 ))]}"; return 0
        fi
        printf "  Invalid selection.\n" >/dev/tty; sleep 0.4
    done
}

# ── State ─────────────────────────────────────────────────────────────────────
STEP=0
TOTAL=0

# ── Helpers ───────────────────────────────────────────────────────────────────
require_jq() {
    command -v jq &>/dev/null && return
    echo "jq not found — installing..."
    sudo pacman -S --noconfirm jq || { echo "Failed to install jq."; exit 1; }
}

die() {
    clear
    printf "\n  Error: %s\n\n" "$1" >&2
    exit 1
}

log_sep() {
    printf "\n══════════════════════════════════\n  %s\n  %s\n" "$1" "$(date)" >> "$LOG"
}

run_module() {
    local label="$1" script="$2"
    STEP=$(( STEP + 1 ))
    log_sep "[$STEP/$TOTAL] $label"

    clear
    printf "\n${M}  [$STEP/$TOTAL] %s${R}\n" "$label"
    printf "${D}  ─────────────────────────────────────────────${R}\n\n"

    local rc=0
    bash "$script" 2>&1 | tee -a "$LOG" || rc=${PIPESTATUS[0]}

    if [[ $rc -ne 0 ]]; then
        if [[ $ANSWERFILE_MODE == true ]]; then
            printf "\n  Warning: %s exited with code %d — continuing.\n" "$label" "$rc" | tee -a "$LOG"
        else
            tui_yesno " Module Failed " \
                "  $label exited with code $rc.\n\n  Continue anyway?" \
                || { clear; exit 1; }
        fi
    fi
}

count_steps() {
    local c="$1" de="$2" a="${3:-}"
    TOTAL=0
    [[ "$c" == *"pkg"*   ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$c" == *"core"*  ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$c" == *"svc"*   ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$c" == *"shell"* ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$de" != "none" ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"ollama"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"llama-cpp"*      ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"open-webui"*     ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"claude"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"networking-cli"* ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"disk-recovery"*  ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"himalaya"*       ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"gnuplot"*        ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"blender-povray"*  ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"toot"*           ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"db-clients"*     ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"mysql"*          ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"productivity"*   ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"yt-dlp"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"sox"*            ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"imagemagick"*    ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"ffmpeg"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"localtunnel"*    ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"butter"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"tlp"*            ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"steam"*          ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"vesktop"*        ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"spotify"*        ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"prism"*          ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"vintagestory"*   ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"openarena"*      ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"tetris"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"doom"*           ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"sauerbraten"*    ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"stuntrally"*     ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"localsend"*      ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"croc"*           ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"onlyoffice"*     ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"wireshark"*      ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"k8s"*            ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"docker"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"podman"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"cockpit"*        ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"ssh-server"*     ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"freeipa-client"*  ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"freeipa-server"*  ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"freeipa-image"*   ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"python"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"zfs"*            ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"wprs"*           ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"chromium"*       ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"firefox-browser"* ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"zen-browser"*    ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"nyxt"*           ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"librewolf"*      ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"min-browser"*    ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"vscodium"*       ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"zed-ide"*        ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"geany"*          ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"codeblocks"*     ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"kate"*           ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"rdp-client"*       ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"lamco-rdp-server"* ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"qemu"*             ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"xournal"*          ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"gimp"*             ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"inkscape"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"krita"*            ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"ardour"*           ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"audacity"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"lmms"*             ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"mixxx"*            ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"cecilia"*          ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"kdenlive"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"openshot"*         ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"shotcut"*          ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"anti-malware"*     ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"timeshift"*        ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"mail-notmuch"*     ]] && TOTAL=$(( TOTAL + 1 ))
    [[ "$a" == *"caldav-sync"*      ]] && TOTAL=$(( TOTAL + 1 ))
}

# ── Answerfile ────────────────────────────────────────────────────────────────
AF_HOSTNAME=""
AF_COMPONENTS=""
AF_DE="none"
AF_APPS=""
AF_COLOR_TEXT=""
AF_COLOR_BG=""
AF_COLOR_HIGHLIGHT=""
AF_COLOR_DARK=""
AF_COLOR_RED=""

load_answerfile() {
    require_jq
    AF_HOSTNAME=$(jq -r '.hostname // ""' "$ANSWERFILE")
    AF_COMPONENTS=$(jq -r '(.components // []) | join(" ")' "$ANSWERFILE")
    AF_DE=$(jq -r '.desktop_environment // "none"' "$ANSWERFILE")
    AF_APPS=$(jq -r '(.apps // []) | join(" ")' "$ANSWERFILE")
    AF_COLOR_TEXT=$(jq -r '.colors.COLOR_TEXT // ""' "$ANSWERFILE")
    AF_COLOR_BG=$(jq -r '.colors.COLOR_BG // ""' "$ANSWERFILE")
    AF_COLOR_HIGHLIGHT=$(jq -r '.colors.COLOR_HIGHLIGHT // ""' "$ANSWERFILE")
    AF_COLOR_DARK=$(jq -r '.colors.COLOR_DARK // ""' "$ANSWERFILE")
    AF_COLOR_RED=$(jq -r '.colors.COLOR_RED // ""' "$ANSWERFILE")
}

# ── MAC address helper ────────────────────────────────────────────────────────
get_mac_suffix() {
    local mac
    mac=$(ip link show 2>/dev/null \
        | awk '/^[0-9]+: [^l][^o]/{iface=1} iface && /link\/ether/{print $2; iface=0; exit}')
    printf '%s' "${mac//:/}"
}

# ── Preflight ─────────────────────────────────────────────────────────────────
if [[ $EUID -eq 0 ]]; then
    # Root context (e.g. archiso chroot): shim sudo as a passthrough
    mkdir -p "$TMP_D/bin"
    printf '#!/bin/bash\nexec "$@"\n' > "$TMP_D/bin/sudo"
    chmod +x "$TMP_D/bin/sudo"
    export PATH="$TMP_D/bin:$PATH"
fi
command -v pacman &>/dev/null || die "pacman not found — Arch Linux required."

if $ANSWERFILE_MODE; then
    load_answerfile
    printf "Answerfile mode: %s\n" "$ANSWERFILE" | tee -a "$LOG"
fi

# ── Network check ─────────────────────────────────────────────────────────────
if ! ping -c1 -W3 archlinux.org &>/dev/null; then
    if $ANSWERFILE_MODE; then
        printf "Warning: no internet connection detected.\n" | tee -a "$LOG"
    else
        tui_msg " No Network Detected " \
            "  No internet connection found.\n\n  Wired: ensure the cable is plugged in.\n  WiFi:  switch to another TTY (Alt+F2) and run: iwctl\n\n  Press Enter here once connected."
        if ! ping -c1 -W3 archlinux.org &>/dev/null; then
            tui_yesno " Still Offline " \
                "  Still no internet connection.\n\n  Packages cannot be downloaded without network access.\n\n  Continue anyway?" \
                || { clear; echo "Aborted — no network."; exit 1; }
        fi
    fi
fi

> "$LOG"
printf "Dotfiles install: %s\nDotfiles dir:    %s\n" "$(date)" "$DOTFILES_DIR" >> "$LOG"

# ── Welcome ───────────────────────────────────────────────────────────────────
if ! $ANSWERFILE_MODE; then
    tui_msg " Welcome " \
        "  the_miro's Arch dotfiles installer\n  Cyberqueer  ·  Wayland  ·  Hyprland\n  ─────────────────────────────────────────\n\n  Arch Linux — network admin, development & gaming\n\n  Source:  $DOTFILES_DIR\n  Log:     $LOG"
fi

# ── Hostname ──────────────────────────────────────────────────────────────────
HOSTNAME_SET=""
if $ANSWERFILE_MODE; then
    if [[ -n "$AF_HOSTNAME" ]]; then
        MAC=$(get_mac_suffix)
        HOSTNAME_SET="${AF_HOSTNAME}-${MAC}"
        printf "Hostname (from answerfile + MAC): %s\n" "$HOSTNAME_SET" | tee -a "$LOG"
    fi
else
    HOSTNAME_SET=$(tui_input " Hostname " \
        "Hostname for this machine (leave blank to keep default)." "")
fi

if [[ -n "$HOSTNAME_SET" ]]; then
    sudo hostnamectl set-hostname "$HOSTNAME_SET" 2>/dev/null \
        || echo "$HOSTNAME_SET" | sudo tee /etc/hostname > /dev/null
    printf "Hostname set: %s\n" "$HOSTNAME_SET" >> "$LOG"
fi

# ── Component selection ───────────────────────────────────────────────────────
if $ANSWERFILE_MODE; then
    COMPONENTS="$AF_COMPONENTS"
else
    COMPONENTS=$(tui_checklist " Select Components " \
        "Select system components to install" \
        "pkg"   "Package managers    yay · nvm · rust"                     on  \
        "core"  "Core packages       100+ base system packages"            on  \
        "svc"   "Core services       NetworkManager · cronie · fail2ban"   on  \
        "shell" "Shell setup         zsh · nvim · yazi · micro · starship" on)
fi

# ── DE selection ──────────────────────────────────────────────────────────────
if $ANSWERFILE_MODE; then
    DE="$AF_DE"
else
    DE=$(tui_menu " Desktop Environment " \
        "Select a desktop environment:" \
        "hyprlua"    "HyprLua    — Hyprland with Lua config (recommended)" \
        "niri"       "Niri       — scrollable-tiling Wayland compositor"  \
        "hyprland"   "Hyprland   — Wayland WM, hyprlang config (legacy)"  \
        "sway"       "Sway       — Wayland tiling WM"                     \
        "kde-plasma" "KDE Plasma — feature-rich Wayland/X11 DE"           \
        "gnome"      "GNOME      — modern Wayland DE"                     \
        "cosmic"     "COSMIC     — Rust-built Wayland DE (System76)"      \
        "xfce"       "XFCE       — lightweight X11 DE"                    \
        "lxqt"       "LXQt       — lightweight Qt X11 DE"                 \
        "none"       "Skip DE installation")
fi

# ── Apps selection ────────────────────────────────────────────────────────────
if $ANSWERFILE_MODE; then
    SELECTED_APPS="$AF_APPS"
else
    SELECTED_APPS=$(tui_checklist " Applications " \
        "Select optional applications to install" \
        \
        ""               "AI / LLM"                                                    header \
        "ollama"         "Ollama            local LLM runner + API server"             off \
        "llama-cpp"      "llama.cpp         standalone inference CLI + server"         off \
        "open-webui"     "Open WebUI        browser UI for Ollama / LLM backends"     off \
        "claude"         "Claude Code       Anthropic CLI via npm"                    off \
        \
        ""               "Networking & Disk"                                            header \
        "networking-cli" "Networking CLI    nmap · nethogs · mitmproxy · httpie"      off \
        "disk-recovery"  "Disk Recovery     ddrescue · f3"                             off \
        \
        ""               "CLI Tools"                                                    header \
        "himalaya"       "Himalaya          terminal email client (AUR)"               off \
        "mail-notmuch"   "Mail (notmuch)    isync · msmtp · notmuch · alot stack"     off \
        "caldav-sync"    "CalDAV Sync       vdirsyncer · khal calendar sync"           off \
        "gnuplot"        "Gnuplot           scientific plotting"                       off \
        "blender-povray" "Blender + POV-Ray  3D modelling & ray-tracing"                off \
        "toot"           "toot              Mastodon CLI client (AUR)"                 off \
        "db-clients"     "DB Clients        pgcli · mycli"                             off \
        "mysql"          "MySQL / MariaDB   mariadb server + setup"                    off \
        "productivity"   "Productivity      taskwarrior · watson · jrnl"               off \
        "yt-dlp"         "yt-dlp            YouTube / media downloader"               off \
        "sox"            "SoX               audio processing toolkit"                  off \
        "imagemagick"    "ImageMagick       image manipulation"                        off \
        "ffmpeg"         "FFmpeg extras     thumbnailer · GStreamer codecs"            off \
        "localtunnel"    "LocalTunnel       expose localhost via tunnel"               off \
        "butter"         "butter            btrfs snapshot backup (AUR)"               off \
        "tlp"            "TLP               laptop power management"                   off \
        \
        ""               "Gaming"                                                       header \
        "steam"          "Steam             gaming platform"                            off \
        "vesktop"        "Vesktop           Discord + Vencord theme"                   off \
        "spotify"        "Spotify           launcher + Spicetify theming"              off \
        "prism"          "PrismLauncher     Minecraft launcher (Flatpak)"              off \
        "vintagestory"   "Vintage Story     survival game (AUR)"                       off \
        "openarena"      "OpenArena         open-source Quake III Arena"                off \
        "tetris"         "Tetris CLI        bastet · vitetris"                          off \
        "doom"           "Doom              Chocolate Doom + Freedoom data"             off \
        "sauerbraten"    "Sauerbraten       open-source FPS (Cube 2)"                  off \
        "stuntrally"     "Stunt Rally       rally racing game (Flatpak)"               off \
        \
        ""               "File Transfer & Office"                                       header \
        "localsend"      "LocalSend         LAN file transfer (AUR)"                   off \
        "croc"           "croc              cross-platform file transfer"              off \
        "onlyoffice"     "OnlyOffice        office suite (AUR)"                       off \
        \
        ""               "Graphics & Design"                                            header \
        "gimp"           "GIMP              GNU image manipulation program"             off \
        "inkscape"       "Inkscape          vector graphics editor"                     off \
        "krita"          "Krita             digital painting application"               off \
        "xournal"        "Xournal++         note-taking & PDF annotator"               off \
        \
        ""               "Audio & Music"                                                header \
        "ardour"         "Ardour            professional DAW"                           off \
        "audacity"       "Audacity          multi-track audio editor"                   off \
        "lmms"           "LMMS              Linux MultiMedia Studio DAW"                off \
        "mixxx"          "Mixxx             DJ mixing software"                         off \
        "cecilia"        "Cecilia           audio signal processing (AUR)"              off \
        \
        ""               "Video Editing"                                                header \
        "kdenlive"       "Kdenlive          KDE non-linear video editor"                off \
        "openshot"       "OpenShot          easy video editor"                          off \
        "shotcut"        "Shotcut           cross-platform video editor"                off \
        \
        ""               "Security"                                                     header \
        "anti-malware"   "Anti-Malware      ClamAV · rkhunter · chkrootkit"            off \
        "timeshift"      "Timeshift         system snapshot / backup + autosnap"        off \
        "wireshark"      "Wireshark         network packet analyser (GUI)"             off \
        \
        ""               "Containers & Server"                                          header \
        "k8s"            "Kubernetes tools  kubectl · podman-desktop"                  off \
        "docker"         "Docker            docker · docker-compose"                   off \
        "podman"         "Podman            rootless containers · buildah"             off \
        "cockpit"        "Cockpit           web UI · machines · podman"               off \
        "ssh-server"     "SSH server        openssh · key-auth · enabled"              off \
        "freeipa-client" "FreeIPA Client    sssd + ipa-client-install + enrollment"   off \
        "freeipa-server" "FreeIPA Server    interactive server setup + client gen"    off \
        "freeipa-image"  "FreeIPA Image     OCI/LXC/Proxmox LXC builder + Keycloak"  off \
        \
        ""               "Dev & System"                                                 header \
        "python"         "Python tools      pyright · pipx · pynvim"                  off \
        "zfs"            "ZFS               zfs-dkms kernel module"                   off \
        "wprs"           "WPRS              wprs-git (AUR)"                            off \
        \
        ""               "Browsers"                                                     header \
        "chromium"        "Chromium          open-source browser (official)"           off \
        "firefox-browser" "Firefox           Mozilla browser (official)"               off \
        "zen-browser"     "Zen Browser       Firefox-based privacy browser (AUR)"      off \
        "nyxt"            "Nyxt              keyboard-driven browser (AUR)"            off \
        "librewolf"       "LibreWolf         hardened Firefox fork (AUR)"              off \
        "min-browser"     "Min               minimal Electron browser (AUR)"           off \
        \
        ""               "IDEs & Editors"                                               header \
        "vscodium"       "VSCodium          telemetry-free VS Code (AUR)"              off \
        "zed-ide"        "Zed               high-performance Rust IDE (official)"      off \
        "geany"          "Geany             lightweight IDE + plugins (official)"      off \
        "codeblocks"     "Code::Blocks      C/C++ IDE (official)"                      off \
        "kate"           "Kate              KDE advanced text editor (official)"       off \
        \
        ""                 "Remote & Virtualization"                                    header \
        "rdp-client"       "RDP Client      Remmina + FreeRDP + VNC plugins"           off \
        "lamco-rdp-server" "Lamco RDP Srv   native Wayland RDP server (AUR, Rust)"    off \
        "qemu"             "QEMU/KVM        full virt stack + virt-manager GUI"        off)
fi

# ── Confirmation (interactive mode only) ──────────────────────────────────────
if ! $ANSWERFILE_MODE; then
    SUMMARY=""
    [[ -n "$HOSTNAME_SET" ]] && SUMMARY+="  ✦ Hostname: $HOSTNAME_SET\n"
    [[ "$COMPONENTS" == *"pkg"*   ]] && SUMMARY+="  ✦ Package managers (yay, nvm, rust)\n"
    [[ "$COMPONENTS" == *"core"*  ]] && SUMMARY+="  ✦ Core packages\n"
    [[ "$COMPONENTS" == *"svc"*   ]] && SUMMARY+="  ✦ Core services\n"
    [[ "$COMPONENTS" == *"shell"* ]] && SUMMARY+="  ✦ Shell setup\n"
    [[ "$DE" != "none" ]] && SUMMARY+="  ✦ Desktop environment: $DE\n"

    if [[ -n "$SELECTED_APPS" ]]; then
        SUMMARY+="\n  Applications:\n"
        for _app in ollama llama-cpp open-webui claude networking-cli disk-recovery \
                    himalaya gnuplot blender-povray toot db-clients mysql productivity \
                    yt-dlp sox imagemagick ffmpeg localtunnel butter tlp steam vesktop \
                    spotify prism vintagestory openarena tetris doom sauerbraten stuntrally localsend croc onlyoffice \
                    gimp inkscape krita xournal ardour audacity lmms mixxx cecilia \
                    kdenlive openshot shotcut \
                    anti-malware timeshift wireshark k8s \
                    docker podman cockpit ssh-server freeipa-client freeipa-server \
                    freeipa-image python zfs wprs chromium firefox-browser zen-browser \
                    nyxt librewolf min-browser vscodium zed-ide geany codeblocks kate \
                    rdp-client lamco-rdp-server qemu; do
            [[ "$SELECTED_APPS" == *"$_app"* ]] && SUMMARY+="  ✦ $_app\n"
        done
    fi

    tui_yesno " Confirm Installation " \
        "  Components to install:\n\n${SUMMARY}\n  Log: $LOG\n\n  Proceed?" \
        || { clear; echo "Aborted."; exit 0; }
fi

count_steps "$COMPONENTS" "$DE" "$SELECTED_APPS"

# ── Installation: base components ─────────────────────────────────────────────
[[ "$COMPONENTS" == *"pkg"*   ]] && run_module "Package Managers" "$MODULES/package-managers.sh"
[[ "$COMPONENTS" == *"core"*  ]] && run_module "Core Packages"    "$MODULES/core-packages.sh"
[[ "$COMPONENTS" == *"svc"*   ]] && run_module "Core Services"    "$MODULES/core.sh"
[[ "$COMPONENTS" == *"shell"* ]] && run_module "Shell Setup"      "$MODULES/shell-setup.sh"

if [[ "$DE" != "none" ]]; then
    case "$DE" in
        hyprlua)    run_module "HyprLua"     "$MODULES/Desktop-Environments/hyprlua.sh" ;;
        niri)       run_module "Niri"        "$MODULES/Desktop-Environments/niri.sh" ;;
        hyprland)   run_module "Hyprland"    "$MODULES/Desktop-Environments/hyprland.sh" ;;
        sway)       run_module "Sway"        "$MODULES/Desktop-Environments/sway.sh" ;;
        kde-plasma) run_module "KDE Plasma"  "$MODULES/Desktop-Environments/kde-plasma.sh" ;;
        gnome)      run_module "GNOME"       "$MODULES/Desktop-Environments/gnome.sh" ;;
        cosmic)     run_module "COSMIC"      "$MODULES/Desktop-Environments/cosmic.sh" ;;
        xfce)       run_module "XFCE"        "$MODULES/Desktop-Environments/xfce.sh" ;;
        lxqt)       run_module "LXQt"        "$MODULES/Desktop-Environments/lxqt.sh" ;;
    esac
fi

# ── Installation: applications ────────────────────────────────────────────────
[[ "$SELECTED_APPS" == *"ollama"*         ]] && run_module "Ollama"            "$APPS/ollama.sh"
[[ "$SELECTED_APPS" == *"llama-cpp"*      ]] && run_module "llama.cpp"         "$APPS/llama-cpp.sh"
[[ "$SELECTED_APPS" == *"open-webui"*     ]] && run_module "Open WebUI"        "$APPS/open-webui.sh"
[[ "$SELECTED_APPS" == *"claude"*         ]] && run_module "Claude Code"       "$APPS/claude.sh"
[[ "$SELECTED_APPS" == *"networking-cli"* ]] && run_module "Networking CLI"    "$APPS/networking-cli.sh"
[[ "$SELECTED_APPS" == *"disk-recovery"*  ]] && run_module "Disk Recovery"     "$APPS/disk-recovery.sh"
[[ "$SELECTED_APPS" == *"himalaya"*       ]] && run_module "Himalaya"          "$APPS/himalaya.sh"
[[ "$SELECTED_APPS" == *"mail-notmuch"*   ]] && run_module "Mail (notmuch)"   "$APPS/mail-notmuch.sh"
[[ "$SELECTED_APPS" == *"caldav-sync"*    ]] && run_module "CalDAV Sync"      "$APPS/caldav-sync.sh"
[[ "$SELECTED_APPS" == *"gnuplot"*        ]] && run_module "Gnuplot"           "$APPS/gnuplot.sh"
[[ "$SELECTED_APPS" == *"blender-povray"*  ]] && run_module "Blender + POV-Ray" "$APPS/blender-povray.sh"
[[ "$SELECTED_APPS" == *"toot"*           ]] && run_module "toot"              "$APPS/toot.sh"
[[ "$SELECTED_APPS" == *"db-clients"*     ]] && run_module "DB Clients"        "$APPS/db-clients.sh"
[[ "$SELECTED_APPS" == *"mysql"*          ]] && run_module "MySQL / MariaDB"   "$APPS/mysql.sh"
[[ "$SELECTED_APPS" == *"productivity"*   ]] && run_module "Productivity"      "$APPS/productivity.sh"
[[ "$SELECTED_APPS" == *"yt-dlp"*         ]] && run_module "yt-dlp"           "$APPS/yt-dlp.sh"
[[ "$SELECTED_APPS" == *"sox"*            ]] && run_module "SoX"              "$APPS/sox.sh"
[[ "$SELECTED_APPS" == *"imagemagick"*    ]] && run_module "ImageMagick"       "$APPS/imagemagick.sh"
[[ "$SELECTED_APPS" == *"ffmpeg"*         ]] && run_module "FFmpeg extras"     "$APPS/ffmpeg.sh"
[[ "$SELECTED_APPS" == *"localtunnel"*    ]] && run_module "LocalTunnel"       "$APPS/localtunnel.sh"
[[ "$SELECTED_APPS" == *"butter"*         ]] && run_module "butter"            "$APPS/butter.sh"
[[ "$SELECTED_APPS" == *"tlp"*            ]] && run_module "TLP"              "$APPS/tlp.sh"
[[ "$SELECTED_APPS" == *"steam"*          ]] && run_module "Steam"             "$APPS/steam.sh"
[[ "$SELECTED_APPS" == *"vesktop"*        ]] && run_module "Vesktop"           "$APPS/vesktop.sh"
[[ "$SELECTED_APPS" == *"spotify"*        ]] && run_module "Spotify"           "$APPS/spotify.sh"
[[ "$SELECTED_APPS" == *"prism"*          ]] && run_module "PrismLauncher"     "$APPS/prismlauncher.sh"
[[ "$SELECTED_APPS" == *"vintagestory"*   ]] && run_module "Vintage Story"     "$APPS/vintagestory.sh"
[[ "$SELECTED_APPS" == *"openarena"*      ]] && run_module "OpenArena"         "$APPS/openarena.sh"
[[ "$SELECTED_APPS" == *"tetris"*         ]] && run_module "Tetris CLI"        "$APPS/tetris.sh"
[[ "$SELECTED_APPS" == *"doom"*           ]] && run_module "Doom"              "$APPS/doom.sh"
[[ "$SELECTED_APPS" == *"sauerbraten"*    ]] && run_module "Sauerbraten"       "$APPS/sauerbraten.sh"
[[ "$SELECTED_APPS" == *"stuntrally"*     ]] && run_module "Stunt Rally"       "$APPS/stuntrally.sh"
[[ "$SELECTED_APPS" == *"localsend"*      ]] && run_module "LocalSend"         "$APPS/localsend.sh"
[[ "$SELECTED_APPS" == *"croc"*           ]] && run_module "croc"              "$APPS/croc.sh"
[[ "$SELECTED_APPS" == *"onlyoffice"*     ]] && run_module "OnlyOffice"        "$APPS/onlyoffice.sh"
[[ "$SELECTED_APPS" == *"gimp"*           ]] && run_module "GIMP"              "$APPS/gimp.sh"
[[ "$SELECTED_APPS" == *"inkscape"*       ]] && run_module "Inkscape"          "$APPS/inkscape.sh"
[[ "$SELECTED_APPS" == *"krita"*          ]] && run_module "Krita"             "$APPS/krita.sh"
[[ "$SELECTED_APPS" == *"xournal"*        ]] && run_module "Xournal++"         "$APPS/xournal.sh"
[[ "$SELECTED_APPS" == *"ardour"*         ]] && run_module "Ardour"            "$APPS/ardour.sh"
[[ "$SELECTED_APPS" == *"audacity"*       ]] && run_module "Audacity"          "$APPS/audacity.sh"
[[ "$SELECTED_APPS" == *"lmms"*           ]] && run_module "LMMS"              "$APPS/lmms.sh"
[[ "$SELECTED_APPS" == *"mixxx"*          ]] && run_module "Mixxx"             "$APPS/mixxx.sh"
[[ "$SELECTED_APPS" == *"cecilia"*        ]] && run_module "Cecilia"           "$APPS/cecilia.sh"
[[ "$SELECTED_APPS" == *"kdenlive"*       ]] && run_module "Kdenlive"          "$APPS/kdenlive.sh"
[[ "$SELECTED_APPS" == *"openshot"*       ]] && run_module "OpenShot"          "$APPS/openshot.sh"
[[ "$SELECTED_APPS" == *"shotcut"*        ]] && run_module "Shotcut"           "$APPS/shotcut.sh"
[[ "$SELECTED_APPS" == *"anti-malware"*   ]] && run_module "Anti-Malware"      "$APPS/anti-malware.sh"
[[ "$SELECTED_APPS" == *"timeshift"*      ]] && run_module "Timeshift"         "$APPS/timeshift.sh"
[[ "$SELECTED_APPS" == *"wireshark"*      ]] && run_module "Wireshark"         "$APPS/wireshark.sh"
[[ "$SELECTED_APPS" == *"k8s"*            ]] && run_module "Kubernetes Tools"  "$APPS/k8s.sh"
[[ "$SELECTED_APPS" == *"docker"*         ]] && run_module "Docker"            "$APPS/docker.sh"
[[ "$SELECTED_APPS" == *"podman"*         ]] && run_module "Podman"            "$APPS/podman.sh"
[[ "$SELECTED_APPS" == *"cockpit"*        ]] && run_module "Cockpit"           "$APPS/cockpit.sh"
[[ "$SELECTED_APPS" == *"ssh-server"*     ]] && run_module "SSH Server"        "$APPS/ssh-server.sh"
[[ "$SELECTED_APPS" == *"freeipa-client"* ]] && run_module "FreeIPA Client"    "$APPS/freeipa-client.sh"
[[ "$SELECTED_APPS" == *"freeipa-server"* ]] && run_module "FreeIPA Server"    "$APPS/freeipa-server.sh"
[[ "$SELECTED_APPS" == *"freeipa-image"*  ]] && run_module "FreeIPA Image"     "$APPS/freeipa-image-builder.sh"
[[ "$SELECTED_APPS" == *"python"*         ]] && run_module "Python Tools"      "$MODULES/optional-Modules/python.sh"
[[ "$SELECTED_APPS" == *"zfs"*            ]] && run_module "ZFS"              "$MODULES/optional-Modules/zfs.sh"
[[ "$SELECTED_APPS" == *"wprs"*           ]] && run_module "WPRS"             "$MODULES/optional-Modules/wprs.sh"
[[ "$SELECTED_APPS" == *"chromium"*       ]] && run_module "Chromium"         "$APPS/chromium.sh"
[[ "$SELECTED_APPS" == *"firefox-browser"* ]] && run_module "Firefox"         "$APPS/firefox.sh"
[[ "$SELECTED_APPS" == *"zen-browser"*    ]] && run_module "Zen Browser"      "$APPS/zen-browser.sh"
[[ "$SELECTED_APPS" == *"nyxt"*           ]] && run_module "Nyxt"             "$APPS/nyxt.sh"
[[ "$SELECTED_APPS" == *"librewolf"*      ]] && run_module "LibreWolf"        "$APPS/librewolf.sh"
[[ "$SELECTED_APPS" == *"min-browser"*    ]] && run_module "Min Browser"      "$APPS/min-browser.sh"
[[ "$SELECTED_APPS" == *"vscodium"*       ]] && run_module "VSCodium"         "$APPS/vscodium.sh"
[[ "$SELECTED_APPS" == *"zed-ide"*        ]] && run_module "Zed IDE"          "$APPS/zed.sh"
[[ "$SELECTED_APPS" == *"geany"*          ]] && run_module "Geany"            "$APPS/geany.sh"
[[ "$SELECTED_APPS" == *"codeblocks"*     ]] && run_module "Code::Blocks"     "$APPS/codeblocks.sh"
[[ "$SELECTED_APPS" == *"kate"*           ]] && run_module "Kate"             "$APPS/kate.sh"
[[ "$SELECTED_APPS" == *"rdp-client"*       ]] && run_module "RDP Client"        "$APPS/rdp-client.sh"
[[ "$SELECTED_APPS" == *"lamco-rdp-server"* ]] && run_module "Lamco RDP Server" "$APPS/lamco-rdp-server.sh"
[[ "$SELECTED_APPS" == *"qemu"*             ]] && run_module "QEMU/KVM"          "$APPS/qemu.sh"

# ── Colorway (final step) ─────────────────────────────────────────────────────
declare -A _cdef
if [[ -f "$DOTFILES_DIR/colors.conf" ]]; then
    while IFS='=' read -r k v; do
        k="${k%%[[:space:]]*}"
        [[ "$k" =~ ^[[:space:]]*# || -z "$k" ]] && continue
        v="${v%%#*}"; v="${v//[[:space:]]/}"; v="${v^^}"
        _cdef[$k]="$v"
    done < "$DOTFILES_DIR/colors.conf"
fi
DEF_TEXT="${_cdef[COLOR_TEXT]:-D6ABAB}"
DEF_BG="${_cdef[COLOR_BG]:-1A1A1A}"
DEF_HIGHLIGHT="${_cdef[COLOR_HIGHLIGHT]:-E40046}"
DEF_DARK="${_cdef[COLOR_DARK]:-5018DD}"
DEF_RED="${_cdef[COLOR_RED]:-F50505}"

_write_colors_conf() {
    local out="$1" t="$2" b="$3" h="$4" d="$5" r="$6"
    printf 'COLOR_TEXT=%s\nCOLOR_BG=%s\nCOLOR_HIGHLIGHT=%s\nCOLOR_DARK=%s\nCOLOR_RED=%s\n' \
        "${t^^}" "${b^^}" "${h^^}" "${d^^}" "${r^^}" > "$out"
}

if $ANSWERFILE_MODE; then
    if [[ -n "$AF_COLOR_TEXT$AF_COLOR_BG$AF_COLOR_HIGHLIGHT$AF_COLOR_DARK$AF_COLOR_RED" ]]; then
        TMP_COLORS="$TMP_D/colors.conf"
        _write_colors_conf "$TMP_COLORS" \
            "${AF_COLOR_TEXT:-$DEF_TEXT}" \
            "${AF_COLOR_BG:-$DEF_BG}" \
            "${AF_COLOR_HIGHLIGHT:-$DEF_HIGHLIGHT}" \
            "${AF_COLOR_DARK:-$DEF_DARK}" \
            "${AF_COLOR_RED:-$DEF_RED}"
        printf "Applying colorway from answerfile...\n" | tee -a "$LOG"
        bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true
    fi
else
    _hdr
    printf "  ${M}${B} Colorway (optional) ${R}\n" >/dev/tty; _sep
    printf "  Customize theme colors — bare 6-digit hex, no #.\n  Press Enter to keep each default.\n\n" >/dev/tty

    printf "  COLOR_TEXT      ${D}[%s]${R} > " "$DEF_TEXT"      >/dev/tty; read -r N_TEXT      </dev/tty; N_TEXT="${N_TEXT:-$DEF_TEXT}"
    printf "  COLOR_BG        ${D}[%s]${R} > " "$DEF_BG"        >/dev/tty; read -r N_BG        </dev/tty; N_BG="${N_BG:-$DEF_BG}"
    printf "  COLOR_HIGHLIGHT ${D}[%s]${R} > " "$DEF_HIGHLIGHT" >/dev/tty; read -r N_HIGHLIGHT </dev/tty; N_HIGHLIGHT="${N_HIGHLIGHT:-$DEF_HIGHLIGHT}"
    printf "  COLOR_DARK      ${D}[%s]${R} > " "$DEF_DARK"      >/dev/tty; read -r N_DARK      </dev/tty; N_DARK="${N_DARK:-$DEF_DARK}"
    printf "  COLOR_RED       ${D}[%s]${R} > " "$DEF_RED"       >/dev/tty; read -r N_RED       </dev/tty; N_RED="${N_RED:-$DEF_RED}"

    if [[ "${N_TEXT^^}"      != "$DEF_TEXT"      || \
          "${N_BG^^}"        != "$DEF_BG"         || \
          "${N_HIGHLIGHT^^}"  != "$DEF_HIGHLIGHT"  || \
          "${N_DARK^^}"      != "$DEF_DARK"        || \
          "${N_RED^^}"       != "$DEF_RED" ]]; then
        TMP_COLORS="$TMP_D/colors.conf"
        _write_colors_conf "$TMP_COLORS" "$N_TEXT" "$N_BG" "$N_HIGHLIGHT" "$N_DARK" "$N_RED"
        clear
        printf "\n${M}  Applying colorway...${R}\n\n"
        bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true
    fi
fi

# ── Sync user config to /etc/skel ─────────────────────────────────────────────
# Captures everything installed by modules so future users start with the same setup.
if [[ -d "$HOME/.config" ]]; then
    printf "\n  Syncing ~/.config to /etc/skel...\n"
    sudo mkdir -p /etc/skel/.config
    sudo cp -r "$HOME/.config/." /etc/skel/.config/
fi
[[ -d "$HOME/.themes" ]] && { sudo mkdir -p /etc/skel/.themes; sudo cp -r "$HOME/.themes/." /etc/skel/.themes/; }
[[ -f "$HOME/.zshrc"  ]] && sudo cp "$HOME/.zshrc"  /etc/skel/.zshrc
[[ -f "$HOME/.bashrc" ]] && sudo cp "$HOME/.bashrc" /etc/skel/.bashrc
[[ -f "$HOME/.vimrc"  ]] && sudo cp "$HOME/.vimrc"  /etc/skel/.vimrc

# ── Done ──────────────────────────────────────────────────────────────────────
if $ANSWERFILE_MODE; then
    printf "\nDone. Log: %s\n" "$LOG"
else
    tui_msg " Done " \
        "  All selected components installed.\n\n  Log: $LOG\n\n  A reboot may be required for all changes to take effect."
    clear
    printf "\n  Done. Log: %s\n\n" "$LOG"
fi
