#!/bin/bash
################################################################################
# kiosk-firstboot - runs ubuntu-based-kiosk's ./install.sh on the first
# boot of a system installed from the custom ISO (iso/build-iso.sh).
#
# install.sh refuses to run as root and expects a regular sudo user, so
# this runs it as the admin account the Ubuntu installer created (the
# first regular user, uid 1000), from a copy of the repo in that user's
# home - the same layout as a manual `git clone && ./install.sh`, so
# re-running ./install.sh later and Advanced -> Upgrade both work as
# usual afterwards.
#
# For the duration of the install only, that user gets passwordless sudo
# (nobody has typed a password yet, and in unattended mode nobody will);
# the drop-in is removed on every exit path, including the reboot at the
# end of install.sh (see ExecStopPost in kiosk-firstboot.service).
#
# Settings come from /etc/kiosk-firstboot.conf, written at ISO build time:
#   KIOSK_UNATTENDED=1   take every prompt's default, skip the Core
#                        Settings menu, reboot into the kiosk when done.
#   KIOSK_UNATTENDED=0   (default) normal interactive install.sh on tty1.
#   KIOSK_OFFLINE_WAIT=N on an ISO built with --offline: seconds to wait
#                        for internet before installing from the bundled
#                        packages instead (default 90).
#   KIOSK_STALL_TIMEOUT=N   on an --offline ISO, unattended: if the install
#                        prints nothing for N seconds (a stalled download on
#                        a flaky link), stop it and finish from the bundled
#                        packages instead (default 600).
################################################################################

set -uo pipefail

CONF=/etc/kiosk-firstboot.conf
STATE_DIR=/var/lib/kiosk-firstboot
PENDING="$STATE_DIR/pending"
DONE_MARKER="$STATE_DIR/done"
SUDOERS_DROPIN=/etc/sudoers.d/90-kiosk-firstboot
APT_DROPIN=/etc/apt/apt.conf.d/90-kiosk-firstboot
REPO_SRC=/opt/ubuntu-based-kiosk
LOG="$STATE_DIR/install.log"
OFFLINE_BUNDLE=/opt/kiosk-offline
OFFLINE_APT=/usr/local/sbin/kiosk-offline-apt
OFFLINE_FLAG="$STATE_DIR/offline-mode"

KIOSK_UNATTENDED=0
KIOSK_OFFLINE_WAIT=90
KIOSK_STALL_TIMEOUT=600
# shellcheck source=/dev/null
[[ -f "$CONF" ]] && source "$CONF"

cleanup() {
    rm -f "$SUDOERS_DROPIN" "$APT_DROPIN"
    # Offline mode was only for this install - hand apt back to the real
    # archive so normal updates work once the kiosk is online.
    if [[ -f "$OFFLINE_FLAG" ]]; then
        "$OFFLINE_APT" off >/dev/null 2>&1 || true
        rm -f "$OFFLINE_FLAG"
    fi
    if [[ -f "$DONE_MARKER" ]]; then
        rm -f "$PENDING"
    fi
}

if [[ "${1:-}" == "--cleanup" ]]; then
    cleanup
    exit 0
fi

have_bundle() { [[ -f "$OFFLINE_BUNDLE/apt/Packages" ]]; }

# Finish this install from the bundled packages instead of the internet
# (cleanup() turns it back off). install.sh notices via lib/config.sh's
# offline_mode_active.
switch_to_offline() {
    touch "$OFFLINE_FLAG"
    if ! "$OFFLINE_APT" on; then
        echo "ERROR: couldn't switch apt to the offline bundle."
        rm -f "$OFFLINE_FLAG"
        return 1
    fi
    return 0
}

# Make sure tty1 gets its login prompt back. getty@tty1 is ordered after
# this unit and normally starts by itself once we exit; this just covers
# a getty that was stopped or never queued (e.g. started by hand later).
release_tty() {
    systemctl --no-block start getty@tty1.service 2>/dev/null || true
}

trap cleanup EXIT
trap 'exit 130' INT TERM HUP

banner() {
    clear
    echo "════════════════════════════════════════════════════════════"
    echo "   Ubuntu Based Kiosk - First Boot Setup"
    echo "════════════════════════════════════════════════════════════"
    echo
}

# Interactive-mode escape hatch when something needs a human: retry,
# drop to a shell as the admin user to fix it, or give up until next boot.
# Returns 0 to retry, 1 to give up.
offer_retry() {
    local choice offline_opt=""
    have_bundle && [[ ! -f "$OFFLINE_FLAG" ]] && offline_opt="   [O] Install from the offline bundle on this disk"
    while true; do
        echo
        echo "  [R] Retry   [S] Shell (as $ADMIN_USER, exit to come back)   [Q] Quit to login prompt$offline_opt"
        read -r -p "Choice [R]: " choice
        case "${choice,,}" in
            ""|r) return 0 ;;
            o) [[ -n "$offline_opt" ]] && switch_to_offline && return 0 ;;
            s) runuser -l "$ADMIN_USER" ;;
            q)
                echo "Setup will run again on next boot, or run it now with:"
                echo "  sudo systemctl start kiosk-firstboot"
                return 1
                ;;
        esac
    done
}

# Returns 0 once online, 2 to install from the offline bundle instead
# (only when there is one), 1 to give up.
wait_for_internet() {
    local waited=0
    until curl -fsS --max-time 10 -o /dev/null https://deb.nodesource.com/ 2>/dev/null \
       || curl -fsS --max-time 10 -o /dev/null http://archive.ubuntu.com/ubuntu/ 2>/dev/null; do
        if (( waited == 0 )); then
            echo "Waiting for an internet connection (needed for packages,"
            echo "Node.js and Electron)..."
            if [[ -f "$OFFLINE_BUNDLE/apt/Packages" ]]; then
                echo "This disk has the offline bundle: installing from it instead"
                echo "if there's still no internet after ${KIOSK_OFFLINE_WAIT}s."
            fi
        fi
        sleep 10
        waited=$((waited + 10))
        if [[ -f "$OFFLINE_BUNDLE/apt/Packages" ]] && (( waited >= KIOSK_OFFLINE_WAIT )); then
            echo "No internet after ${waited}s - installing from the offline bundle on this disk."
            return 2
        fi
        if (( waited % 120 == 0 )); then
            echo "Still no internet after ${waited}s. Network status:"
            networkctl list --no-pager 2>/dev/null || ip -brief addr
            if [[ "$KIOSK_UNATTENDED" != "1" ]]; then
                echo "WiFi or static IP? Use the Shell option and edit /etc/netplan/*.yaml, then 'sudo netplan apply'."
                offer_retry || return 1
            fi
        fi
    done
    echo "Internet connection OK."
}

################################################################################

# Keep kernel/systemd chatter off the screen the installer is drawing on.
dmesg -n 1 2>/dev/null || true
chvt 1 2>/dev/null || true
export TERM=linux

ADMIN_USER="$(getent passwd 1000 | cut -d: -f1)"
banner
if [[ -z "$ADMIN_USER" ]]; then
    echo "ERROR: no regular user (uid 1000) found - can't run install.sh."
    echo "Create one, then: sudo systemctl start kiosk-firstboot"
    release_tty
    exit 1
fi
if [[ "$ADMIN_USER" == "kiosk" ]]; then
    echo "ERROR: the admin account is named 'kiosk', which install.sh reserves"
    echo "for the kiosk's own restricted user. Reinstall with a different username."
    release_tty
    exit 1
fi
ADMIN_HOME="$(getent passwd "$ADMIN_USER" | cut -d: -f6)"
REPO_DEST="$ADMIN_HOME/ubuntu-based-kiosk"

if [[ ! -d "$REPO_DEST" ]]; then
    cp -a "$REPO_SRC" "$REPO_DEST"
    chmod -R u+w "$REPO_DEST"
    chown -R "$ADMIN_USER:$ADMIN_USER" "$REPO_DEST"
fi

echo "Admin user : $ADMIN_USER"
echo "Installer  : $REPO_DEST/install.sh"
echo "Mode       : $([[ "$KIOSK_UNATTENDED" == "1" ]] && echo "unattended (defaults, reboots when done)" || echo "interactive")"
echo "Log        : $LOG"
echo

# Anything else on a fresh system that wants the dpkg lock (e.g. the
# first unattended-upgrades run): wait for it rather than failing. Not
# `cloud-init status --wait` - cloud-init's final stage runs after
# multi-user.target, which waits for this service: a deadlock.
mkdir -p "$STATE_DIR"
printf 'DPkg::Lock::Timeout "900";\n' > "$APT_DROPIN"

wait_for_internet
case $? in
    0) ;;
    2)
        # install.sh notices this by itself (lib/config.sh's
        # offline_mode_active) and unpacks prebuilt node_modules instead
        # of running npm; cleanup() switches it back off.
        touch "$OFFLINE_FLAG"
        if ! "$OFFLINE_APT" on; then
            echo "ERROR: couldn't switch apt to the offline bundle."
            release_tty
            exit 1
        fi
        ;;
    *) release_tty; exit 1 ;;
esac

# install.sh's own preflight needs jq; Advanced -> Upgrade needs git.
echo "Installing installer prerequisites (jq, git)..."
DEBIAN_FRONTEND=noninteractive apt-get update -q >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y -q jq git curl >/dev/null

cat > "$SUDOERS_DROPIN".tmp <<SUDOERS
# Temporary - removed by /usr/local/sbin/kiosk-firstboot when it exits.
$ADMIN_USER ALL=(ALL) NOPASSWD: ALL
Defaults:$ADMIN_USER env_keep += "DEBIAN_FRONTEND"
SUDOERS
chmod 440 "$SUDOERS_DROPIN".tmp
if ! visudo -cf "$SUDOERS_DROPIN".tmp >/dev/null; then
    rm -f "$SUDOERS_DROPIN".tmp
    echo "ERROR: generated sudoers drop-in failed validation."
    release_tty
    exit 1
fi
mv "$SUDOERS_DROPIN".tmp "$SUDOERS_DROPIN"

run_install() {
    local -a envs=(
        HOME="$ADMIN_HOME" USER="$ADMIN_USER" LOGNAME="$ADMIN_USER"
        SHELL=/bin/bash TERM=linux LANG="${LANG:-C.UTF-8}"
        PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
        # config.sh would otherwise derive this from SUDO_USER/whoami.
        BUILD_USER="$ADMIN_USER"
        KIOSK_UNATTENDED="$KIOSK_UNATTENDED"
        KIOSK_PROVISION_DONE_MARKER="$DONE_MARKER"
    )
    # Even interactively: stdout goes through tee, which debconf dialogs
    # cannot draw on, and nothing install.sh installs needs a debconf answer.
    envs+=(DEBIAN_FRONTEND=noninteractive)

    # runuser rather than sudo -u: sudo would set SUDO_USER=root, which
    # config.sh reads as "the admin running this tool".
    # A stalled download on a flaky connection would otherwise hang an
    # unattended install forever. With the offline bundle as a safety net,
    # watch the log: no new output for KIOSK_STALL_TIMEOUT seconds means
    # stuck, so the whole process group is stopped (the caller then
    # finishes from the bundle).
    if [[ "$KIOSK_UNATTENDED" == "1" ]] && have_bundle && [[ ! -f "$OFFLINE_FLAG" ]]; then
        touch "$LOG"
        setsid bash -c '
            runuser -u "$1" -- env -i "${@:4}" bash -c "cd \"\$1\" && exec ./install.sh" _ "$2" 2>&1 | tee -a "$3"
            exit "${PIPESTATUS[0]}"
        ' _ "$ADMIN_USER" "$REPO_DEST" "$LOG" "${envs[@]}" &
        local pid=$!
        (
            while kill -0 "$pid" 2>/dev/null; do
                sleep 20
                age=$(( $(date +%s) - $(stat -c %Y "$LOG") ))
                if (( age > KIOSK_STALL_TIMEOUT )); then
                    echo
                    echo "No output for ${age}s - the online install looks stuck; stopping it."
                    kill -TERM -- "-$pid" 2>/dev/null
                    sleep 30
                    kill -KILL -- "-$pid" 2>/dev/null
                    break
                fi
            done
        ) &
        local watcher=$!
        wait "$pid"
        local rc=$?
        kill "$watcher" 2>/dev/null
        wait "$watcher" 2>/dev/null
        return "$rc"
    fi
    runuser -u "$ADMIN_USER" -- env -i "${envs[@]}" \
        bash -c 'cd "$1" && exec ./install.sh' _ "$REPO_DEST" \
        2>&1 | tee -a "$LOG"
    return "${PIPESTATUS[0]}"
}

attempt=1
while true; do
    run_install
    rc=$?
    if [[ -f "$DONE_MARKER" ]]; then
        # Reached only when the final "Reboot now?" was declined.
        echo
        echo "Kiosk install complete. Reboot to start the kiosk: sudo reboot"
        cleanup
        release_tty
        exit 0
    fi

    echo
    echo "install.sh did not finish (exit code $rc). Full output: $LOG"
    if [[ "$KIOSK_UNATTENDED" == "1" ]]; then
        # Online attempt failed or stalled: the bundle on this disk is
        # always going to work, so use it rather than retry the same thing.
        if have_bundle && [[ ! -f "$OFFLINE_FLAG" ]]; then
            echo "Online install didn't finish - continuing from the offline bundle on this disk."
            if switch_to_offline; then
                attempt=$((attempt + 1))
                continue
            fi
        fi
        if (( attempt >= 3 )); then
            echo "Giving up after $attempt attempts; will try again next boot."
            release_tty
            exit 1
        fi
        attempt=$((attempt + 1))
        echo "Retrying in 60 seconds (attempt $attempt of 3)..."
        sleep 60
    else
        offer_retry || { release_tty; exit 1; }
    fi
done
