#!/usr/bin/env bash
set -Eeuo pipefail

readonly VERSION="0.2.0"
readonly DEFAULT_PACKAGES=(base linux-onix-zen linux-firmware sudo networkmanager onix-base onixos-cli-installer archlinux-keyring chaotic-keyring chaotic-mirrorlist olang syslinux zsh)
readonly SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
if [[ -f "$SCRIPT_DIR/installer-profiles.json" ]]; then
  readonly PROFILE_CONFIG="$SCRIPT_DIR/installer-profiles.json"
else
  readonly PROFILE_CONFIG="/usr/share/onixos-cli-installer/installer-profiles.json"
fi
DISK=""
HOSTNAME="onixos"
USERNAME="onix"
LOCALE="en_US.UTF-8"
TIMEZONE="UTC"
PACKAGES=("${DEFAULT_PACKAGES[@]}")
GROUP="core"
SELECTIONS="core"
DISPLAY_MANAGER=""
SERVICE_LIST=""
DOCKER_SELECTED=0
USER_PASSWORD_HASH=""
ROOT_PASSWORD_HASH=""
DRY_RUN=0
ASSUME_YES=0

die() { printf 'error: %s\n' "$*" >&2; exit 1; }
log() { printf '==> %s\n' "$*"; }
run() {
  if (( DRY_RUN )); then printf '+ %q' "$@"; printf '\n'; else "$@"; fi
}
usage() {
  cat <<EOF
OnixOS CLI installer ${VERSION}

Usage: onixos-installer [options]

  --disk DEVICE       Disk to erase and install to (required with --yes)
  --hostname NAME     Target hostname (default: ${HOSTNAME})
  --user NAME         Create the first user (default: ${USERNAME})
  --locale LOCALE     System locale (default: ${LOCALE})
  --timezone ZONE     Timezone (default: ${TIMEZONE})
  --packages LIST     Space-separated core package list
  --group NAME        Installation group: core, server, or desktop
  --selections LIST   Comma-separated profile selections
  --user-password-hash HASH  SHA-512 password hash for the first user
  --root-password-hash HASH  SHA-512 password hash for root
  --yes               Do not ask for confirmation
  --dry-run           Show actions without changing the system
  --version           Show version
  -h, --help          Show this help

The installer creates a GPT disk with an EFI system partition and an ext4
root partition and installs systemd-boot. Existing data on the selected disk
will be erased. Run it from a UEFI booted installation medium.
EOF
}
valid_name() { [[ "$1" =~ ^[a-z_][a-z0-9_-]*$ ]]; }
valid_hash() { [[ "$1" == \$* && "$1" != *[[:space:]:]* ]]; }
profile_data() {
  python3 - "$PROFILE_CONFIG" "$GROUP" "$SELECTIONS" "$1" <<'PY'
import json
import sys

with open(sys.argv[1], encoding="utf-8") as stream:
    profiles = json.load(stream)
group = profiles.get("groups", {}).get(sys.argv[2])
if group is None:
    raise SystemExit(1)
options = group.get("options", {})
selections = [item for item in sys.argv[3].split(",") if item]
if any(item not in options for item in selections):
    raise SystemExit(1)
if group.get("selection") == "single" and len(selections) != 1:
    raise SystemExit(1)
if group.get("selection") == "multi" and not selections:
    raise SystemExit(1)
if sys.argv[4] == "packages":
    for item in selections:
        packages = options[item].get("packages", [])
        if packages:
            print("\n".join(packages))
else:
    for item in selections:
        value = options[item].get(sys.argv[4], "")
        if value:
            print(value)
PY
}
select_profile_packages() {
  [[ -f "$PROFILE_CONFIG" ]] || die "profile configuration not found: $PROFILE_CONFIG"
  profile_data label >/dev/null || die "invalid installation profile"
  mapfile -t PROFILE_PACKAGES < <(profile_data packages)
  if ((${#PROFILE_PACKAGES[@]})); then
    PACKAGES+=("${PROFILE_PACKAGES[@]}")
  fi
  DISPLAY_MANAGER=$(profile_data display_manager | head -n 1)
  mapfile -t PROFILE_SERVICES < <(profile_data services)
  SERVICE_LIST="NetworkManager.service ${PROFILE_SERVICES[*]}"
  if [[ ",$SELECTIONS," == *,docker,* ]]; then
    DOCKER_SELECTED=1
  fi
  declare -A seen_packages=()
  local -a local_packages=()
  local package
  for package in "${PACKAGES[@]}"; do
    if [[ -n "$package" && -z "${seen_packages[$package]+set}" ]]; then
      seen_packages[$package]=1
      local_packages+=("$package")
    fi
  done
  PACKAGES=("${local_packages[@]}")
}
require_commands() {
  local command
  for command in pacstrap arch-chroot genfstab sgdisk partprobe mkfs.fat mkfs.ext4 mount umount; do
    command -v "$command" >/dev/null || die "required command not found: $command"
  done
}
cleanup() {
  if mountpoint -q /mnt/boot 2>/dev/null; then umount -R /mnt/boot || true; fi
  if mountpoint -q /mnt 2>/dev/null; then umount -R /mnt || true; fi
}
trap cleanup EXIT

configure_pacman_repositories() {
  PACMAN_CONF=$(mktemp)
  cp /etc/pacman.conf "$PACMAN_CONF"
  if ! grep -q '^\[onix-base\]' "$PACMAN_CONF"; then
    cat >> "$PACMAN_CONF" <<'EOF'

[onix-base]
SigLevel = Never
Server = https://repo.onix-project.com/base/$arch

[chaotic-aur]
SigLevel = Never
Server = https://cdn-mirror.chaotic.cx/$repo/$arch

[onix-kernel]
SigLevel = Never
Server = https://repo.onix-project.com/kernel/$arch
EOF
  fi
  install -Dm644 "$PACMAN_CONF" /mnt/etc/pacman.conf
}

cleanup_pacman_conf() {
  [[ -n "${PACMAN_CONF:-}" && -f "$PACMAN_CONF" ]] && rm -f "$PACMAN_CONF"
}
trap cleanup_pacman_conf EXIT

while (($#)); do
  case "$1" in
    --disk) (($# > 1)) || die "--disk needs a device"; DISK=$2; shift 2 ;;
    --hostname) (($# > 1)) || die "--hostname needs a value"; HOSTNAME=$2; shift 2 ;;
    --user) (($# > 1)) || die "--user needs a value"; USERNAME=$2; shift 2 ;;
    --locale) (($# > 1)) || die "--locale needs a value"; LOCALE=$2; shift 2 ;;
    --timezone) (($# > 1)) || die "--timezone needs a value"; TIMEZONE=$2; shift 2 ;;
    --packages) (($# > 1)) || die "--packages needs a value"; read -r -a PACKAGES <<< "$2"; shift 2 ;;
    --group) (($# > 1)) || die "--group needs a value"; GROUP=$2; shift 2 ;;
    --selections) (($# > 1)) || die "--selections needs a value"; SELECTIONS=$2; shift 2 ;;
    --user-password-hash) (($# > 1)) || die "--user-password-hash needs a value"; USER_PASSWORD_HASH=$2; shift 2 ;;
    --root-password-hash) (($# > 1)) || die "--root-password-hash needs a value"; ROOT_PASSWORD_HASH=$2; shift 2 ;;
    --yes) ASSUME_YES=1; shift ;;
    --dry-run) DRY_RUN=1; shift ;;
    --version) printf '%s\n' "$VERSION"; exit 0 ;;
    -h|--help) usage; exit 0 ;;
    *) die "unknown option: $1" ;;
  esac
done

select_profile_packages

(( EUID == 0 || DRY_RUN )) || die "run this installer as root"
if (( !DRY_RUN )) && [[ ! -d /sys/firmware/efi ]]; then
  die "boot the installation medium in UEFI mode"
fi
valid_name "$HOSTNAME" || die "invalid hostname: $HOSTNAME"
valid_name "$USERNAME" || die "invalid username: $USERNAME"
if (( !DRY_RUN )); then
  [[ -n "$USER_PASSWORD_HASH" ]] || die "user password is required"
  [[ -n "$ROOT_PASSWORD_HASH" ]] || die "root password is required"
  valid_hash "$USER_PASSWORD_HASH" || die "invalid user password hash"
  valid_hash "$ROOT_PASSWORD_HASH" || die "invalid root password hash"
fi
[[ -n "$DISK" ]] || {
  if (( ASSUME_YES )); then die "--disk is required with --yes"; fi
  read -r -p "Install to disk (ALL DATA WILL BE ERASED): " DISK
}
if (( !DRY_RUN )); then
  [[ -b "$DISK" ]] || die "not a block device: $DISK"
fi

if (( !ASSUME_YES && !DRY_RUN )); then
  printf '\nTarget: %s\nGroup: %s\nSelections: %s\nPackages: %s\n' "$DISK" "$GROUP" "$SELECTIONS" "${PACKAGES[*]}"
  read -r -p "Continue and erase this disk? Type 'yes': " confirmation
  [[ "$confirmation" == yes ]] || die "installation cancelled"
fi

(( DRY_RUN )) || require_commands
root_partition="${DISK}2"
efi_partition="${DISK}1"
[[ "$DISK" =~ (nvme|mmcblk|loop)$ ]] && { root_partition="${DISK}p2"; efi_partition="${DISK}p1"; }

log "partitioning $DISK"
run umount -R /mnt 2>/dev/null || true
run sgdisk --zap-all "$DISK"
run sgdisk -n 1:0:+1G -t 1:ef00 -c 1:EFI -n 2:0:0 -t 2:8304 -c 2:OnixOS "$DISK"
run partprobe "$DISK"
run mkfs.fat -F32 -n EFI "$efi_partition"
run mkfs.ext4 -F -L OnixOS "$root_partition"
run mount "$root_partition" /mnt
run mkdir -p /mnt/boot
run mount "$efi_partition" /mnt/boot

log "installing core packages"
if (( DRY_RUN )); then
  printf '+ configure OnixOS package repositories\n'
else
  configure_pacman_repositories
fi
run pacstrap ${PACMAN_CONF:+-C "$PACMAN_CONF"} -K /mnt "${PACKAGES[@]}"
run genfstab -U /mnt
if (( !DRY_RUN )); then genfstab -U /mnt >> /mnt/etc/fstab; fi

log "configuring the installed system"
if (( DRY_RUN )); then
  printf '+ configure locale=%s timezone=%s hostname=%s user=%s group=%s selections=%s\n' "$LOCALE" "$TIMEZONE" "$HOSTNAME" "$USERNAME" "$GROUP" "$SELECTIONS"
else
  arch-chroot /mnt /bin/bash -s -- "$LOCALE" "$TIMEZONE" "$HOSTNAME" "$USERNAME" "$root_partition" "$SERVICE_LIST" "$DOCKER_SELECTED" "$USER_PASSWORD_HASH" "$ROOT_PASSWORD_HASH" <<'CHROOT'
set -Eeuo pipefail
locale=$1; timezone=$2; hostname=$3; username=$4; root_partition=$5; service_list=$6; docker_selected=$7; user_password_hash=$8; root_password_hash=$9
printf '%s UTF-8\n' "$locale" >> /etc/locale.gen
sed -i "s/^#${locale}/${locale}/" /etc/locale.gen
locale-gen
printf 'LANG=%s\n' "$locale" > /etc/locale.conf
ln -sfn "/usr/share/zoneinfo/$timezone" /etc/localtime
hwclock --systohc
printf '%s\n' "$hostname" > /etc/hostname
printf '127.0.0.1 localhost\n::1 localhost\n127.0.1.1 %s.localdomain %s\n' "$hostname" "$hostname" > /etc/hosts
useradd -m -G wheel -s /bin/bash "$username"
if (( docker_selected )); then
  usermod -aG docker "$username"
fi
printf '%s:%s\n' "$username" "$user_password_hash" | chpasswd -e
printf 'root:%s\n' "$root_password_hash" | chpasswd -e
printf '%s\n' '%wheel ALL=(ALL:ALL) ALL' > /etc/sudoers.d/10-wheel
chmod 0440 /etc/sudoers.d/10-wheel
bootctl --esp-path=/boot install
root_uuid=$(blkid -s UUID -o value "$root_partition")
cat > /boot/loader/loader.conf <<EOF
default onixos.conf
timeout 3
editor no
EOF
cat > /boot/loader/entries/onixos.conf <<EOF
title OnixOS
linux /vmlinuz-linux
initrd /initramfs-linux.img
options root=UUID=$root_uuid rw
EOF
  for service in $service_list; do
    systemctl enable "$service" 2>/dev/null || true
  done
CHROOT
fi
log "OnixOS core installation completed"
