#!/bin/sh
# dacty installer (docs/steps/dacty_011.md step 6). Per user; asks for sudo only to install packages.
#
#   curl -fsSL https://kyryllko.tech/install.sh | sh                    install or update
#   curl -fsSL https://kyryllko.tech/install.sh | sh -s -- --no-ai      without the local AI model
#   curl -fsSL https://kyryllko.tech/install.sh | sh -s -- --uninstall  remove (keeps your data)
#   curl -fsSL https://kyryllko.tech/install.sh | sh -s -- --purge      remove with settings and learned words
#
# What it does, in order:
#   1. downloads the dacty binary (checked with sha256) to ~/.local/bin/dacty
#   2. installs the Fcitx5 GTK/Qt input modules (and llama-server for AI) with your package manager
#   3. sets GTK_IM_MODULE/QT_IM_MODULE=fcitx for your X11 session (~/.xsessionrc, ~/.xprofile)
#   4. starts dacty at login (~/.config/autostart/dacty.desktop)
#   5. downloads the English-Polish translation models (Firefox's, from Mozilla)
#   6. with AI: downloads Qwen3-0.6B (397 MB, sha256 checked) and runs it locally as dacty-llm.service
# Nothing you type leaves the computer: dacty, translation and AI all run locally.
set -eu

BASE=${DACTY_BASE:-https://kyryllko.tech/dacty}
LANGS=${DACTY_LANGS:-pl}
AI=${DACTY_AI:-1}
MODE=install
for arg in "$@"; do
  case $arg in
    --uninstall) MODE=uninstall ;;
    --purge) MODE=purge ;;
    --no-ai) AI=0 ;;
    --lang=*) LANGS=${arg#--lang=} ;;
    -h|--help) sed -n '2,16p' "$0" 2>/dev/null || true; exit 0 ;;
    *) echo "dacty: unknown option $arg" >&2; exit 2 ;;
  esac
done

BIN_DIR=$HOME/.local/bin
BIN=$BIN_DIR/dacty
DATA=${XDG_DATA_HOME:-$HOME/.local/share}/dacty
CONF=${XDG_CONFIG_HOME:-$HOME/.config}
STATE=${XDG_STATE_HOME:-$HOME/.local/state}/dacty
UNIT=$CONF/systemd/user/dacty-llm.service
MODEL=Qwen3-0.6B-Q4_K_M.gguf
MODEL_URL=https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/$MODEL
MODEL_SHA=ac2d97712095a558e31573f62f466a3f9d93990898b0ec79d7c974c1780d524a
# llama.cpp's own build, used when the distribution has no llama-server (Ubuntu 24.04) or an old one;
# sha256 = the digest GitHub publishes for the asset (docs/steps/dacty_011_logs.md step 8).
LLAMA_TAG=b12182
LLAMA_URL=https://github.com/ggml-org/llama.cpp/releases/download/$LLAMA_TAG/llama-$LLAMA_TAG-bin-ubuntu-x64.tar.gz
LLAMA_SHA=c1dbfdb9cbef7ee50b6ab08142d6424184d68b242906837770439ed118c57d99
BEGIN='# >>> dacty >>>'
END='# <<< dacty <<<'

if [ -t 1 ]; then Y=$(printf '\033[1;33m'); G=$(printf '\033[1;32m'); N=$(printf '\033[0m'); else Y= G= N=; fi
say() { printf '%s==>%s %s\n' "$Y" "$N" "$*"; }
die() { printf 'dacty: %s\n' "$*" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }

as_root() {
  if [ "$(id -u)" = 0 ]; then "$@"
  elif have sudo; then sudo "$@"
  else return 1
  fi
}

remove_block() {
  [ -f "$1" ] || return 0
  sed -i "/^$BEGIN\$/,/^$END\$/d" "$1"
}

add_block() {
  remove_block "$1"
  printf '%s\nexport GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx\n%s\n' "$BEGIN" "$END" >> "$1"
}

stop_daemon() {
  # Only the daemon (no arguments), never `dacty status` or the helper.
  for p in $(pgrep -u "$(id -u)" -x dacty 2>/dev/null || true); do
    if [ "$(tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null)" = "$BIN " ]; then kill "$p" 2>/dev/null || true; fi
  done
}

uninstall() {
  say "removing dacty"
  stop_daemon
  if [ -f "$UNIT" ]; then
    systemctl --user disable --now dacty-llm.service >/dev/null 2>&1 || true
    rm -f "$UNIT"
    systemctl --user daemon-reload >/dev/null 2>&1 || true
  fi
  rm -f "$BIN" "$CONF/autostart/dacty.desktop" "$CONF/environment.d/90-dacty.conf"
  if grep -qs '^X-Dacty=hidden' "$CONF/autostart/org.fcitx.Fcitx5.desktop"; then rm -f "$CONF/autostart/org.fcitx.Fcitx5.desktop"; fi
  remove_block "$HOME/.xsessionrc"
  remove_block "$HOME/.xprofile"
  if [ "$MODE" = purge ]; then
    rm -rf "$DATA" "$CONF/dacty" "$STATE"
    say "settings, learned words and models deleted"
  else
    say "kept: settings ($CONF/dacty), learned words and models ($DATA); --purge deletes them"
  fi
  say "the Fcitx5 input modules and llama.cpp stay installed (remove them with your package manager)"
  printf '%sdacty is removed.%s Log out and back in to finish.\n' "$G" "$N"
}

packages() {
  gui="" ai=""
  if have apt-get; then
    gui="fcitx5-frontend-gtk3 fcitx5-frontend-gtk4 fcitx5-frontend-qt5 fcitx5-frontend-qt6"; ai="llama.cpp-tools"
    install_cmd() { as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends "$@"; }
    as_root apt-get update -qq >/dev/null 2>&1 || true
  elif have pacman; then
    gui="fcitx5-gtk fcitx5-qt"; ai="llama-cpp"
    install_cmd() { as_root pacman -S --needed --noconfirm "$@"; }
  elif have dnf; then
    gui="fcitx5-gtk fcitx5-qt"; ai="llama-cpp"
    install_cmd() { as_root dnf install -y -q "$@"; }
  elif have zypper; then
    gui="fcitx5-gtk3 fcitx5-gtk4 fcitx5-qt5 fcitx5-qt6"; ai="llamacpp"
    install_cmd() { as_root zypper --non-interactive install "$@"; }
  else
    say "unknown package manager: install the Fcitx5 GTK and Qt input modules yourself"
    return 0
  fi
  [ "$AI" = 1 ] || ai=""
  say "installing the input modules${ai:+ and llama-server} (asks for your password)"
  # shellcheck disable=SC2086
  if ! install_cmd $gui $ai >/dev/null 2>&1; then
    for p in $gui $ai; do
      install_cmd "$p" >/dev/null 2>&1 || say "package $p is not available here (skipped)"
    done
  fi
}

binary() {
  case $(uname -s)-$(uname -m) in
    Linux-x86_64) ;;
    *) die "only Linux on x86_64 is supported for now" ;;
  esac
  tmp=$(mktemp -d)
  trap 'rm -rf "$tmp"' EXIT
  say "downloading dacty"
  curl -fsSL "$BASE/dacty-x86_64-linux.gz" -o "$tmp/dacty.gz" || die "download failed ($BASE)"
  curl -fsSL "$BASE/dacty-x86_64-linux.sha256" -o "$tmp/sha256" || die "checksum download failed"
  gunzip "$tmp/dacty.gz"
  want=$(cut -d' ' -f1 "$tmp/sha256")
  got=$(sha256sum "$tmp/dacty" | cut -d' ' -f1)
  [ "$want" = "$got" ] || die "checksum mismatch: the download is broken, nothing was changed"
  mkdir -p "$BIN_DIR"
  install -m 755 "$tmp/dacty" "$BIN"
}

session() {
  add_block "$HOME/.xsessionrc"
  add_block "$HOME/.xprofile"
  mkdir -p "$CONF/environment.d" "$CONF/autostart"
  printf 'GTK_IM_MODULE=fcitx\nQT_IM_MODULE=fcitx\n' > "$CONF/environment.d/90-dacty.conf"
  cat > "$CONF/autostart/dacty.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=dacty
Comment=Active typing: suggestions, fixes and inline commands in every app
Exec="$BIN"
Icon=input-keyboard
Terminal=false
NoDisplay=true
X-GNOME-Autostart-enabled=true
EOF
  # dacty takes Fcitx5's place on the session bus; a Fcitx5 started at login would block it.
  if [ -e /etc/xdg/autostart/org.fcitx.Fcitx5.desktop ]; then
    printf '[Desktop Entry]\nType=Application\nName=Fcitx 5\nHidden=true\nX-Dacty=hidden\n' > "$CONF/autostart/org.fcitx.Fcitx5.desktop"
  fi
}

languages() {
  for code in $LANGS; do
    say "downloading the translation models for :$code (about 70 MB)"
    "$BIN" models add "$code" || say "translation models for $code failed; later: dacty models add $code"
  done
}

llama_release() {
  dir=$DATA/llama/llama-$LLAMA_TAG
  if [ ! -x "$dir/llama-server" ]; then
    say "downloading llama.cpp $LLAMA_TAG (23 MB, from its GitHub release)"
    mkdir -p "$DATA/llama"
    curl -fsSL "$LLAMA_URL" -o "$DATA/llama/llama.tgz" || return 1
    if ! printf '%s  %s\n' "$LLAMA_SHA" "$DATA/llama/llama.tgz" | sha256sum -c --status >/dev/null 2>&1; then
      rm -f "$DATA/llama/llama.tgz"
      say "llama.cpp checksum mismatch: skipped"
      return 1
    fi
    tar xzf "$DATA/llama/llama.tgz" -C "$DATA/llama" && rm -f "$DATA/llama/llama.tgz"
  fi
  [ -x "$dir/llama-server" ] && server=$dir/llama-server
}

ai() {
  [ "$AI" = 1 ] || { say "AI skipped (--no-ai)"; return 0; }
  mem=$(awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo)
  [ "$mem" -ge 3000 ] || { say "less than 3 GB of memory: AI skipped (the model needs about 800 MB)"; return 0; }
  server=$(command -v llama-server || true)
  if [ -n "$server" ] && ! "$server" --help 2>/dev/null | grep -q -- '--sleep-idle-seconds'; then server=""; fi
  [ -n "$server" ] || llama_release || true
  [ -n "$server" ] || { say "llama-server is not available: AI commands stay off"; return 0; }
  mkdir -p "$DATA/models"
  if ! printf '%s  %s\n' "$MODEL_SHA" "$DATA/models/$MODEL" | sha256sum -c --status >/dev/null 2>&1; then
    say "downloading the AI model Qwen3-0.6B (397 MB, once)"
    curl -fL --progress-bar "$MODEL_URL" -o "$DATA/models/$MODEL.part" || { say "AI model download failed"; return 0; }
    printf '%s  %s\n' "$MODEL_SHA" "$DATA/models/$MODEL.part" | sha256sum -c --status || { rm -f "$DATA/models/$MODEL.part"; say "AI model checksum mismatch: skipped"; return 0; }
    mv "$DATA/models/$MODEL.part" "$DATA/models/$MODEL"
  fi
  ln -sf "$MODEL" "$DATA/models/default.gguf"
  mkdir -p "$(dirname "$UNIT")"
  # Flags and hardening from packaging/systemd/dacty-llm.service (docs/steps/dacty_004.md); no
  # --cors-origins: llama-server 8681 (Ubuntu 26.04) does not have it (dacty_011_logs.md).
  cat > "$UNIT" <<EOF
[Unit]
Description=dacty - local AI model for :formal :casual :friendly :short

[Service]
Type=exec
StateDirectory=dacty
ExecStartPre=/bin/sh -c 'f="\$STATE_DIRECTORY/llm-api-key"; if [ ! -s "\$f" ]; then umask 077; head -c 32 /dev/urandom | base64 -w 0 > "\$f"; printf "\\\\n" >> "\$f"; chmod 600 "\$f"; fi'
ExecStart=$server -m %h/.local/share/dacty/models/default.gguf --host 127.0.0.1 --port 8095 -c 2048 -np 1 -fa on -ctk q8_0 -ctv q8_0 -t 4 --cache-ram 0 --reasoning off --no-webui --sleep-idle-seconds 60 --api-key-file \${STATE_DIRECTORY}/llm-api-key
Restart=on-failure
RestartSec=5s
Nice=10
MemoryMax=1G
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=yes
NoNewPrivileges=yes
LockPersonality=yes
UMask=0077
RestrictAddressFamilies=AF_UNIX AF_INET
IPAddressDeny=any
IPAddressAllow=localhost

[Install]
WantedBy=default.target
EOF
  if systemctl --user daemon-reload >/dev/null 2>&1 && systemctl --user enable --now dacty-llm.service >/dev/null 2>&1; then
    say "local AI running (it sleeps after a quiet minute)"
  else
    say "start the AI later with: systemctl --user enable --now dacty-llm.service"
  fi
}

install_all() {
  [ "$(id -u)" != 0 ] || die "run this as your own user, not root (it asks for sudo when needed)"
  have curl || die "curl is needed"
  binary
  packages
  session
  languages
  ai
  if pgrep -u "$(id -u)" -x dacty >/dev/null 2>&1; then stop_daemon; fi
  if [ -n "${DISPLAY:-}" ]; then (setsid "$BIN" >/dev/null 2>&1 &) ; fi
  if [ "${XDG_SESSION_TYPE:-x11}" = wayland ]; then
    say "this is a Wayland session: dacty works on X11 for now (choose an Xorg/X11 session at login)"
  fi
  if have fcitx5; then
    say "Fcitx5 is installed: while dacty runs it takes Fcitx5's place"
  fi
  printf '\n%sdacty is installed.%s Log out and back in (or restart) once, then type in any app:\n' "$G" "$N"
  printf '  suggestions appear as you type - Alt+1..5 picks\n'
  printf '  mistakes are offered as fixes after a word   - Alt+1 fixes\n'
  printf '  end a line with :pl :en :fix :formal :casual :short and a space - Alt+1 replaces it\n'
  printf '  dacty status | dacty config list | dacty help   (in a new terminal)\n'
}

case $MODE in
  install) install_all ;;
  uninstall|purge) uninstall ;;
esac
