#!/usr/bin/env bash
# STYGION OS installer — https://stygion.eu/os
#
#   curl -fsSL https://stygion.eu/os/install | bash
#   curl -fsSL https://stygion.eu/os/install | bash -s -- --yes --components look,bar
#
# Downloads the newest STYGION OS release from stygion.eu, checks its minisign signature
# against the key below and its SHA-256, unpacks it to ~/.local/share/stygion-os and hands
# over to `stygion-os install`, which shows what it will change and asks first.
# Never runs as root; asks for sudo only for packages you agree to and for the optional boot look.
# Back to before at any time: stygion-os remove
#
# Options: --yes (no questions) · --components a,b (look,wallpapers,bar,claude,boot)
#          --channel beta|alpha · --no-packages · --allow-downgrade · --help
# Environment: STYGION_OS_BASE (default https://stygion.eu)
#
# STYGION OS is free to use and All Rights Reserved — see LICENSE in the package.
# Copyright (c) 2026 STYGION.
set -euo pipefail

# The release key (minisign). Only a tarball signed by its private half is ever unpacked.
STYGION_OS_PUBKEY='RWR5KxPI1LyiXhN4wTus0BnrExTs3Ie4Dn9sj4367Zdi54VMRHeXoSzn'

# Everything below runs from main, called on the last line: a download cut short runs nothing.
main() {
  local base=${STYGION_OS_BASE:-https://stygion.eu}
  base=${base%/}
  local yes=0 update=0 channel="" comps="" nopkgs=0 force=0 downgrade=0
  while (($#)); do
    case $1 in
      -y | --yes) yes=1 ;;
      --update) update=1 ;;
      --force) force=1 ;;
      --no-packages) nopkgs=1 ;;
      --allow-downgrade) downgrade=1 ;;
      --channel) channel=${2:-}; shift ;;
      --channel=*) channel=${1#*=} ;;
      --components) comps=${2:-}; shift ;;
      --components=*) comps=${1#*=} ;;
      -h | --help) sed -n '2,19p' "${BASH_SOURCE[0]:-/dev/null}" 2>/dev/null | sed 's/^# \{0,1\}//' || true
                   echo "curl -fsSL https://stygion.eu/os/install | bash -s -- --help"; return 0 ;;
      *) die "$(t "unknown option" "neznámá volba"): $1" ;;
    esac
    shift
  done

  ((EUID != 0)) || die "$(t "Do not run this as root (no sudo in front). It asks for sudo itself when needed." \
                            "Nespouštěj jako root (bez sudo na začátku). O sudo si řekne sám, když je potřeba.")"
  [[ $(uname -s) == Linux ]] || die "$(t "STYGION OS is for Arch Linux + Hyprland." "STYGION OS je pro Arch Linux + Hyprland.")"
  command -v pacman >/dev/null || die "$(t "No pacman: STYGION OS is for Arch Linux (and Arch-based) + Hyprland." \
                                           "Chybí pacman: STYGION OS je pro Arch Linux (a odvozené) + Hyprland.")"
  if ! command -v Hyprland >/dev/null && ! command -v hyprctl >/dev/null; then
    ((force)) || die "$(t "Hyprland is not installed. STYGION OS is a look for Hyprland (--force to unpack anyway)." \
                          "Hyprland není nainstalovaný. STYGION OS je vzhled pro Hyprland (--force rozbalí i tak).")"
  fi
  case $base in
    https://*) PROTO='=https' ;;
    http://127.0.0.1* | http://localhost*) PROTO='=http,https' ;;   # local testing only
    *) die "STYGION_OS_BASE: https only ($base)" ;;
  esac

  local data="${XDG_DATA_HOME:-$HOME/.local/share}/stygion-os"
  local need=() tool
  for tool in curl tar gzip sha256sum; do command -v "$tool" >/dev/null || need+=("$tool"); done
  command -v jq >/dev/null || command -v python3 >/dev/null || need+=(jq)
  command -v minisign >/dev/null || command -v openssl >/dev/null || need+=(minisign)
  if ((${#need[@]})); then
    local pkgs=() n
    for n in "${need[@]}"; do case $n in sha256sum) pkgs+=(coreutils) ;; *) pkgs+=("$n") ;; esac; done
    say "$(t "Needed first:" "Nejdřív je potřeba:") ${pkgs[*]}"
    if ((yes)) && ! sudo -n true 2>/dev/null; then die "sudo pacman -S --needed ${pkgs[*]}"; fi
    ask "$(t "Install with" "Nainstalovat přes") sudo pacman -S --needed ${pkgs[*]} ?" y "$yes" || die "$(t "Stopped." "Zastaveno.")"
    sudo pacman -S --needed --noconfirm "${pkgs[@]}"
  fi

  # --- what is the newest release
  TMPD=$(mktemp -d)
  trap 'rm -rf "$TMPD"' EXIT
  local tmp=$TMPD
  local q=""; [[ -n $channel ]] && q="?channel=$channel"
  say "$(t "Asking" "Ptám se") $base $(t "for the newest STYGION OS…" "na nejnovější STYGION OS…")"
  fetch "$base/api/v1/updates/bundle/os$q" "$tmp/bundle.json" ||
    die "$(t "Could not reach $base (offline, or no release yet)." "Nedostupné $base (offline, nebo ještě žádné vydání).")"
  local version filename url sigurl size sum tag
  version=$(jget "$tmp/bundle.json" version)
  filename=$(jget "$tmp/bundle.json" filename)
  url=$(jget "$tmp/bundle.json" url)
  sigurl=$(jget "$tmp/bundle.json" signatureUrl)
  size=$(jget "$tmp/bundle.json" size)
  sum=$(jget "$tmp/bundle.json" sha256)
  tag=$(jget "$tmp/bundle.json" tag)
  # One version, one spelling: X.Y.Z, digits only, no leading zeros. Every other place that names
  # it (tag, file name, the signed comment, VERSION in the package) must say exactly the same.
  strict_version "$version" || die "$(t "bad version in the answer:" "chybná verze v odpovědi:") '$version'"
  local want="stygion-os-$version.tar.gz"
  [[ $filename == "$want" ]] || die "$(t "unexpected file name:" "nečekané jméno souboru:") '$filename'"
  [[ -z $tag || $tag == "v$version" ]] || die "$(t "tag does not match the version:" "tag nesedí s verzí:") '$tag' / $version"
  [[ -n $url && -n $sigurl ]] || die "$(t "the answer has no download address" "v odpovědi chybí adresa ke stažení")"
  url=$(absolute "$base" "$url"); sigurl=$(absolute "$base" "$sigurl")

  local installed=""
  installed=$(cat "$data/current/VERSION" 2>/dev/null || true)
  if [[ -n $installed ]] && ! strict_version "$installed"; then
    die "$(t "the installed version file is damaged ('$installed'); reinstall: stygion-os remove, then this again" \
             "soubor s nainstalovanou verzí je poškozený ('$installed'); přeinstaluj: stygion-os remove a znovu tohle")"
  fi
  if ((update)) && [[ $installed == "$version" ]] && ! ((force)); then
    say "STYGION OS $version — $(t "already the newest." "už nejnovější.")"
    return 0
  fi

  # --- download and check
  say "$(t "Downloading" "Stahuji") $want…"
  fetch "$url" "$tmp/$want" || die "$(t "download failed" "stažení selhalo")"
  fetch "$sigurl" "$tmp/$want.minisig" || die "$(t "signature download failed — nothing installed" "stažení podpisu selhalo — nic se neinstaluje")"
  if [[ $size =~ ^[0-9]+$ ]] && (( size > 0 )) && [[ $(stat -c %s "$tmp/$want") != "$size" ]]; then
    die "$(t "size does not match — nothing installed" "nesedí velikost — nic se neinstaluje")"
  fi
  if [[ -n $sum && $sum != null ]]; then
    [[ $(sha256sum "$tmp/$want" | cut -d' ' -f1) == "${sum#sha256:}" ]] ||
      die "$(t "SHA-256 does not match — nothing installed" "nesedí SHA-256 — nic se neinstaluje")"
  fi
  local trusted
  trusted=$(verify_minisign "$tmp/$want" "$tmp/$want.minisig" "$tmp") ||
    die "$(t "SIGNATURE CHECK FAILED — nothing installed. Please tell us: support@stygion.eu" \
             "KONTROLA PODPISU SELHALA — nic se neinstaluje. Dej nám vědět: support@stygion.eu")"
  [[ $trusted == "$want" ]] || die "$(t "signed for '$trusted', not '$want' — nothing installed" \
                                       "podepsáno pro '$trusted', ne '$want' — nic se neinstaluje")"
  # The version from here on is the signed one (equal to the answer's, checked just above).
  local signed=${trusted#stygion-os-}; signed=${signed%.tar.gz}
  if ! strict_version "$signed" || [[ $signed != "$version" ]]; then die "signed version '$signed'"; fi
  # A genuine but older signed release must not come back as "the newest" (rollback to a version
  # with a known hole). Going back is possible, but only when asked for by name.
  if [[ -n $installed ]] && (($(ver_cmp "$signed" "$installed") < 0)) && ! ((downgrade)); then
    die "$(t "offered $signed is older than the installed $installed — refusing (on purpose: --allow-downgrade)" \
             "nabízená $signed je starší než nainstalovaná $installed — odmítám (záměrně: --allow-downgrade)")"
  fi
  say "$(t "Signature OK" "Podpis v pořádku") (minisign, $(t "key" "klíč") ${STYGION_OS_PUBKEY:0:12}…)."

  # --- unpack next to the old version, then switch
  # parent folders made here (~/.local/share on a fresh account) go away again with `remove`
  local d=$data made=()
  while [[ ! -d $d ]]; do made=("$d" "${made[@]}"); d=$(dirname "$d"); done
  mkdir -p "$data/versions" "$data/state"
  if ((${#made[@]} > 1)); then printf '%s\n' "${made[@]:0:${#made[@]}-1}" >>"$data/state/created-parents"; fi
  local stage="$data/versions/.stage-$$"
  rm -rf "$stage"; mkdir -p "$stage"
  tar -xzf "$tmp/$want" -C "$stage" --no-same-owner --no-same-permissions
  [[ -d $stage/stygion-os-$signed && $(cat "$stage/stygion-os-$signed/VERSION" 2>/dev/null) == "$signed" ]] ||
    { rm -rf "$stage"; die "$(t "the package does not look right — nothing installed" "balík nevypadá správně — nic se neinstaluje")"; }
  chmod -R u+rwX,go-w "$stage/stygion-os-$signed"
  rm -rf "$data/versions/$signed"
  mv "$stage/stygion-os-$signed" "$data/versions/$signed"
  rmdir "$stage"
  ln -sfn "versions/$signed" "$data/current.new"
  mv -T "$data/current.new" "$data/current"
  # keep this version and the one before it
  local v
  for v in "$data"/versions/*; do
    [[ $(basename "$v") == "$signed" || $(basename "$v") == "$installed" ]] || rm -rf "$v"
  done
  say "STYGION OS $signed $(t "unpacked to" "rozbaleno do") $data"

  local cli="$data/current/bin/stygion-os" args=()
  ((yes)) && args+=(--yes)
  ((nopkgs)) && args+=(--no-packages)
  rm -rf "$TMPD"; trap - EXIT   # exec below skips the EXIT trap
  if [[ -n $installed && -f $data/state/installed ]]; then
    "$cli" _core
    exec "$cli" _reapply "${args[@]}"
  fi
  "$cli" _core
  # shellcheck disable=SC2086
  exec "$cli" install ${comps//,/ } "${args[@]}"
}

# ------------------------------------------------------------------ helpers
# strict_version v — X.Y.Z, three numbers, no leading zeros, nothing else (no v, no suffix, no spaces)
strict_version() { [[ $1 =~ ^(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})$ ]]; }

# ver_cmp a b -> -1, 0 or 1; both must pass strict_version; compared number by number
ver_cmp() {
  if ! strict_version "$1" || ! strict_version "$2"; then echo -1; return; fi   # unknown shape never counts as newer
  local -a A B; local i
  IFS=. read -r -a A <<<"$1"; IFS=. read -r -a B <<<"$2"
  for i in 0 1 2; do
    ((A[i] > B[i])) && { echo 1; return; }
    ((A[i] < B[i])) && { echo -1; return; }
  done
  echo 0
}

is_cs() { case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in cs*) return 0 ;; *) return 1 ;; esac; }
t() { if is_cs; then printf '%s' "$2"; else printf '%s' "$1"; fi; }
say() { printf '%s\n' "$*"; }
die() { printf '\033[38;2;232;69;107m✗ %s\033[0m\n' "$*" >&2; exit 1; }

ask() { # question default(y|n) yes-flag ; reads the terminal, not the pipe
  local ans hint
  (("${3:-0}")) && return 0
  [[ $2 == y ]] && hint="[Y/n]" || hint="[y/N]"
  { exec 3</dev/tty; } 2>/dev/null || die "$(t "no terminal to ask on; run with --yes" "není terminál pro otázku; spusť s --yes")"
  printf '%s %s ' "$1" "$hint" >/dev/tty
  read -r ans <&3 || ans=""
  exec 3<&-
  case "${ans:-$2}" in [yYaA]*) return 0 ;; *) return 1 ;; esac
}

fetch() { # url file
  curl -fsSL --proto "$PROTO" --proto-redir "$PROTO" --tlsv1.2 --retry 2 --connect-timeout 15 --max-time 600 \
    -H "User-Agent: stygion-os-installer" -o "$2" "$1"
}

absolute() { # base url -> url
  case $2 in
    http://* | https://*) printf '%s' "$2" ;;
    /*) printf '%s%s' "$1" "$2" ;;
    *) printf '%s/%s' "$1" "$2" ;;
  esac
}

jget() { # file key -> string value ("" when missing or null)
  if command -v jq >/dev/null; then
    jq -r --arg k "$2" '.[$k] // "" | tostring' "$1"
  else
    python3 -I -c 'import json,sys; v=json.load(open(sys.argv[1])).get(sys.argv[2]); print("" if v is None else v)' "$1" "$2"
  fi
}

# verify_minisign file sigfile workdir -> prints the trusted comment when the signature is good.
# Uses minisign when it is there; otherwise the same check with openssl (Ed25519, BLAKE2b-512).
verify_minisign() {
  local f=$1 s=$2 w=$3
  if command -v minisign >/dev/null && [[ ${STYGION_OS_VERIFY:-} != openssl ]]; then
    minisign -V -P "$STYGION_OS_PUBKEY" -m "$f" -x "$s" -Q 2>/dev/null
    return
  fi
  command -v openssl >/dev/null || return 1
  local tc
  printf '%s' "$STYGION_OS_PUBKEY" | base64 -d >"$w/pk.bin" 2>/dev/null || return 1
  sed -n 2p "$s" | base64 -d >"$w/sig.bin" 2>/dev/null || return 1
  sed -n 4p "$s" | base64 -d >"$w/gsig.bin" 2>/dev/null || return 1
  tc=$(sed -n 3p "$s")
  [[ $tc == "trusted comment: "* ]] || return 1
  tc=${tc#trusted comment: }
  [[ $(stat -c %s "$w/pk.bin") == 42 && $(stat -c %s "$w/sig.bin") == 74 && $(stat -c %s "$w/gsig.bin") == 64 ]] || return 1
  # same key id in key and signature
  cmp -s <(head -c 10 "$w/pk.bin" | tail -c 8) <(head -c 10 "$w/sig.bin" | tail -c 8) || return 1
  printf '\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00' >"$w/pub.der"
  tail -c 32 "$w/pk.bin" >>"$w/pub.der"
  openssl pkey -pubin -inform DER -in "$w/pub.der" -out "$w/pub.pem" 2>/dev/null || return 1
  case $(head -c 2 "$w/sig.bin") in
    ED) openssl dgst -blake2b512 -binary "$f" >"$w/msg.bin" || return 1 ;;   # prehashed (minisign default)
    Ed) cp "$f" "$w/msg.bin" ;;
    *) return 1 ;;
  esac
  tail -c 64 "$w/sig.bin" >"$w/s.bin"
  openssl pkeyutl -verify -pubin -inkey "$w/pub.pem" -rawin -in "$w/msg.bin" -sigfile "$w/s.bin" >/dev/null 2>&1 || return 1
  # the trusted comment is signed too (global signature over signature + comment)
  { tail -c 64 "$w/sig.bin"; printf '%s' "$tc"; } >"$w/g.bin"
  openssl pkeyutl -verify -pubin -inkey "$w/pub.pem" -rawin -in "$w/g.bin" -sigfile "$w/gsig.bin" >/dev/null 2>&1 || return 1
  printf '%s\n' "$tc"
}

main "$@"
