#!/bin/sh
# Mountaineer installer — MVP (shell + dialog).
#
# Runs in the live ISO environment. Implements the 17-step flow from
# INSTALL.md. ratatui-shaped UX is a v0.2 goal; for MVP every interactive
# prompt uses `dialog`. Non-interactive installs are supported via
# `--config <file>` (TODO — config schema not yet defined).
#
# Status: not yet tested end-to-end. Each step is documented with what it
# expects to find and how it fails. Run with `--dry-run` to walk every
# prompt without touching the disk.

set -eu

# --- globals -------------------------------------------------------------
DRY_RUN=0
APKOVL_PATH="${APKOVL_PATH:-/media/cdrom/mountaineer.apkovl.tar.gz}"
CONFIG=""
TARGET=/mnt
ROOT_DATASET=rpool/ROOT/mountaineer

while [ $# -gt 0 ]; do
    case "$1" in
        --dry-run) DRY_RUN=1 ;;
        --apkovl)  shift; APKOVL_PATH="$1" ;;
        --config)  shift; CONFIG="$1" ;;
        *)         echo "unknown flag: $1" >&2; exit 2 ;;
    esac
    shift
done

# Auto-detect a config on standard paths if none was passed.
if [ -z "$CONFIG" ]; then
    for candidate in /media/cdrom/install.yaml /mnt/config/install.yaml /media/MTN-CFG/install.yaml; do
        if [ -f "$candidate" ]; then
            CONFIG="$candidate"
            break
        fi
    done
fi

# --- config reader (minimal YAML — flat key: value pairs only) ----------
# Mountaineer's install config is intentionally flat. If it grows into
# something a real YAML parser is required for, switch to yq.
cfg_get() {
    # cfg_get <key> [<default>]
    local key="$1"
    local default="${2:-}"
    if [ -z "$CONFIG" ] || [ ! -f "$CONFIG" ]; then
        echo "$default"
        return
    fi
    local val
    val=$(awk -F': *' -v k="$key" '
        $0 ~ "^"k": *" {
            sub(/^[^:]+: */, "")
            sub(/ *#.*/, "")
            gsub(/^"|"$/, "")
            print
            exit
        }
    ' "$CONFIG")
    if [ -z "$val" ]; then
        echo "$default"
    else
        echo "$val"
    fi
}

INTERACTIVE=1
if [ -n "$CONFIG" ] && [ -f "$CONFIG" ]; then
    echo "==> using install config: $CONFIG"
    INTERACTIVE=0
fi

run() {
    if [ "$DRY_RUN" = 1 ]; then
        echo "[dry-run] $*"
    else
        # shellcheck disable=SC2068
        $@
    fi
}

die() { echo "error: $*" >&2; exit 1; }

# --- step 1: refuse legacy BIOS ------------------------------------------
require_uefi() {
    if [ ! -d /sys/firmware/efi ]; then
        die "this system booted in legacy BIOS mode; Mountaineer is UEFI-only (see INSTALL.md). install aborted."
    fi
}

# --- step 2: pick a disk -------------------------------------------------
pick_disk() {
    if [ "$INTERACTIVE" = 0 ]; then
        DISK=$(cfg_get disk)
        [ -n "$DISK" ] || die "config has no 'disk' key"
        [ -b "$DISK" ] || die "config disk '$DISK' is not a block device"
        echo "==> using disk $DISK from config"
        return
    fi

    # Build a dialog menu of candidate disks. Exclude readonly, removable,
    # and the boot medium itself.
    candidates=$(lsblk -dn -o NAME,TYPE,SIZE,MODEL | awk '$2 == "disk" {print $1, $3, substr($0, index($0, $4))}')
    if [ -z "$candidates" ]; then
        die "no candidate disks found via lsblk"
    fi

    menu_args=""
    while IFS= read -r line; do
        name=$(echo "$line" | awk '{print $1}')
        size=$(echo "$line" | awk '{print $2}')
        model=$(echo "$line" | cut -d' ' -f3-)
        menu_args="$menu_args /dev/$name ${size}_${model}"
    done <<EOF
$candidates
EOF

    # shellcheck disable=SC2086
    dialog --no-tags --menu "Select install disk (WILL BE WIPED):" 20 70 10 $menu_args 2>/tmp/disk
    DISK=$(cat /tmp/disk)
    rm -f /tmp/disk
    [ -n "$DISK" ] || die "no disk selected"

    dialog --yesno "WIPE $DISK and install Mountaineer? This destroys all data on the disk." 8 60 || \
        die "wipe declined"
}

# --- step 3: optional encryption -----------------------------------------
ask_encryption() {
    if [ "$INTERACTIVE" = 0 ]; then
        case "$(cfg_get encrypt false)" in
            true|yes|1) ENCRYPT=1; PASSPHRASE=$(cfg_get passphrase)
                        [ -n "$PASSPHRASE" ] || die "encrypt=true requires a passphrase in config" ;;
            *)          ENCRYPT=0; PASSPHRASE="" ;;
        esac
        echo "==> encryption: $([ $ENCRYPT = 1 ] && echo on || echo off)"
        return
    fi

    if dialog --yesno "Encrypt the root pool with a passphrase? (Recommended for laptops; often inappropriate for racked servers.)" 9 60; then
        ENCRYPT=1
        # Ask twice and confirm match.
        while :; do
            dialog --insecure --passwordbox "Root pool passphrase:" 8 60 2>/tmp/p1
            dialog --insecure --passwordbox "Confirm passphrase:" 8 60 2>/tmp/p2
            if [ "$(cat /tmp/p1)" = "$(cat /tmp/p2)" ] && [ -s /tmp/p1 ]; then
                PASSPHRASE=$(cat /tmp/p1)
                break
            fi
            dialog --msgbox "Passphrases did not match or were empty. Try again." 7 50
        done
        rm -f /tmp/p1 /tmp/p2
    else
        ENCRYPT=0
        PASSPHRASE=""
    fi
}

# --- step 4: partition ---------------------------------------------------
partition() {
    run sgdisk --zap-all "$DISK"
    run sgdisk -n1:1M:+512M -t1:EF00 -c1:MTN-ESP "$DISK"
    run sgdisk -n2:0:0 -t2:BF00 -c2:MTN-ROOT "$DISK"
    run partprobe "$DISK" || true
    sleep 1
    # NVMe disks expose partitions as ${DISK}p1, ${DISK}p2;
    # SATA/SCSI as ${DISK}1, ${DISK}2.
    case "$DISK" in
        *nvme*|*mmcblk*) ESP="${DISK}p1"; ZPART="${DISK}p2" ;;
        *)               ESP="${DISK}1";   ZPART="${DISK}2"  ;;
    esac
}

# --- step 5: create the pool ---------------------------------------------
create_pool() {
    pool_opts="-f \
        -o ashift=12 \
        -o autotrim=on \
        -O compression=lz4 \
        -O atime=off \
        -O xattr=sa \
        -O acltype=posixacl \
        -O dnodesize=auto \
        -O normalization=formD \
        -O mountpoint=none \
        -O canmount=off"

    if [ "$ENCRYPT" = 1 ]; then
        # Pipe passphrase to zpool create via /dev/stdin.
        if [ "$DRY_RUN" = 1 ]; then
            echo "[dry-run] zpool create $pool_opts -O encryption=on -O keyformat=passphrase -O keylocation=prompt rpool $ZPART"
        else
            # shellcheck disable=SC2086
            printf '%s' "$PASSPHRASE" | zpool create $pool_opts \
                -O encryption=on -O keyformat=passphrase -O keylocation=prompt \
                rpool "$ZPART"
        fi
    else
        # shellcheck disable=SC2086
        run zpool create $pool_opts rpool "$ZPART"
    fi
}

# --- step 6: dataset hierarchy -------------------------------------------
create_datasets() {
    run zfs create -o canmount=off -o mountpoint=none rpool/ROOT
    run zfs create -o mountpoint=/ -o canmount=noauto "$ROOT_DATASET"
    run zfs mount "$ROOT_DATASET"
    run zfs set reservation=1G "$ROOT_DATASET"

    run zfs create -o mountpoint=/home rpool/home
    run zfs create -o canmount=off -o mountpoint=/var rpool/var
    run zfs create rpool/var/log
    run zfs create rpool/var/lib
    run zfs create -o recordsize=128k rpool/var/lib/containers
    run zfs create -o sync=disabled rpool/var/cache

    # Mount the rest under $TARGET via altroot semantics.
    run zpool export rpool
    if [ "$ENCRYPT" = 1 ]; then
        if [ "$DRY_RUN" = 0 ]; then
            printf '%s' "$PASSPHRASE" | zpool import -N -R "$TARGET" rpool
            zfs load-key rpool
        fi
    else
        run zpool import -N -R "$TARGET" rpool
    fi
    run zfs mount -a
}

# --- step 7: ESP ---------------------------------------------------------
format_esp() {
    run mkfs.vfat -F32 -n MTN-ESP "$ESP"
    run mkdir -p "$TARGET/boot/efi"
    # busybox mount can't auto-detect FAT32 on a freshly formatted partition
    # — needs explicit fstype.
    run mount -t vfat "$ESP" "$TARGET/boot/efi"
}

# --- step 8: ARC sizing --------------------------------------------------
compute_arc_max() {
    # MemTotal is in kilobytes.
    total_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
    total_gib=$(( total_kb / 1024 / 1024 ))

    if   [ "$total_gib" -le 2 ];  then arc=$((256 * 1024 * 1024))
    elif [ "$total_gib" -le 4 ];  then arc=$((512 * 1024 * 1024))
    elif [ "$total_gib" -le 8 ];  then arc=$((1   * 1024 * 1024 * 1024))
    elif [ "$total_gib" -le 16 ]; then arc=$((2   * 1024 * 1024 * 1024))
    elif [ "$total_gib" -le 32 ]; then arc=$((4   * 1024 * 1024 * 1024))
    else
        # 12.5% of RAM, capped at 16 GiB.
        arc=$(( total_kb * 1024 / 8 ))
        cap=$(( 16 * 1024 * 1024 * 1024 ))
        [ "$arc" -gt "$cap" ] && arc="$cap"
    fi

    ARC_MAX="$arc"
    echo "==> RAM: ${total_gib} GiB; zfs_arc_max: ${ARC_MAX} bytes"
    if [ "$INTERACTIVE" = 1 ]; then
        dialog --msgbox "Detected ${total_gib} GiB RAM. Setting zfs_arc_max=${ARC_MAX} bytes.\n\nOverride post-install by editing /etc/modprobe.d/zfs.conf." 10 60
    fi
}

# --- step 9: install the base --------------------------------------------
install_base() {
    # Read the world file out of the apkovl directly so the installed
    # package set matches the apkovl-supplied declaration.
    tmpovl=$(mktemp -d)
    if [ "$DRY_RUN" = 0 ]; then
        tar -xzf "$APKOVL_PATH" -C "$tmpovl"
    fi
    WORLD="$tmpovl/etc/apk/world"
    REPOS="$tmpovl/etc/apk/repositories"

    run mkdir -p "$TARGET/etc/apk"
    if [ "$DRY_RUN" = 0 ]; then
        cp "$REPOS" "$TARGET/etc/apk/repositories"
        cp "$WORLD" "$TARGET/etc/apk/world"
        # Seed Alpine signing keys so apk trusts the index. Without these
        # apk reports "UNTRUSTED signature" and treats every package as
        # missing.
        mkdir -p "$TARGET/etc/apk/keys"
        cp /etc/apk/keys/*.pub "$TARGET/etc/apk/keys/" 2>/dev/null || true
    fi

    # Install every package listed in the world file. Strip inline `#`
    # comments and blank lines, then collapse to a single space-separated
    # list. The world file allows trailing comments on package lines
    # ("ifupdown-ng-wifi  # harmless on Server"), which apk does not.
    pkgs=$(sed -e 's/#.*//' -e '/^[[:space:]]*$/d' "$WORLD" | xargs)
    # shellcheck disable=SC2086
    run apk --root "$TARGET" --initdb \
        --repositories-file "$TARGET/etc/apk/repositories" \
        add $pkgs

    rm -rf "$tmpovl"
}

# --- step 10: unpack apkovl over the target ------------------------------
unpack_apkovl() {
    run tar -xzf "$APKOVL_PATH" -C "$TARGET"
}

# --- step 11: write the ARC config ---------------------------------------
write_arc_conf() {
    if [ "$DRY_RUN" = 1 ]; then
        echo "[dry-run] write zfs_arc_max=$ARC_MAX to $TARGET/etc/modprobe.d/zfs.conf"
    else
        cat > "$TARGET/etc/modprobe.d/zfs.conf" <<EOF
# Mountaineer: zfs_arc_max set at install time from RAM size.
options zfs zfs_arc_max=$ARC_MAX
EOF
    fi
}

# --- step 12: install zfsbootmenu ----------------------------------------
# Pinned to the upstream-signed unified EFI binary (kernel+initramfs+ZFS
# bundled). Update both VERSION and SHA256 together; sha256.txt at
# https://github.com/zbm-dev/zfsbootmenu/releases/download/<v>/sha256.txt
# is the source of truth.
ZBM_VERSION=v3.1.0
ZBM_KERNEL=linux6.18
ZBM_EFI_SHA256=0a2a192de90be4935c21a810f9e5281f0cbf625a76b2d6e9125c39c13becfe83

install_zbm() {
    esp_boot="$TARGET/boot/efi/EFI/BOOT"
    run mkdir -p "$esp_boot"

    asset="zfsbootmenu-release-x86_64-${ZBM_VERSION}-${ZBM_KERNEL}.EFI"
    url="https://github.com/zbm-dev/zfsbootmenu/releases/download/${ZBM_VERSION}/${asset}"

    if [ "$DRY_RUN" = 1 ]; then
        echo "[dry-run] curl -fL $url"
        echo "[dry-run] verify sha256 == $ZBM_EFI_SHA256"
        echo "[dry-run] install to $esp_boot/BOOTX64.EFI"
        echo "[dry-run] efibootmgr --create --disk $DISK --part 1 --label Mountaineer --loader '\\EFI\\BOOT\\BOOTX64.EFI'"
        return
    fi

    tmp=$(mktemp -d)
    echo "==> downloading zfsbootmenu $ZBM_VERSION ($ZBM_KERNEL)"
    if ! curl -fLsS -o "$tmp/zbm.EFI" "$url"; then
        rm -rf "$tmp"
        die "failed to download $url — check network and ZBM_VERSION"
    fi

    actual=$(sha256sum "$tmp/zbm.EFI" | awk '{print $1}')
    if [ "$actual" != "$ZBM_EFI_SHA256" ]; then
        rm -rf "$tmp"
        die "ZBM checksum mismatch: expected $ZBM_EFI_SHA256, got $actual"
    fi

    cp "$tmp/zbm.EFI" "$esp_boot/BOOTX64.EFI"
    rm -rf "$tmp"

    # Tell ZBM what kernel cmdline to use when chainloading Mountaineer.
    # `console=tty0 console=ttyS0,115200`: kernel emits to both consoles
    # so headless/serial-only hosts (VMs, oob mgmt) see boot messages
    # and physical hosts get a graphical console. Last-listed is init's
    # controlling tty. `ro` is conventional for ZFS root; `quiet` keeps
    # the boot quiet but visible. Set as a ZFS property on the root
    # dataset; ZBM reads `org.zfsbootmenu:commandline` per-dataset.
    # NB: bypass `run` because it word-splits, which mangles the
    # space-separated cmdline value.
    if [ "$DRY_RUN" = 1 ]; then
        echo "[dry-run] zfs set org.zfsbootmenu:commandline='ro quiet console=tty0 console=ttyS0,115200' $ROOT_DATASET"
    else
        zfs set org.zfsbootmenu:commandline='ro quiet console=tty0 console=ttyS0,115200' "$ROOT_DATASET"
    fi

    # Register a UEFI boot entry. Some firmware (notably QEMU/EDK2 without
    # an efivarfs writable mount) refuses; the BOOTX64.EFI default path
    # works as fallback either way, so this is best-effort.
    efibootmgr --create --disk "$DISK" --part 1 \
        --label "Mountaineer" --loader '\EFI\BOOT\BOOTX64.EFI' || \
        echo "warning: efibootmgr could not add boot entry — relying on \\EFI\\BOOT\\BOOTX64.EFI fallback path" >&2
}

# Bind-mount the host pseudo-filesystems into the target so chroot'd
# helpers (ssh-keygen, s6-rc-compile, post-install hooks) can open
# /dev/null, read /proc, etc. Idempotent.
bind_target_dev() {
    for d in dev proc sys; do
        run mkdir -p "$TARGET/$d"
        mountpoint -q "$TARGET/$d" 2>/dev/null && continue
        run mount --bind "/$d" "$TARGET/$d"
    done
}

unbind_target_dev() {
    for d in sys proc dev; do
        mountpoint -q "$TARGET/$d" 2>/dev/null && run umount "$TARGET/$d"
    done
    return 0
}

# --- step 13: per-host identity ------------------------------------------
generate_identity() {
    bind_target_dev
    run chroot "$TARGET" ssh-keygen -A
    if [ "$DRY_RUN" = 0 ]; then
        dd if=/dev/urandom bs=16 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' > "$TARGET/etc/machine-id"
        echo >> "$TARGET/etc/machine-id"
    fi
}

# --- step 14: first-boot prompts -----------------------------------------
first_boot_prompts() {
    if [ "$INTERACTIVE" = 0 ]; then
        HOSTNAME=$(cfg_get hostname mountaineer)
        PUBKEY=$(cfg_get ssh_pubkey)
        ROOTPW=$(cfg_get root_password)
        echo "==> hostname: $HOSTNAME"
    else
        dialog --inputbox "Hostname:" 8 50 "mountaineer" 2>/tmp/hn
        HOSTNAME=$(cat /tmp/hn); rm -f /tmp/hn
        [ -n "$HOSTNAME" ] || HOSTNAME=mountaineer

        dialog --inputbox "Operator SSH public key (paste, or leave blank to add later):" 10 70 "" 2>/tmp/pk
        PUBKEY=$(cat /tmp/pk); rm -f /tmp/pk

        dialog --insecure --passwordbox "Console root password (sshd does not allow password login; this is for console only):" 9 60 2>/tmp/rp
        ROOTPW=$(cat /tmp/rp); rm -f /tmp/rp
    fi

    if [ "$DRY_RUN" = 0 ]; then
        echo "$HOSTNAME" > "$TARGET/etc/hostname"
    fi

    if [ -n "$PUBKEY" ] && [ "$DRY_RUN" = 0 ]; then
        mkdir -p "$TARGET/root/.ssh"
        chmod 700 "$TARGET/root/.ssh"
        # Even though root login is denied for password auth, an
        # authorized_keys entry on root lets the operator bootstrap an
        # unprivileged account via console. Per first-boot route, the
        # operator should then add a real user and remove this entry.
        echo "$PUBKEY" > "$TARGET/root/.ssh/authorized_keys"
        chmod 600 "$TARGET/root/.ssh/authorized_keys"
    fi

    if [ -n "$ROOTPW" ] && [ "$DRY_RUN" = 0 ]; then
        echo "root:$ROOTPW" | chroot "$TARGET" chpasswd
    fi

    # Network: for MVP default to DHCP on every interface. Static and wifi
    # are post-install via /etc/network/interfaces.d/ — first-boot route
    # documents how.
    if [ "$DRY_RUN" = 0 ]; then
        cat > "$TARGET/etc/network/interfaces" <<'EOF'
# Mountaineer: DHCP on every non-loopback interface by default.
# Per-interface overrides go in /etc/network/interfaces.d/.

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
EOF
    fi
}

# --- step 15: compile s6-rc tree -----------------------------------------
compile_s6rc() {
    run chroot "$TARGET" s6-rc-compile /etc/s6-rc/compiled /etc/s6-rc/source
}

# --- step 16: summary + confirm ------------------------------------------
summary() {
    cat <<EOF
==> Install summary
    Disk:       $DISK
    Encrypted:  $([ "$ENCRYPT" = 1 ] && echo yes || echo no)
    Hostname:   $HOSTNAME
    SSH key:    $([ -n "$PUBKEY" ] && echo set || echo NOT SET)
    ARC max:    $ARC_MAX bytes
EOF
    if [ "$INTERACTIVE" = 1 ]; then
        dialog --yesno "Proceed with finalize + reboot?" 7 50 || die "install cancelled at confirm step"
    fi
}

# --- step 17: finalize ---------------------------------------------------
finalize() {
    unbind_target_dev
    run umount "$TARGET/boot/efi"
    run zfs umount -a
    run zpool export rpool
    run reboot
}

# --- main flow -----------------------------------------------------------
require_uefi
pick_disk
ask_encryption
partition
create_pool
create_datasets
format_esp
compute_arc_max
install_base
unpack_apkovl
write_arc_conf
install_zbm
generate_identity
first_boot_prompts
compile_s6rc
summary
finalize
