#!/usr/bin/env bash
# arch-autoinstall.sh — automated Arch Linux base installer
#
# If /answerfile.json exists (e.g. embedded in the ISO via build.sh --preconf),
# all prompts are answered from it. Missing fields fall back to interactive prompts.
#
# Answerfile fields: drive, kernel, keymap, hostname, username, encrypt, fido2_root,
#                    fido2_user, run_tui, password, luks_password
# The user "password" (and "luks_password" for encrypted installs) may be supplied
# in the answerfile for fully unattended deployment; when omitted they are prompted
# interactively. Storing secrets in the answerfile is a convenience/secret-handling
# tradeoff — keep the file access-restricted (it is embedded world-readable in the
# ISO by build.sh, so only bake passwords into images you control).

# -E: propagate ERR trap into functions and subshells; -e: abort on any error;
# -u: treat unset variables as errors; -o pipefail: catch failures inside pipes.
set -Eeuo pipefail

############################################
# LOGGING
############################################
LOGFILE="$HOME/arch-autoinstall.log"
{
    echo
    echo "############################################"
    echo " Arch Auto-Install Log - Started $(date)"
    echo "############################################"
    echo
} >> "$LOGFILE"
# Redirect all subsequent stdout and stderr to both the terminal and the log.
# `tee -a` appends so repeated runs accumulate without overwriting; process
# substitution keeps the descriptor open for the full lifetime of the script.
exec > >(tee -a "$LOGFILE") 2>&1

############################################
# Error handler — TUI prompt to send log via croc
############################################
error_handler() {
    # Capture exit code before any other command can overwrite $?.
    # $LINENO is passed as $1 from the trap, so we default to '?' if missing.
    local exit_code=$? line_num="${1:-?}"
    echo "" >> "$LOGFILE"
    echo "ERROR: installer failed at line $line_num (exit code $exit_code)" >> "$LOGFILE"

    # Prefer a dialog TUI if available; fall back to plain readline on headless installs.
    if command -v dialog &>/dev/null; then
        if dialog --clear --title " Installer Error " \
                  --yesno \
                  "Installation failed at line $line_num (exit code: $exit_code).\n\nSend the log to another system via croc for analysis?" \
                  9 62; then
            clear
            # croc may not be present in the base live ISO — install it on demand.
            if ! command -v croc &>/dev/null; then
                echo "Installing croc..."
                pacman -Sy --noconfirm croc 2>/dev/null || true
            fi
            if command -v croc &>/dev/null; then
                croc send "$LOGFILE"
            else
                echo "croc unavailable — log is at: $LOGFILE"
            fi
        else
            clear
            echo "Log saved to: $LOGFILE"
        fi
    else
        echo ""
        echo "Installation failed at line $line_num (exit code $exit_code)."
        read -rp "Send log via croc? [y/N]: " _croc_ans
        # ${_croc_ans,,} lowercases the answer so "Y", "y" both match.
        if [[ "${_croc_ans,,}" == "y" ]]; then
            command -v croc &>/dev/null || pacman -Sy --noconfirm croc 2>/dev/null || true
            croc send "$LOGFILE" || true
        else
            echo "Log saved to: $LOGFILE"
        fi
    fi
    exit "$exit_code"
}
# Fire error_handler on any ERR signal, passing the current line number.
# Single quotes prevent $LINENO from being evaluated at trap-definition time.
trap 'error_handler $LINENO' ERR

############################################
# ANSWERFILE
############################################
# Allow an external override via the environment; default to /answerfile.json
# which is where build.sh --preconf embeds the file in the live ISO.
ANSWERFILE="${ANSWERFILE:-/answerfile.json}"
AF_MODE=false
[[ -f "$ANSWERFILE" ]] && AF_MODE=true

af_get() {
    # af_get <jq-filter> [default]
    # Reads one field from the answerfile with a jq expression.
    # `// empty` returns empty string instead of "null" when the field is absent;
    # the outer `|| true` prevents a jq parse error from aborting the script.
    local val
    val=$(jq -r "${1} // empty" "$ANSWERFILE" 2>/dev/null || true)
    if [[ -z "$val" ]]; then printf '%s' "${2:-}"; else printf '%s' "$val"; fi
}

af_bool() {
    # Returns YES or NO from a JSON boolean field
    # `// false` provides a safe default when the key is absent entirely.
    local val; val=$(jq -r "${1} // false" "$ANSWERFILE" 2>/dev/null || true)
    [[ "$val" == "true" ]] && echo "YES" || echo "NO"
}

get_mac_suffix() {
    # Produce a compact MAC address string (no colons) from the first non-loopback
    # interface to create a unique per-machine hostname suffix at deploy time.
    # The awk pattern skips "lo" by requiring the 3rd char to differ from 'o'.
    local mac
    mac=$(ip link show 2>/dev/null \
        | awk '/^[0-9]+: [^l][^o]/{iface=1} iface && /link\/ether/{print $2; iface=0; exit}')
    # Remove colons via parameter substitution: ${var//pattern/replacement}.
    printf '%s' "${mac//:/}"
}

# Map drive paths to their correct partition suffix.
# NVMe (/dev/nvme0n1) and eMMC (/dev/mmcblk0) use a 'p' before the part number;
# conventional block devices (/dev/sda) do not.
part() { [[ "$1" == *nvme* || "$1" == *mmcblk* ]] && echo "${1}p${2}" || echo "${1}${2}"; }

if $AF_MODE; then
    echo "Answerfile detected: $ANSWERFILE"
    # jq is required to parse the answerfile; it may not be on the base live ISO.
    command -v jq &>/dev/null || pacman -Sy --noconfirm jq
fi

############################################
# NETWORK CHECK
############################################
# Send one ICMP packet (-c1) with a 3-second timeout (-W3) to Cloudflare DNS.
# In AF_MODE we warn but continue — packages in pacstrap may already be cached.
if ! ping -c1 -W3 1.1.1.1 &>/dev/null; then
    if $AF_MODE; then
        echo "Warning: no internet connection detected — continuing in answerfile mode."
    else
        echo "No internet connection detected."
        echo "  Wired: ensure the cable is plugged in."
        echo "  WiFi:  switch to another TTY (Alt+F2) and run: iwctl"
        echo ""
        read -rp "Press Enter once connected (or Ctrl+C to abort)..."
        if ! ping -c1 -W3 1.1.1.1 &>/dev/null; then
            echo "Warning: still offline. Packages cannot be downloaded without network."
            read -rp "Continue anyway? [y/N]: " _net_ans
            [[ "${_net_ans,,}" == "y" ]] || { echo "Aborted — no network."; exit 1; }
        fi
    fi
fi

############################################
# KEYMAP
# To add more layouts: append "code|Display Name" to KEYMAPS
############################################
# Each entry is "keymap-code|Human-readable name"; the pipe acts as a cheap delimiter
# so both pieces of data live in a single array element without needing two arrays.
KEYMAPS=(
    "us|English US"
    "de|German"
)

if $AF_MODE; then
    LIVE_KEYMAP=$(af_get '.keymap' 'us')
else
    echo "Select keyboard layout:"
    for i in "${!KEYMAPS[@]}"; do
        # %%|* strips everything from the first '|' onward → the keymap code.
        _km_code="${KEYMAPS[$i]%%|*}"
        # ##*| strips everything up to and including the last '|' → the display name.
        _km_name="${KEYMAPS[$i]##*|}"
        printf "  %d) %-14s (%s)\n" $((i+1)) "$_km_name" "$_km_code"
    done
    read -rp "Choice [1]: " _KM_IDX
    # Convert 1-based user input to 0-based array index; default to 1 if blank.
    _KM_IDX=$(( ${_KM_IDX:-1} - 1 ))
    if (( _KM_IDX >= 0 && _KM_IDX < ${#KEYMAPS[@]} )); then
        LIVE_KEYMAP="${KEYMAPS[$_KM_IDX]%%|*}"
    else
        # Out-of-range input silently falls back to the first entry (us).
        LIVE_KEYMAP="${KEYMAPS[0]%%|*}"
    fi
fi
# Apply the chosen layout to the live environment immediately so subsequent
# interactive prompts can be typed with the correct physical keyboard.
loadkeys "$LIVE_KEYMAP"
# Also export as KEYMAP so the chroot heredoc can write /etc/vconsole.conf.
KEYMAP="$LIVE_KEYMAP"

############################################
# SAFETY WARNING
############################################
if $AF_MODE; then
    echo "WARNING: Automated install — all data on $(af_get '.drive' '/dev/?') will be ERASED."
    # Give the operator a 5-second window to abort with Ctrl-C before destructive ops begin.
    echo "Proceeding in 5 seconds... (Ctrl-C to abort)"
    sleep 5
else
    echo "WARNING: This will ERASE ALL DATA on the selected drive!"
    read -rp "Type 'YES' to continue: " confirm
    # Require the exact string "YES" (case-sensitive) to prevent accidental wipes.
    [[ "$confirm" == "YES" ]] || { echo "Aborted."; exit 1; }
fi

############################################
# REQUIRED PACKAGES FOR INSTALL ENVIRONMENT
############################################
# Wait for the live ISO's keyring initialization before any install. archiso
# initializes the pacman keyring asynchronously at boot via pacman-init.service
# (master-key generation can take a minute+ in VMs with little entropy), and
# this installer autostarts at login — racing that service leaves keys
# unpopulated/untrusted, so every package fails with "signature is unknown
# trust" / "invalid or corrupted package (PGP signature)". Do NOT run
# pacman-key --init ourselves here: doing so concurrently with
# pacman-init.service corrupts the half-built keyring. Wait, then refresh
# archlinux-keyring so an ISO built weeks ago still trusts current packager keys.
if systemctl cat pacman-init.service &>/dev/null; then
    echo "Waiting for pacman keyring initialization (pacman-init.service)..."
    while [[ "$(systemctl is-active pacman-init.service 2>/dev/null)" == "activating" ]]; do
        sleep 2
    done
else
    # Not on archiso (e.g. re-run from an installed system) — ensure a keyring exists.
    [[ -d /etc/pacman.d/gnupg ]] || { pacman-key --init; pacman-key --populate archlinux; }
fi
pacman -Sy --noconfirm archlinux-keyring

# Install tools needed by this script into the live environment before partitioning:
# parted — disk partitioning; cryptsetup — LUKS; libfido2/pam-u2f — FIDO2 enrollment.
pacman -Sy --noconfirm parted cryptsetup libfido2 pam-u2f

############################################
# DRIVE SELECTION
############################################
# Print current block device layout so the operator can confirm the target drive.
lsblk
if $AF_MODE && [[ -n "$(af_get '.drive')" ]]; then
    DRIVE=$(af_get '.drive')
    echo "Drive (from answerfile): $DRIVE"
else
    read -rp "Enter target drive (e.g., /dev/sda): " DRIVE
fi

############################################
# USER INPUT
############################################
if $AF_MODE; then
    KERNEL=$(af_get '.kernel' 'linux')
    RAW_HOSTNAME=$(af_get '.hostname' '')
    # Append MAC suffix to make the hostname unique when the same answerfile is
    # deployed to multiple machines (e.g. a lab rollout or reinstall fleet).
    if [[ -n "$RAW_HOSTNAME" ]]; then
        HOSTNAME="${RAW_HOSTNAME}-$(get_mac_suffix)"
    else
        HOSTNAME="arch"
    fi
    USERNAME=$(af_get '.username' '')
    ENCRYPT_DISK=$(af_bool '.encrypt')
    FIDO_ROOT=$(af_bool '.fido2_root')
    FIDO_USER=$(af_bool '.fido2_user')
    RUN_TUI=$(af_bool '.run_tui')
    echo "Kernel:   $KERNEL"
    echo "Hostname: $HOSTNAME"
    echo "Username: $USERNAME"
    echo "Encrypt:  $ENCRYPT_DISK  / FIDO2 root: $FIDO_ROOT  / FIDO2 user: $FIDO_USER"
else
    read -rp "Enter kernel package (e.g., linux, linux-lts): " KERNEL
    read -rp "Enter hostname: " HOSTNAME
    read -rp "Enter username: " USERNAME
    read -rp "Enable disk encryption? (YES/NO): " ENCRYPT_DISK
    # Default FIDO2 root to NO; only ask when encryption is active because FIDO2
    # unlocking only makes sense on an encrypted root partition.
    FIDO_ROOT="NO"
    if [[ "$ENCRYPT_DISK" == "YES" ]]; then
        read -rp "Enable FIDO2 unlocking for root partition? (YES/NO): " FIDO_ROOT
    fi
    read -rp "Enable FIDO2 authentication for user login? (YES/NO): " FIDO_USER
fi

# User password: use the answerfile's "password" field when present (enables a
# fully unattended install); otherwise prompt interactively. Read via af_get so a
# missing/empty field cleanly falls through to the prompt.
USERPASS=""
if $AF_MODE; then USERPASS="$(af_get '.password')"; fi
if [[ -n "$USERPASS" ]]; then
    echo "Password for $USERNAME: taken from answerfile."
else
    read -rp "Enter password for $USERNAME: " USERPASS
fi
[[ -z "$USERPASS" ]] && { echo "Error: password cannot be empty."; exit 1; }

# In interactive mode, decide now whether to run the dotfiles TUI inside chroot.
# AF_MODE already has RUN_TUI set from the answerfile.
if ! $AF_MODE; then
    read -rp "Run dotfiles TUI setup inside chroot now? [YES/no]: " _RUN_TUI_IN
    # Default to YES if the user presses Enter without typing anything.
    _RUN_TUI_IN="${_RUN_TUI_IN:-YES}"
    [[ "${_RUN_TUI_IN^^}" == "YES" ]] && RUN_TUI="YES" || RUN_TUI="NO"
fi

############################################
# RAM / PARTITION SIZING
############################################
# Read installed RAM in GiB; used to size both the swap and to calculate remaining
# root space.  `--giga` prints in GiB units; awk extracts the total column.
RAM_GB=$(free --giga | awk '/^Mem:/ {print $2}')
# 10 GiB boot partition: large enough for multiple kernels, initramfs, and UKIs.
BOOT_SIZE=10GiB
SWAP_SIZE="${RAM_GB}GiB"

# lsblk -b reports raw bytes; divide down to GiB for integer arithmetic.
DISK_SIZE=$(lsblk -b -dn -o SIZE "$DRIVE")
DISK_GIB=$((DISK_SIZE / 1024 / 1024 / 1024))
# Root gets whatever is left after reserving 10 GiB for boot and RAM_GB for swap.
ROOT_GIB=$((DISK_GIB - RAM_GB - 10))
if (( ROOT_GIB < 8 )); then
    echo "ERROR: Not enough disk space. Root would be only ${ROOT_GIB}GiB (need ≥8GiB)."
    exit 1
fi

echo "Partition plan:"
echo "  Boot: ${BOOT_SIZE}"
echo "  Root: ${ROOT_GIB}GiB"
echo "  Swap: ${SWAP_SIZE}"

############################################
# PARTITION DISK
############################################
# --script suppresses interactive prompts; mklabel gpt wipes any existing table.
# Partition 1: ESP at 1MiB–10GiB, flagged boot/esp so firmware can find it.
# Partition 2: ROOT immediately after the ESP.
# Partition 3: SWAP fills the rest (100%).
# Starting at 1MiB aligns the first partition to a 1 MiB boundary, which is
# optimal for both SSD erase blocks and spinner track alignment.
parted "$DRIVE" --script mklabel gpt \
    mkpart ESP fat32 1MiB 10GiB \
    set 1 boot on \
    mkpart ROOT 10GiB "$((10 + ROOT_GIB))"GiB \
    mkpart SWAP "$((10 + ROOT_GIB))"GiB 100%

# Resolve partition device paths via the `part` helper (handles NVMe 'p' suffix).
BOOT_PART=$(part "$DRIVE" 1)
ROOT_PART=$(part "$DRIVE" 2)
SWAP_PART=$(part "$DRIVE" 3)

############################################
# FORMAT BOOT + SWAP
############################################
# FAT32 is required for the EFI System Partition; -F32 sets the FAT variant.
mkfs.fat -F32 "$BOOT_PART"
# mkswap writes the swap header; swapon activates it for use by pacstrap.
mkswap "$SWAP_PART"
swapon "$SWAP_PART"

############################################
# ENCRYPTION (OPTIONAL)
############################################
LUKS_BACKUP_KEY=""   # path to key file, set only when encryption is active

if [[ "$ENCRYPT_DISK" == "YES" ]]; then
    echo "Encrypting root partition..."
    # LUKS passphrase: from the answerfile's "luks_password" when present (so an
    # encrypted install can run unattended), otherwise prompt interactively.
    LUKS_PASS=""
    if $AF_MODE; then LUKS_PASS="$(af_get '.luks_password')"; fi
    # --type luks2 is required for systemd-cryptenroll (FIDO2 token enrollment).
    if [[ -n "$LUKS_PASS" ]]; then
        echo "LUKS passphrase: taken from answerfile."
        # --batch-mode skips the interactive "Are you sure" + passphrase prompts;
        # --key-file=- reads the passphrase from stdin (the piped value).
        printf '%s' "$LUKS_PASS" | cryptsetup -v luksFormat "$ROOT_PART" --type luks2 --batch-mode --key-file=-
        printf '%s' "$LUKS_PASS" | cryptsetup open "$ROOT_PART" cryptroot --key-file=-
    else
        # -v (verbose) shows progress; prompts for the primary passphrase interactively.
        cryptsetup -v luksFormat "$ROOT_PART" --type luks2
        # Open the container and expose it as /dev/mapper/cryptroot for formatting.
        cryptsetup open "$ROOT_PART" cryptroot
    fi

    # ── Auto-generate backup LUKS key ──────────────────────────────────────────
    # A random key is enrolled as a second LUKS slot so recovery is possible
    # without the primary passphrase. It is written to /_LUKS_BACKUP_KEY in the
    # new system (inside the encrypted container) where only root can read it.
    LUKS_BACKUP_KEY=$(mktemp /tmp/luks-backup-key.XXXXXX)
    # Read 64 bytes from /dev/urandom and base64-encode them (-w0 disables line
    # wrapping) to produce a printable key with high entropy.
    dd if=/dev/urandom bs=64 count=1 2>/dev/null | base64 -w0 > "$LUKS_BACKUP_KEY"
    echo "Enrolling auto-generated backup LUKS key..."
    # luksAddKey needs the existing passphrase to authorise adding the new key file
    # into a free LUKS slot. With an answerfile passphrase we pipe it in on stdin;
    # otherwise cryptsetup prompts for it interactively.
    if [[ -n "$LUKS_PASS" ]]; then
        printf '%s' "$LUKS_PASS" | cryptsetup luksAddKey "$ROOT_PART" "$LUKS_BACKUP_KEY"
    else
        cryptsetup luksAddKey "$ROOT_PART" "$LUKS_BACKUP_KEY"
    fi

    # ── Optional FIDO2 enrollment ─────────────────────────────────────────────
    if [[ "$FIDO_ROOT" == "YES" ]]; then
        echo "Insert FIDO2 key for LUKS and touch when prompted..."
        # --fido2-device=auto finds any connected FIDO2 token automatically.
        # --fido2-with-client-pin=no skips PIN verification; the hardware
        # user-presence tap alone is sufficient for this setup.
        systemd-cryptenroll "$ROOT_PART" --fido2-device=auto --fido2-with-client-pin=no
    fi

    ############################################
    # BTRFS ON ENCRYPTED ROOT
    ############################################
    # Format the decrypted mapper device, not the raw partition.
    mkfs.btrfs /dev/mapper/cryptroot
    # Mount flat (no subvolume) first so we can create the subvolume layout.
    mount /dev/mapper/cryptroot /mnt
    # @ is the conventional name for the root subvolume in btrfs-on-Arch setups;
    # @home separates user data so it can be snapshotted or rolled back independently.
    btrfs subvolume create /mnt/@
    btrfs subvolume create /mnt/@home
    # Unmount the flat view before remounting through the subvolumes.
    umount /mnt

    # Mount each subvolume at its final path for pacstrap to populate.
    mount -o subvol=@ /dev/mapper/cryptroot /mnt
    mkdir -p /mnt/home
    mount -o subvol=@home /dev/mapper/cryptroot /mnt/home

else
    echo "Skipping encryption — formatting root directly."

    ############################################
    # BTRFS ON UNENCRYPTED ROOT
    ############################################
    # Same subvolume layout as the encrypted branch for consistency.
    mkfs.btrfs "$ROOT_PART"
    mount "$ROOT_PART" /mnt
    btrfs subvolume create /mnt/@
    btrfs subvolume create /mnt/@home
    umount /mnt

    mount -o subvol=@ "$ROOT_PART" /mnt
    mkdir -p /mnt/home
    mount -o subvol=@home "$ROOT_PART" /mnt/home
fi

mkdir -p /mnt/boot
# Mount the ESP so GRUB and the kernel can be installed inside the new root tree.
mount "$BOOT_PART" /mnt/boot

# Place backup key inside the new system (only accessible when disk is unlocked)
if [[ -n "$LUKS_BACKUP_KEY" ]]; then
    # `install -m 400` creates the destination file with mode 0400 (root read-only)
    # in one atomic step, avoiding an intermediate world-readable state.
    install -m 400 "$LUKS_BACKUP_KEY" /mnt/_LUKS_BACKUP_KEY
    # Remove the temp file from the live environment immediately after copying.
    rm -f "$LUKS_BACKUP_KEY"
    echo "Backup LUKS key written to /_LUKS_BACKUP_KEY in new system."
fi

############################################
# GPU DETECTION
############################################
# lspci lists PCI devices; filter for VGA and 3D controllers to identify the GPU.
# `|| true` prevents set -e from aborting if no GPU line is found.
GPU_INFO=$(lspci | grep -E "VGA|3D" || true)
GPU_PKGS=""
if echo "$GPU_INFO" | grep -qi "NVIDIA"; then
    GPU_PKGS="nvidia nvidia-utils"
elif echo "$GPU_INFO" | grep -qi "AMD"; then
    # xf86-video-amdgpu provides the Xorg DDX driver; the kernel DRM driver ships in linux-firmware.
    GPU_PKGS="xf86-video-amdgpu"
elif echo "$GPU_INFO" | grep -qi "Intel"; then
    GPU_PKGS="xf86-video-intel"
fi
echo "Detected GPU: ${GPU_INFO:-none}"

############################################
# BASE INSTALL
############################################
# pacstrap installs packages into /mnt using pacman's keyring (-K initialises it).
# GPU_PKGS is intentionally unquoted so it expands to multiple words (or nothing).
# shellcheck disable=SC2086
pacstrap -K /mnt base base-devel "$KERNEL" linux-firmware vim bash zsh git less btop fastfetch \
    networkmanager openssh grub cryptsetup libfido2 pam-u2f efibootmgr sudo btrfs-progs lvm2 jq $GPU_PKGS

############################################
# FSTAB
############################################
# genfstab -U uses UUIDs (rather than device names or labels) so the fstab
# remains valid even if drive letter assignments change after a hardware swap.
genfstab -U /mnt >> /mnt/etc/fstab

############################################
# COPY ANSWERFILE INTO NEW SYSTEM
############################################
# Make the answerfile available inside the new system so the dotfiles TUI
# installer can read the same selections without re-prompting the user.
if $AF_MODE; then
    install -m 644 "$ANSWERFILE" /mnt/answerfile.json
fi

############################################
# PASS VARIABLES INTO CHROOT
############################################
# Variables must be exported so the arch-chroot heredoc inherits them.
# The heredoc uses single-quote delimiter ('CHROOT_EOF') which prevents expansion
# outside the chroot, so env vars must be in the environment, not inline.
export HOSTNAME USERNAME USERPASS ROOT_PART KERNEL FIDO_ROOT FIDO_USER ENCRYPT_DISK KEYMAP

############################################
# CHROOT CONFIGURATION
############################################
# arch-chroot binds /proc, /sys, /dev and runs commands inside the new root.
# Single-quoted 'CHROOT_EOF' prevents the outer shell from expanding variables;
# the inner shell resolves them from the exported environment above.
arch-chroot /mnt /bin/bash <<'CHROOT_EOF'
set -euo pipefail

# Locale
# Uncomment (create) the en_US.UTF-8 entry so locale-gen can compile it.
echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
locale-gen
echo "LANG=en_US.UTF-8" > /etc/locale.conf
# vconsole.conf sets the keymap for the virtual console (TTY) on every boot.
echo "KEYMAP=${KEYMAP}" > /etc/vconsole.conf

# Time / hostname
# Symlink the timezone file so /etc/localtime always points at the correct zoneinfo.
ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime
# --systohc writes the current system time to the hardware clock (RTC).
hwclock --systohc
echo "$HOSTNAME" > /etc/hostname

# NetworkManager + SSH
# Enable at boot so the system has networking on first login without manual setup.
systemctl enable NetworkManager
# Enable SSH server so the machine is reachable headlessly after install.
systemctl enable sshd
# Allow root login via password for initial automated post-install steps;
# FreeIPA enrollment and ansipa deploy lock this down afterwards.
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
echo "root:${USERPASS}" | chpasswd

# Populate /etc/skel before user creation so useradd -m copies everything
echo "Cloning dotfiles into /etc/skel..."
git clone https://git.abdelbaki.eu/The_miro/Dotfiles.git /etc/skel/Dotfiles \
    || echo "Warning: dotfiles clone failed — clone manually after first boot."
mkdir -p /etc/skel/{Desktop,Documents,Downloads,Music,Pictures,Public,Templates,Videos}

# User
# -m: create home dir; -G wheel: add to sudoers group; -s /bin/zsh: default shell.
useradd -m -G wheel -s /bin/zsh "$USERNAME"
# chpasswd reads "user:pass" from stdin to set the password non-interactively.
echo "$USERNAME:$USERPASS" | chpasswd
# Grant wheel group full sudo access (ALL:ALL covers any user/group runas context).
# Use a drop-in rather than appending to /etc/sudoers: the default sudoers ends
# with '@includedir /etc/sudoers.d', so an appended '%wheel' rule would be parsed
# AFTER the drop-ins and — since the last matching rule wins — override the
# temporary 99-setup-nopasswd NOPASSWD rule used during the in-chroot TUI run,
# making the user re-enter their password on every sudo. A 10-wheel drop-in sorts
# before 99-setup-nopasswd, so NOPASSWD wins while it is present and password
# auth resumes once it is removed.
# Guard that drop-ins are actually read (the stock sudoers already includes this).
grep -q '^@includedir /etc/sudoers.d' /etc/sudoers || echo '@includedir /etc/sudoers.d' >> /etc/sudoers
echo '%wheel ALL=(ALL:ALL) ALL' > /etc/sudoers.d/10-wheel
chmod 0440 /etc/sudoers.d/10-wheel

###################################################
# INITRAMFS CONFIG
###################################################
# The HOOKS line controls which initramfs modules are compiled in.
# Three variants depending on encryption and FIDO2 choices:
# 1. FIDO2 root unlock: needs `systemd` + `sd-encrypt` for systemd-cryptsetup.
# 2. Password-only LUKS: uses classic `encrypt` hook (no systemd dependency).
# 3. Unencrypted: minimal hook set — no encrypt hook needed.
if [[ "$ENCRYPT_DISK" == "YES" && "$FIDO_ROOT" == "YES" ]]; then
    sed -i 's/^HOOKS=.*/HOOKS=(base udev systemd autodetect microcode modconf kms consolefont block sd-encrypt btrfs filesystems keyboard keymap fsck)/' /etc/mkinitcpio.conf
elif [[ "$ENCRYPT_DISK" == "YES" ]]; then
    sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect microcode modconf kms consolefont block encrypt btrfs filesystems keyboard keymap fsck)/' /etc/mkinitcpio.conf
else
    sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect microcode modconf kms consolefont block btrfs filesystems keyboard fsck)/' /etc/mkinitcpio.conf
fi

# -P regenerates all installed kernel presets (more thorough than -p <name>).
mkinitcpio -P

###################################################
# GRUB CONFIG
###################################################
# Read the raw partition UUID; used in kernel cmdline for stable device identification.
UUID=$(blkid -s UUID -o value "$ROOT_PART")
if [[ "$ENCRYPT_DISK" == "YES" ]]; then
    if [[ "$FIDO_ROOT" == "YES" ]]; then
        # systemd-cryptsetup syntax: rd.luks.name=<UUID>=<name>.
        # rd.luks.options=fido2-device=auto tells sd-encrypt to probe for the FIDO2 token.
        KERNEL_CMD="rd.luks.name=${UUID}=cryptroot rd.luks.options=fido2-device=auto root=/dev/mapper/cryptroot"
    else
        # Classic initramfs encrypt hook syntax: cryptdevice=UUID=<UUID>:<name>.
        KERNEL_CMD="cryptdevice=UUID=${UUID}:cryptroot root=/dev/mapper/cryptroot"
    fi
else
    # Unencrypted btrfs: root= by UUID and rootflags to select the @ subvolume.
    KERNEL_CMD="root=UUID=${UUID} rootflags=subvol=@"
fi

# Inject the kernel command line into /etc/default/grub using sed.
# The `|` delimiter avoids conflicts with `/` in device paths.
sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"$KERNEL_CMD\"|" /etc/default/grub

# Install GRUB to the EFI partition; --bootloader-id sets the NVRAM entry name.
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=M-Archy-GRUB
# Also install to the UEFI removable-media fallback path (/EFI/BOOT/BOOTX64.EFI).
# The --bootloader-id install above only creates an NVRAM boot variable; a machine
# whose NVRAM is empty or gets cleared (fresh board, CMOS reset, firmware that
# drops stale entries, or — critically for this imaging/netboot installer — a disk
# cloned or redeployed to different hardware) then has "No bootable device". The
# removable fallback is what firmware boots when no NVRAM entry matches, so it makes
# the installed disk portable and self-booting regardless of NVRAM state.
grub-install --target=x86_64-efi --efi-directory=/boot --removable
# Generate grub.cfg from /etc/default/grub and detected kernels.
grub-mkconfig -o /boot/grub/grub.cfg

###################################################
# USER FIDO2 LOGIN (directory + PAM only)
###################################################
# Key enrollment runs outside the chroot — pamu2fcfg cannot reach /dev/hidraw*
# from inside because the host's udev owns those device nodes.
if [[ "$FIDO_USER" == "YES" ]]; then
    mkdir -p "/home/$USERNAME/.config/Yubico"
    chown "$USERNAME:$USERNAME" "/home/$USERNAME/.config/Yubico"
    # `cue` prints a prompt so the user knows to touch the key when challenged.
    echo "auth required pam_u2f.so cue" >> /etc/pam.d/system-local-login
fi

CHROOT_EOF

############################################
# USER FIDO2 ENROLLMENT (outside chroot)
############################################
# pamu2fcfg must run outside arch-chroot: the host's udev manages /dev/hidraw*
# and the new user has no access to those nodes from inside the chroot.
if [[ "$FIDO_USER" == "YES" ]]; then
    echo "Enrolling FIDO2 key for user login (outside chroot)..."
    U2F_KEYFILE="/mnt/home/${USERNAME}/.config/Yubico/u2f_keys"
    mkdir -p "/mnt/home/${USERNAME}/.config/Yubico"
    # Scope the credential to this hostname so it cannot be replayed on another system.
    pamu2fcfg -u "$USERNAME" -o "pam://$HOSTNAME" -i "pam://$HOSTNAME" > "$U2F_KEYFILE"
    # Query numeric UID/GID from inside the chroot so ownership is correct.
    _NEWUID=$(arch-chroot /mnt id -u "$USERNAME" 2>/dev/null || echo "1000")
    _NEWGID=$(arch-chroot /mnt id -g "$USERNAME" 2>/dev/null || echo "1000")
    chown -R "$_NEWUID:$_NEWGID" "/mnt/home/${USERNAME}/.config/Yubico"
    chmod 600 "$U2F_KEYFILE"
    echo "FIDO2 key enrolled for $USERNAME."
fi

############################################
# DOTFILES TUI SETUP (in-chroot, optional)
############################################
if [[ "${RUN_TUI^^}" == "YES" ]]; then
    # Grant passwordless sudo temporarily so the TUI installer can call pacman/yay
    # without needing a password inside the chroot (the real sudoers is already set).
    # The file is removed immediately after the TUI exits.
    #
    # Two lines are needed, not just a NOPASSWD command rule:
    #   * `<user> ALL=(ALL:ALL) NOPASSWD: ALL` makes `sudo <cmd>` passwordless.
    #   * `Defaults:<user> !authenticate` makes `sudo -v` passwordless too. Several
    #     installers (starship, rustup, …) call `sudo -v` to pre-authorise, and that
    #     check demands a password whenever the user has ANY password-required
    #     sudoers entry — which tester does, via the wheel rule in 10-wheel. Without
    #     !authenticate the TUI stalls on a hidden `[sudo] password` prompt.
    printf 'Defaults:%s !authenticate\n%s ALL=(ALL:ALL) NOPASSWD: ALL\n' "$USERNAME" "$USERNAME" \
        | arch-chroot /mnt tee /etc/sudoers.d/99-setup-nopasswd > /dev/null
    arch-chroot /mnt chmod 0440 /etc/sudoers.d/99-setup-nopasswd

    echo "Running tui-install.sh as ${USERNAME} inside chroot..."
    # `runuser -u` switches to the unprivileged user inside the chroot so that
    # AUR helpers and dotfiles are owned/built by the correct UID.
    arch-chroot /mnt runuser -u "${USERNAME}" -- \
        bash "/home/${USERNAME}/Dotfiles/setup/tui-install.sh" \
        || echo "Warning: tui-install exited with errors — check ~/dotfiles-install.log in the new system."

    # Remove the temporary no-password sudoers drop-in after setup is done.
    arch-chroot /mnt rm -f /etc/sudoers.d/99-setup-nopasswd
fi

# Remove answerfile from new system after setup is complete (contains sensitive paths/config)
if $AF_MODE && [[ -f /mnt/answerfile.json ]]; then
    rm -f /mnt/answerfile.json
fi

############################################
# SUMMARY
############################################
echo
echo "############################################"
echo " INSTALL SUMMARY"
echo "############################################"
echo "Drive:             $DRIVE"
echo "Boot partition:    $BOOT_PART"
echo "Root partition:    $ROOT_PART"
echo "Swap partition:    $SWAP_PART"
echo
echo "Hostname:          $HOSTNAME"
echo "Username:          $USERNAME"
echo "Kernel:            $KERNEL"
echo "GPU detected:      ${GPU_INFO:-none}"
echo
echo "Disk encryption:   $ENCRYPT_DISK"
echo "FIDO2 root unlock: $FIDO_ROOT"
echo "FIDO2 user login:  $FIDO_USER"
[[ "$ENCRYPT_DISK" == "YES" ]] && echo "LUKS backup key:   /_LUKS_BACKUP_KEY (in new system)"
echo
echo "Boot size:         $BOOT_SIZE"
echo "Root size:         ${ROOT_GIB}GiB"
echo "Swap size:         $SWAP_SIZE"
echo
echo "Log file:          $LOGFILE"
echo "############################################"
echo

# Copy install log into /boot so it survives and is accessible before login.
# `2>/dev/null || true` silently ignores failures (e.g. /mnt/boot not writable).
cp "$LOGFILE" /mnt/boot/ 2>/dev/null || true

echo "Installation complete! Unmount and reboot:"
echo "  umount -R /mnt && reboot"
if [[ "${RUN_TUI^^}" != "YES" ]]; then
    echo
    echo "After first boot, login as $USERNAME and run:"
    echo "  ~/Dotfiles/setup/tui-install.sh"
fi
