#!/usr/bin/env bash
# Astitvaams Print Helper — installer for Linux, macOS and Raspberry Pi OS.
#
# Safe to run again: an existing install is upgraded in place and the service
# restarted, so "reinstall to fix it" and "update to the new version" are the
# same command. Nothing is asked twice.
set -euo pipefail

APP="ams-print-helper"
AGENT="com.astitva.$APP"          # macOS LaunchAgent label - must match selfinstall.py
LEGACY_AGENT="com.aams.printhelper"
DIR="${AMS_HELPER_DIR:-$HOME/.$APP}"
PY="${PYTHON:-python3}"
OS="$(uname -s)"

SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"

# Double-clicked from Files? Then there is no terminal, and every message this
# script prints - including the ones that say what to do next - goes nowhere.
# Re-open inside a terminal so a failure is readable instead of a window that
# blinks and disappears.
if [ -z "${AMS_HELPER_REEXEC:-}" ] && [ ! -t 1 ] \
   && [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then
  for T in x-terminal-emulator gnome-terminal konsole xfce4-terminal \
           mate-terminal tilix xterm; do
    command -v "$T" >/dev/null 2>&1 || continue
    export AMS_HELPER_REEXEC=1
    case "$T" in
      gnome-terminal|tilix) exec "$T" -- bash "$SELF" ;;
      *)                    exec "$T" -e bash "$SELF" ;;
    esac
  done
fi

# Is there a real terminal to ask a question on?
#
# `[ -r /dev/tty ]` is not the test: the device node exists and looks readable
# even when there is no controlling terminal, and opening it then fails with
# "No such device or address" - printed by the shell, over the top of the
# question. Actually open it. And when there is none, never fall through to a
# sudo that would sit waiting for a password nobody can type.
has_tty() { { true </dev/tty; } 2>/dev/null; }

say() { printf '\n\033[1m%s\033[0m\n' "$1"; }

DIED=0
die() { DIED=1; printf '\n\033[31m%s\033[0m\n' "$1" >&2; exit 1; }

# `set -e` exits silently on any unchecked failure, and a terminal opened by a
# file manager closes the instant the script ends. Together that is a window
# that flashes and vanishes with no message at all - which is exactly what it
# did on Ubuntu. Always say something, and always wait to be read.
on_exit() {
  rc=$?
  if [ "$rc" -ne 0 ] && [ "$DIED" -eq 0 ]; then
    printf '\n\033[31mThe installer stopped unexpectedly (code %s).\033[0m\n' "$rc"
    printf 'Nothing on your computer needs undoing. You can run this again.\n'
  fi
  if [ -t 0 ]; then
    printf '\nPress Enter to close this window. '
    read -r _ || true
  fi
}
trap on_exit EXIT

# Offer to install Python rather than stopping at "Python is required" — the
# person running this is the one standing next to the printer, not an admin, and
# a dead end here is where the whole thing gets abandoned.
if ! command -v "$PY" >/dev/null 2>&1; then
  if command -v apt-get >/dev/null 2>&1; then
    printf '\nPython 3 is needed and is not installed.\n'
    printf 'Install it now? You will be asked for your password. [Y/n] '
    YN="n"
    if has_tty; then read -r YN </dev/tty || YN="n"; YN="${YN:-Y}"; fi
    case "$YN" in
      [Yy]*) sudo apt-get update -qq && sudo apt-get install -y python3 python3-venv \
                || die "Could not install Python. Ask your IT team for 'python3'." ;;
      *) die "Cannot continue without Python 3." ;;
    esac
  elif command -v brew >/dev/null 2>&1; then
    printf '\nPython 3 is needed and is not installed. Installing…\n'
    brew install python || die "Could not install Python. Ask your IT team for 'python3'."
  else
    die "Python 3 is required. Install it from https://www.python.org/downloads/ then run this again."
  fi
fi
command -v "$PY" >/dev/null 2>&1 || die "Python 3 is required. Install it, then run this again."
"$PY" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 9) else 1)' \
  || die "Python 3.9 or newer is required (found $($PY -V 2>&1))."

EXISTING=""
[ -f "$DIR/version.txt" ] && EXISTING="$(cat "$DIR/version.txt" 2>/dev/null || true)"
[ -n "$EXISTING" ] && say "Updating Astitvaams Print Helper (installed: $EXISTING)" || say "Installing Astitvaams Print Helper"

mkdir -p "$DIR"
cp "$(dirname "$0")/ams_print_helper.py" "$DIR/"
cp "$(dirname "$0")/requirements.txt" "$DIR/"
# setup_ui and selfinstall are imported lazily, so a missing one is not noticed
# until the user opens the setup page and gets a blank error.
cp "$(dirname "$0")/setup_ui.py" "$DIR/"
cp "$(dirname "$0")/selfinstall.py" "$DIR/"

# Which Astitvaams site may drive the printer. A browser refuses to call this helper
# from a page it has not been told to trust, and the refusal happens before the
# request arrives — so without this the printer looks broken and the log is
# empty. Asked here rather than hardcoded: every deployment has its own address.
ORIGINS_FILE="$DIR/origins.txt"
SHIPPED_ORIGINS="$(dirname "$0")/origins.txt"

# The printer Astitvaams already has registered, roll type included. Copied so
# the setup page's test label uses the same roll as a real label instead of
# guessing one. Re-copied on every install, because it changes in Astitvaams.
SHIPPED_PRINTER="$(dirname "$0")/printer.json"
if [ -f "$SHIPPED_PRINTER" ]; then
  cp "$SHIPPED_PRINTER" "$DIR/printer.json"
  say "Printer settings taken from Astitvaams"
fi
if [ -n "${AMS_HELPER_SITE:-}" ]; then
  printf '%s\n' "$AMS_HELPER_SITE" > "$ORIGINS_FILE"
  say "Astitvaams address set to $AMS_HELPER_SITE"
# The download came from Astitvaams, so Astitvaams already put its own address in the zip.
# Nothing to ask, nothing to type, nothing to get wrong.
elif [ -f "$SHIPPED_ORIGINS" ]; then
  cp "$SHIPPED_ORIGINS" "$ORIGINS_FILE"
  say "Astitvaams address set to $(grep -m1 -E '^[[:space:]]*[^#[:space:]]' "$SHIPPED_ORIGINS" || echo '?')"
# NOT `[ ! -s ]`: skipping the prompt writes a comment-only template, which is
# a non-empty file. The next run therefore never asked again, while the helper —
# which strips comments — still had zero configured origins. Every re-install
# printed "Updating..." and left the browser blocking every request with
#   No 'Access-Control-Allow-Origin' header is present
# Ask whenever there is no REAL address in the file.
elif ! grep -qE '^[[:space:]]*[^#[:space:]]' "$ORIGINS_FILE" 2>/dev/null; then
  if [ -t 0 ]; then
    printf '\nWhat is your Astitvaams web address? (e.g. https://aams.yourcompany.com)\n'
    printf 'Press Enter to skip — local development works without it.\n> '
    read -r SITE || SITE=""
    if [ -n "$SITE" ]; then
      printf '%s\n' "$SITE" > "$ORIGINS_FILE"
      say "Astitvaams address saved"
    fi
  else
    # Non-interactive install (scripted rollout): leave a template so the
    # address can be filled in without reinstalling.
    printf '# One Astitvaams web address per line, e.g. https://aams.yourcompany.com\n' > "$ORIGINS_FILE"
  fi
fi

say "Setting up (this takes a minute the first time)"

# A virtual environment is the tidy option, but plenty of Debian/Ubuntu systems
# ship Python without python3-venv and installing it needs root. Rather than
# dead-end someone who only wants to print a label, fall back to a private
# package directory — no sudo, no system packages touched either way.
# Ubuntu and Debian ship python3 on its own: `venv` and `pip` are SEPARATE apt
# packages that a desktop image does not include. So a laptop that genuinely
# "already has Python" still fails here — and it used to fail with "check the
# internet connection, or a firewall blocking pypi.org", which is the one thing
# that was never wrong. Detect it, name it, and offer to fix it.
SETUP_LOG="$DIR/setup.log"
RUN_PY=""
PIP_IN_LIBS=0

try_venv()  { rm -rf "$DIR/venv"; "$PY" -m venv "$DIR/venv" >"$SETUP_LOG" 2>&1; }
have_pip()  { "$PY" -m pip --version >/dev/null 2>&1; }

# Get pip without root, when apt is unavailable or the password is refused.
ensure_pip() {
  have_pip && return 0
  "$PY" -m ensurepip --default-pip >>"$SETUP_LOG" 2>&1 && have_pip && return 0
  GETPIP="$DIR/get-pip.py"
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL https://bootstrap.pypa.io/get-pip.py -o "$GETPIP" 2>>"$SETUP_LOG" || return 1
  elif command -v wget >/dev/null 2>&1; then
    wget -qO "$GETPIP" https://bootstrap.pypa.io/get-pip.py 2>>"$SETUP_LOG" || return 1
  else
    return 1
  fi
  # Into the private folder, so nothing system-wide is touched and PEP 668
  # ("externally-managed-environment") never applies.
  "$PY" "$GETPIP" --target "$DIR/libs" --no-warn-script-location >>"$SETUP_LOG" 2>&1 || return 1
  PIP_IN_LIBS=1
  have_pip || PIP_IN_LIBS=1
  return 0
}

if try_venv; then
  RUN_PY="$DIR/venv/bin/python"
elif command -v apt-get >/dev/null 2>&1; then
  YN="n"
  if has_tty && command -v sudo >/dev/null 2>&1; then
    printf '\nPython is here, but two pieces it needs are packaged separately on\n'
    printf 'Ubuntu and Debian, and are not installed:\n'
    printf '    python3-venv  python3-pip\n\n'
    printf 'Install them now? You will be asked for your password. [Y/n] '
    read -r YN </dev/tty || YN="n"
    YN="${YN:-Y}"
  fi
  case "$YN" in
    [Yy]*)
      sudo apt-get update -qq \
        && sudo apt-get install -y python3-venv python3-pip \
        || say "  (could not install them — trying without admin rights instead)"
      try_venv && RUN_PY="$DIR/venv/bin/python" || true
      ;;
    *) say "  (skipped — trying without admin rights instead)" ;;
  esac
fi

if [ -n "$RUN_PY" ]; then
  "$RUN_PY" -m pip install --quiet --upgrade pip >>"$SETUP_LOG" 2>&1 || true
  # Python 3.12 stopped putting setuptools in new venvs, and one dependency
  # (packbits) is published as source only — so pip has to BUILD it, and a build
  # with no setuptools fails with "Cannot import 'setuptools.build_meta'", which
  # reads as a network error and is not one.
  "$RUN_PY" -m pip install --quiet --upgrade setuptools wheel >>"$SETUP_LOG" 2>&1 || true
  "$RUN_PY" -m pip install --quiet --prefer-binary -r "$DIR/requirements.txt" >>"$SETUP_LOG" 2>&1 \
    || die "Could not download the printing packages. Check the internet connection, or a firewall blocking pypi.org, then run this again. Details: $SETUP_LOG"
else
  say "  (installing into $DIR/libs instead — no admin rights needed)"
  ensure_pip || die "Python here has no 'venv' and no 'pip', and neither could be added automatically.
On Ubuntu or Debian, run this one line and then start this installer again:

    sudo apt install python3-venv python3-pip

Details: $SETUP_LOG"
  export PYTHONPATH="$DIR/libs"
  "$PY" -m pip install --quiet --target "$DIR/libs" --upgrade setuptools wheel >>"$SETUP_LOG" 2>&1 || true
  "$PY" -m pip install --quiet --target "$DIR/libs" --prefer-binary -r "$DIR/requirements.txt" >>"$SETUP_LOG" 2>&1 \
    || die "Could not download the printing packages. Check the internet connection, or a firewall blocking pypi.org, then run this again. Details: $SETUP_LOG"
  RUN_PY="$PY"
fi

# Prove the dependencies actually load before claiming success — a helper that
# installs cleanly and then fails on the first print is worse than one that
# refuses now.
VERSION="$(cd "$DIR" && PYTHONPATH="${PYTHONPATH:-}" "$RUN_PY" -c \
  'import brother_ql, PIL, ams_print_helper as h; print(h.__version__)' 2>/dev/null || true)"
if [ -z "$VERSION" ]; then
  WHY="$(cd "$DIR" && PYTHONPATH="${PYTHONPATH:-}" "$RUN_PY" -c \
    'import brother_ql, PIL, ams_print_helper' 2>&1 | tail -2 || true)"
  die "Installed, but the printing library will not load:
$WHY

Run this installer again. If it keeps happening, send the file $SETUP_LOG."
fi
echo "$VERSION" > "$DIR/version.txt"

# Stop the copy that is already running, BEFORE starting the new one.
#
# The port allows one listener. An old helper that keeps it makes the new one
# exit at startup with "Another copy may already be running", so the installer
# says "Installed" and Astitvaams goes on reporting the OLD version — which is
# exactly what happened: files updated to 1.4.1, /ping still answering 1.1.0.
#
# `systemctl --user enable --now` was the trap: --now starts a stopped service
# and does nothing at all to a running one. Windows never had this because its
# installer stops the process explicitly; Linux did not.
#
# pkill as well as systemctl, because a helper started by hand or by an older
# installer is not a systemd service and systemctl knows nothing about it.
port_free() {
  "$RUN_PY" -c "
import socket, sys
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(('127.0.0.1', 18726)); sys.exit(0)
except OSError:
    sys.exit(1)
finally:
    s.close()" >/dev/null 2>&1
}

# pkill is not on every minimal system, so fall back to ps. Only this user's
# processes, and only this program.
kill_helpers() {
  SIG="${1:-TERM}"
  if command -v pkill >/dev/null 2>&1; then
    pkill "-$SIG" -u "$(id -u)" -f "ams_print_helper\.py" >/dev/null 2>&1 || true
  else
    ps -eo pid=,args= 2>/dev/null | while read -r _pid _args; do
      case "$_args" in
        *ams_print_helper.py*) kill "-$SIG" "$_pid" >/dev/null 2>&1 || true ;;
      esac
    done
  fi
}

stop_running_helper() {
  systemctl --user stop "$APP.service" >/dev/null 2>&1 || true
  for A in "$AGENT" "$LEGACY_AGENT"; do
    launchctl unload "$HOME/Library/LaunchAgents/$A.plist" >/dev/null 2>&1 || true
  done
  rm -f "$HOME/Library/LaunchAgents/$LEGACY_AGENT.plist"
  kill_helpers TERM
  # Wait for the socket, then stop being polite about it. A helper that ignores
  # SIGTERM still holds the port, and the new copy would exit on the clash.
  for i in 1 2 3 4 5 6; do
    port_free && return 0
    [ "$i" = "3" ] && kill_helpers KILL
    sleep 1
  done
}
stop_running_helper

if [ "$OS" = "Darwin" ]; then
  # macOS: a LaunchAgent starts it at login and restarts it if it stops.
  #
  # ONE label, matching selfinstall.py. They used to differ - this script wrote
  # com.aams.printhelper, the setup page's "Finish & start automatically" wrote
  # com.astitva.ams-print-helper - so a Mac that had both would start two copies
  # at login. One took the port, the other failed forever, and updating one left
  # the other behind serving the old version.
  PLIST="$HOME/Library/LaunchAgents/$AGENT.plist"
  mkdir -p "$HOME/Library/LaunchAgents"
  cat > "$PLIST" <<PLIST_EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>$AGENT</string>
  <key>ProgramArguments</key>
  <!-- --serve matters: without it the helper takes the double-click path and
       opens a browser window at every single login. -->
  <array><string>$RUN_PY</string><string>$DIR/ams_print_helper.py</string><string>--serve</string></array>
  <key>EnvironmentVariables</key><dict>
    <key>PYTHONPATH</key><string>${PYTHONPATH:-}</string>
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardOutPath</key><string>$DIR/helper.log</string>
  <key>StandardErrorPath</key><string>$DIR/helper.log</string>
</dict></plist>
PLIST_EOF
  launchctl unload "$PLIST" 2>/dev/null || true
  launchctl load "$PLIST"
  say "Installed. It runs now and starts again every time you log in."
else
  # Linux / Raspberry Pi OS: a user systemd service, so no root is needed.
  UNIT_DIR="$HOME/.config/systemd/user"
  mkdir -p "$UNIT_DIR"
  cat > "$UNIT_DIR/$APP.service" <<UNIT_EOF
[Unit]
Description=Astitvaams Print Helper
After=network-online.target

[Service]
Environment=PYTHONPATH=${PYTHONPATH:-}
ExecStart=$RUN_PY $DIR/ams_print_helper.py --serve
Restart=always
RestartSec=5

[Install]
WantedBy=default.target
UNIT_EOF
  # Everything is installed by this point. A machine without systemd — WSL, a
  # container, a trimmed-down desktop — must not turn that into a failed
  # install: start the helper directly and say what will not survive a reboot.
  if command -v systemctl >/dev/null 2>&1 && systemctl --user daemon-reload 2>/dev/null; then
    systemctl --user enable "$APP.service" >/dev/null 2>&1 || true
    # restart, not `enable --now`: the unit may already be running the old code.
    systemctl --user restart "$APP.service"
    # Without this the service stops when you log out — on a machine that just
    # sits there printing, that is exactly when it is needed.
    loginctl enable-linger "$USER" 2>/dev/null || true
    say "Installed. It is running now and starts automatically on boot."
  else
    PYTHONPATH="${PYTHONPATH:-}" nohup "$RUN_PY" "$DIR/ams_print_helper.py" --serve \
      >>"$DIR/helper.log" 2>&1 &
    say "Installed and running."
    printf 'This computer has no systemd, so it will not start again by itself.\n'
    printf 'After a restart, run this installer again.\n'
  fi
fi

# Open the setup page. This is the part a non-technical person actually needs:
# type the Astitvaams address, find the printer, print one test label. Telling them to
# "check /ping" proves the program started but not that printing works, which is
# the only thing they care about.
SETUP_URL="http://127.0.0.1:18726/setup"

# Ask the running helper what it is, instead of assuming the files on disk are
# what answers. Copying files is not installing; a stale process serving the old
# version looked identical to success until Astitvaams disagreed on the number.
sleep 2
LIVE="$("$RUN_PY" -c "
import json, urllib.request
try:
    print(json.load(urllib.request.urlopen('http://127.0.0.1:18726/ping', timeout=5))['version'])
except Exception:
    print('')" 2>/dev/null || true)"

if [ "$LIVE" = "$VERSION" ]; then
  say "Running version $LIVE."
elif [ -n "$LIVE" ]; then
  die "Version $VERSION was installed, but version $LIVE is still answering on
port 18726 — an older copy is still running and Astitvaams will keep showing $LIVE.

Close it and run this installer again:

    systemctl --user restart $APP.service
    pkill -f ams_print_helper.py"
else
  die "Installed, but nothing is answering on port 18726.
See what went wrong:

    systemctl --user status $APP.service
    tail -20 $DIR/helper.log"
fi

printf '\nOpening setup in your browser…\n' 
if command -v xdg-open >/dev/null 2>&1; then xdg-open "$SETUP_URL" >/dev/null 2>&1 &
elif command -v open >/dev/null 2>&1; then open "$SETUP_URL" >/dev/null 2>&1 &
fi
printf '\nIf it did not open, go to: %s\n' "$SETUP_URL"
printf 'Enter your Astitvaams address, find your printer, print a test label.\n\n'
