#!/bin/bash
# dprofile - install Das OS profiles: sets of packages from the Void
# repository with their services and groups, plus optional applications from
# Flathub. The profiles are in /usr/share/das/profiles/NAME.conf.
set -euo pipefail

# shellcheck source=helpers.sh
. "${DAS_HELPERS:-/usr/lib/das/helpers.sh}"

profiles_dir=${DAS_PROFILES:-/usr/share/das/profiles}
flathub=https://dl.flathub.org/repo/flathub.flatpakrepo

usage() {
  if [[ -n $uk ]]; then
    cat <<'EOF'
Використання: dprofile КОМАНДА [ПРОФІЛЬ] [...]

  list                    профілі та їхній стан (типова команда)
  show ПРОФІЛЬ            пакети, служби й додаткові застосунки профілю
  install ПРОФІЛЬ         встановити профіль (зі знімком системи для відкату):
      -y, --yes           без запитань
      --apps ID,...|all|none
                          які застосунки з Flathub додати (інакше запитає)
  apps ПРОФІЛЬ            додаткові застосунки профілю з Flathub
  app add ID...           встановити застосунки з Flathub (ID або його кінець:
                          AndroidStudio, GoLand)
  app rm ID...            видалити їх

Профіль dev: dprofile install dev
EOF
  else
    cat <<'EOF'
Usage: dprofile COMMAND [PROFILE] [...]

  list                    profiles and their state (the default)
  show PROFILE            the profile's packages, services and optional apps
  install PROFILE         install a profile (with a system snapshot to roll back to):
      -y, --yes           ask nothing
      --apps ID,...|all|none
                          which Flathub applications to add (asks otherwise)
  apps PROFILE            the profile's optional applications from Flathub
  app add ID...           install applications from Flathub (an ID or its end:
                          AndroidStudio, GoLand)
  app rm ID...            remove them

The dev profile: dprofile install dev
EOF
  fi
}

desktop() {
  command -v Xorg >/dev/null
}

live() {
  grep -q 'root=live:' /proc/cmdline 2>/dev/null
}

profile_names() {
  local f
  for f in "$profiles_dir"/*.conf; do
    [[ -e $f ]] || continue
    f=${f##*/}
    echo "${f%.conf}"
  done
}

# load NAME: reads the profile into title, desc, packages, services, groups,
# subids, shared_root and apps (one "ID NAME" per line)
load() {
  local f=$profiles_dir/$1.conf
  [[ $1 =~ ^[a-z0-9-]+$ && -r $f ]] ||
    die "немає профілю '$1' (dprofile list)" "no profile '$1' (dprofile list)"
  title_uk="" title_en="" desc_uk="" desc_en="" server_desc_uk="" server_desc_en=""
  packages="" desktop_packages="" services="" groups="" subids="" shared_root="" apps=""
  # shellcheck source=profiles/dev.conf
  . "$f"
  title=$(say "$title_uk" "$title_en")
  desc=$(say "$desc_uk" "$desc_en")
  if desktop; then
    packages+=" $desktop_packages"
  else
    apps=""
    [[ -z $server_desc_en ]] || desc=$(say "$server_desc_uk" "$server_desc_en")
  fi
  # shellcheck disable=SC2086
  packages=$(printf '%s\n' $packages)
  apps=$(grep . <<<"$apps" || true)
}

installed() {
  xbps-query "$1" >/dev/null 2>&1
}

# Packages of the loaded profile that are not installed yet
missing_packages() {
  local p
  while read -r p; do
    installed "$p" || echo "$p"
  done <<<"$packages"
}

app_installed() {
  command -v flatpak >/dev/null && flatpak info --system "$1" >/dev/null 2>&1
}

cmd_list() {
  local name total missing state
  for name in $(profile_names); do
    load "$name"
    total=$(grep -c . <<<"$packages")
    missing=$(missing_packages | grep -c . || true)
    if ((missing == 0)); then
      state=$(say "встановлено" "installed")
    elif ((missing < total)); then
      state=$(say "частково: бракує $missing з $total пакетів" "partly: $missing of $total packages missing")
    else
      state=$(say "не встановлено" "not installed")
    fi
    pad "$name" 8
    echo "$title — $state"
    echo "         $desc"
  done
}

cmd_show() {
  local p id name
  load "$1"
  echo "$title: $desc"
  say "Пакети (* — уже встановлені):" "Packages (* marks the installed ones):"
  while read -r p; do
    if installed "$p"; then echo "  * $p"; else echo "    $p"; fi
  done <<<"$packages"
  [[ -z $services ]] || say "Служби: $services" "Services: $services"
  [[ -z $groups ]] || say "Групи користувача: $groups" "User groups: $groups"
  if [[ -n $apps ]]; then
    say "Додатково з Flathub (dprofile app add ID):" "Optional, from Flathub (dprofile app add ID):"
    while read -r id name; do
      if app_installed "$id"; then echo "  * $(pad "$name" 30)$id"; else echo "    $(pad "$name" 30)$id"; fi
    done <<<"$apps"
  fi
}

cmd_apps() {
  local id name
  load "$1"
  [[ -n $apps ]] || {
    say "У профілю $1 немає додаткових застосунків (або тут немає графічного сеансу)." \
      "The $1 profile has no optional applications (or there is no graphical session here)."
    return
  }
  while read -r id name; do
    if app_installed "$id"; then echo "* $(pad "$name" 30)$id"; else echo "  $(pad "$name" 30)$id"; fi
  done <<<"$apps"
}

# resolve_app SPEC: the Flathub ID of SPEC, a full ID or its last part in any case
resolve_app() {
  local spec=$1 name id found=""
  if [[ $spec == *.*.* ]]; then
    echo "$spec"
    return
  fi
  shopt -s nocasematch
  for name in $(profile_names); do
    load "$name"
    while read -r id _; do
      [[ -n $id && ${id##*.} == "$spec" ]] && found=$id
    done <<<"$apps"
  done
  shopt -u nocasematch
  [[ -n $found ]] || die "невідомий застосунок '$spec': повний ID з Flathub або назва з dprofile apps" \
    "unknown application '$spec': a full Flathub ID or a name from dprofile apps"
  echo "$found"
}

setup_flathub() {
  flatpak remote-add --system --if-not-exists flathub "$flathub"
}

# app_add ID...: installs Flathub applications system-wide
app_add() {
  (($# > 0)) || return 0
  command -v flatpak >/dev/null ||
    die "немає flatpak: спершу dprofile install dev" "flatpak is missing: dprofile install dev first"
  setup_flathub
  say "Встановлюю з Flathub: $*" "Installing from Flathub: $*"
  # The exports are in the users' XDG_DATA_DIRS (/etc/profile.d/flatpak.sh),
  # not in root's: without them flatpak warns that the menu won't find the apps
  XDG_DATA_DIRS=/var/lib/flatpak/exports/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share} \
    flatpak install --system --noninteractive -y flathub "$@"
  say "Застосунки з'являться в меню після наступного входу." "The applications appear in the menu after the next login."
}

cmd_app() {
  local action=${1:-} ids=() spec
  shift || true
  desktop || die "застосунки з Flathub — лише для системи з графічним сеансом" \
    "Flathub applications are only for a system with a graphical session"
  (($# > 0)) || die "вкажіть застосунок: dprofile app $action GoLand" "give an application: dprofile app $action GoLand"
  for spec in "$@"; do
    ids+=("$(resolve_app "$spec")")
  done
  case $action in
    add | install)
      need_root app add "${ids[@]}"
      app_add "${ids[@]}"
      ;;
    rm | remove)
      need_root app rm "${ids[@]}"
      flatpak uninstall --system --noninteractive -y "${ids[@]}"
      # The runtimes no other application uses take most of the space
      flatpak uninstall --system --noninteractive -y --unused
      ;;
    *) die "dprofile app add|rm ID..." "dprofile app add|rm ID..." ;;
  esac
}

# choose_apps: asks which optional applications to install into "chosen"
choose_apps() {
  local i=0 id name ids=() reply n ok
  say "Додаткові середовища з Flathub (великі: 1–4 ГБ разом із рантаймами Flatpak):" \
    "Optional IDEs from Flathub (large: 1-4 GB with the Flatpak runtimes):"
  while read -r id name; do
    ids+=("$id")
    i=$((i + 1))
    if app_installed "$id"; then
      echo "  $i) $(pad "$name" 30)$(say "уже встановлено" "already installed")"
    else
      echo "  $i) $name"
    fi
  done <<<"$apps"
  while true; do
    chosen=()
    ok=1
    read -r -p "$(say "Номери через пробіл (Enter — жодного): " "Numbers separated by spaces (Enter for none): ")" reply || return 0
    for n in ${reply//,/ }; do
      if [[ $n =~ ^[0-9]+$ ]] && ((n >= 1 && n <= ${#ids[@]})); then
        chosen+=("${ids[n - 1]}")
      else
        warn "'$n': номер від 1 до ${#ids[@]}" "'$n': a number from 1 to ${#ids[@]}"
        ok=""
      fi
    done
    [[ -z $ok ]] || return 0
  done
}

# snapper pre/post pair around the installation, when snapshots are set up
snapshot_pre() {
  [[ -e /etc/snapper/configs/root ]] || return 0
  snapper -c root create --type pre --print-number --cleanup-algorithm number \
    --description "dprofile install $1"
}

snapshot_post() {
  [[ -n $1 ]] || return 0
  snapper -c root create --type post --pre-number "$1" --cleanup-algorithm number \
    --description "dprofile install $2" || true
}

# add_subids USER: subordinate IDs for rootless containers, after the
# highest range already given out
add_subids() {
  local user=$1 file start
  for file in /etc/subuid /etc/subgid; do
    touch "$file"
    grep -q "^$user:" "$file" && continue
    start=$(awk -F: 'BEGIN { m = 100000 } { if ($2 + $3 > m) m = $2 + $3 } END { print m }' "$file")
    if [[ $file == /etc/subuid ]]; then
      usermod --add-subuids "$start-$((start + 65535))" "$user"
    else
      usermod --add-subgids "$start-$((start + 65535))" "$user"
    fi
  done
}

# Mount propagation for rootless containers, at every boot and right now
make_root_shared() {
  local f=/etc/runit/core-services/60-das-shared-root.sh
  [[ -e $f ]] && return
  cat >"$f" <<'EOF'
# Created by dprofile: rootless Podman wants / to be a shared mount
mount --make-rshared /
EOF
  mount --make-rshared /
}

cmd_install() {
  local name="" yes="" asked="" app_spec="" user=${SUDO_USER:-} missing chosen=() id pre="" sv g joined=""
  while (($# > 0)); do
    case $1 in
      -y | --yes) yes=1 ;;
      --asked) asked=1 ;;
      --apps) app_spec=${2:-}; shift ;;
      --apps=*) app_spec=${1#--apps=} ;;
      -*) die "невідомий параметр $1" "unknown option $1" ;;
      *) name=$1 ;;
    esac
    shift
  done
  [[ -n $name ]] || die "вкажіть профіль: dprofile install dev" "give a profile: dprofile install dev"
  load "$name"
  live && die "це live-сесія: спершу встановіть Das OS на диск" "this is a live session: install Das OS to a disk first"
  [[ $user == root ]] && user=""
  missing=$(missing_packages | xargs)

  # The questions come before sudo, so the password is asked for a known plan;
  # --asked is the run through sudo after them
  if [[ -z $asked ]]; then
    echo "$title: $desc"
    if [[ -n $missing ]]; then
      say "Буде встановлено: $missing" "To be installed: $missing"
    else
      say "Усі пакети вже встановлено." "All packages are installed already."
    fi
    if [[ -n $apps && -z $app_spec && -z $yes ]] && interactive; then
      choose_apps
      app_spec=$(IFS=,; echo "${chosen[*]}")
      app_spec=${app_spec:-none}
    fi
    if [[ -z $yes ]] && interactive; then
      confirm "Продовжити?" "Continue?" || exit 1
    fi
  fi
  need_root install "$name" --asked ${app_spec:+--apps "$app_spec"}

  chosen=()
  if [[ -n $apps ]]; then
    case $app_spec in
      "" | none) ;;
      all) mapfile -t chosen < <(awk '{ print $1 }' <<<"$apps") ;;
      *)
        for id in ${app_spec//,/ }; do
          chosen+=("$(resolve_app "$id")")
        done
        ;;
    esac
  fi

  if [[ -n $missing ]]; then
    xbps-install -S
    # xbps refuses to install anything while an update of itself is pending
    if xbps-install -un 2>/dev/null | grep -q '^xbps-[0-9]'; then
      xbps-install -uy xbps
    fi
    pre=$(snapshot_pre "$name") ||
      die "не вдалося створити знімок системи, нічого не встановлено" "could not take a system snapshot, nothing was installed"
    # shellcheck disable=SC2086
    if ! xbps-install -y $missing; then
      snapshot_post "$pre" "$name"
      die "xbps-install не вдався" "xbps-install failed"
    fi
    snapshot_post "$pre" "$name"
  fi

  for sv in $services; do
    if [[ -d /etc/sv/$sv && ! -e /etc/runit/runsvdir/default/$sv ]]; then
      ln -s "/etc/sv/$sv" /etc/runit/runsvdir/default/
      say "Служба $sv увімкнена" "Service $sv enabled"
    fi
  done

  [[ -z $shared_root ]] || make_root_shared

  if [[ -n $user ]]; then
    for g in $groups; do
      if getent group "$g" >/dev/null && ! id -nG "$user" | grep -qw "$g"; then
        gpasswd -a "$user" "$g" >/dev/null
        joined+=" $g"
      fi
    done
    [[ -n $subids ]] && add_subids "$user"
    [[ -z $joined ]] || say "$user тепер у групах:$joined (діє з наступного входу)" \
      "$user is now in the groups:$joined (from the next login)"
  elif [[ -n $groups ]]; then
    say "Додайте користувачів до груп самі: duser groups КОРИСТУВАЧ +${groups// / +}" \
      "Add users to the groups yourself: duser groups USER +${groups// / +}"
  fi

  if command -v flatpak >/dev/null && desktop; then
    setup_flathub
    app_add "${chosen[@]}"
  fi

  say "Профіль $name встановлено." "The $name profile is installed."
  [[ -z $pre ]] || say "Відкат: у меню завантаження «Das OS snapshots» → знімок №$pre, потім sudo snapper-rollback $pre." \
    "To roll back: \"Das OS snapshots\" → snapshot #$pre in the boot menu, then sudo snapper-rollback $pre."
}

cmd=${1:-list}
[[ $# -gt 0 ]] && shift

case $cmd in
  list | ls) cmd_list ;;
  show | info)
    [[ $# -gt 0 ]] || die "вкажіть профіль: dprofile show dev" "give a profile: dprofile show dev"
    cmd_show "$1"
    ;;
  # Quiet, for das-welcome: success when every package is installed
  check)
    load "${1:-}"
    [[ -z $(missing_packages) ]]
    ;;
  install) cmd_install "$@" ;;
  apps)
    [[ $# -gt 0 ]] || die "вкажіть профіль: dprofile apps dev" "give a profile: dprofile apps dev"
    cmd_apps "$1"
    ;;
  app) cmd_app "$@" ;;
  -h | --help | help) usage ;;
  *)
    usage >&2
    exit 1
    ;;
esac
