#!/usr/bin/env bash
#
# open-dmo employee installer (Linux x86_64, no sudo).
#
#   curl -fsSL https://cdn.jsdelivr.net/npm/opendmo/install.sh | bash
#
# Installs the opendmo binary payload into ~/.local/share/opendmo and puts an
# `opendmo` wrapper on PATH at ~/.local/bin/opendmo. Office state (~/.open-dmo)
# is never touched, by install or by --uninstall.
#
# Download source — two modes:
#   default        the public npm registry: the opendmo-linux-x64 package
#                  tarball, verified against the registry's sha512 integrity.
#   payload dir    --url <base> or OPENDMO_INSTALL_URL: an office-internal
#                  http/file base serving opendmo-linux-x64.tar.gz +
#                  SHA256SUMS (air-gapped offices, the test harness).
#
# Options (pipe form: `| bash -s -- --version 0.2.0`):
#   --url <base>       Payload-dir base URL (or OPENDMO_INSTALL_URL env var)
#   --registry <base>  npm registry base (default https://registry.npmjs.org,
#                      or the OPENDMO_NPM_REGISTRY environment variable)
#   --version <X.Y.Z>  Pin a package version (registry mode only)
#   --uninstall        Remove the installed payload and the PATH wrapper
#   --join <file>      Join the office right after installing (runs
#                      `opendmo employee create --join <file>`), making
#                      install + join one command. In pipe form there is no
#                      terminal to prompt on, so pass --employee.
#   --employee <id>    Your employee id (employees.yaml), for --join —
#                      name/email come from your roster row
#   --name "<name>"    Bootstrap only: display name for an office with no
#                      roster yet (a composed roster always wins)

set -euo pipefail

PLATFORM_PKG="opendmo-linux-x64"
PAYLOAD_TARBALL="opendmo-linux-x64.tar.gz"
INSTALL_DIR="$HOME/.local/share/opendmo"
BIN_DIR="$HOME/.local/bin"
WRAPPER="$BIN_DIR/opendmo"
DESKTOP_ENTRY="$HOME/.local/share/applications/opendmo-desktop.desktop"

# Color is presentation only: TTY stdout without NO_COLOR gets ANSI, anything
# else (pipes, CI, NO_COLOR=1) gets the same text plain. One vocabulary with
# the CLI (src/opendmo/term.ts): cyan = what you type, green = success,
# yellow = warning, red = failure, dim = labels and explanations.
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
    C_STEP=$'\033[36m' C_OK=$'\033[32m' C_WARN=$'\033[33m' C_ERR=$'\033[31m'
    C_BOLD=$'\033[1m' C_DIM=$'\033[2m' C_CMD=$'\033[36m' C_OFF=$'\033[0m'
else
    C_STEP="" C_OK="" C_WARN="" C_ERR="" C_BOLD="" C_DIM="" C_CMD="" C_OFF=""
fi
say() { printf '%s\n' "$*"; }
step() { printf '%s\n' "${C_STEP}▸${C_OFF} $*"; }
ok() { printf '%s\n' "${C_OK}✓${C_OFF} $*"; }
note() { printf '%s\n' "${C_WARN}!${C_OFF} $*"; }
section() { printf '\n%s\n' "${C_BOLD}$*${C_OFF}"; }
# A labeled row (dim 7-char label, cyan command value) and its dim sub-line,
# aligned to the value column.
kv() { printf '  %s%-7s%s  %s%s%s\n' "$C_DIM" "$1" "$C_OFF" "$C_CMD" "$2" "$C_OFF"; }
sub() { printf '           %s%s%s\n' "$C_DIM" "$*" "$C_OFF"; }
die() {
    printf '%s\n' "${C_ERR}✗ install.sh:${C_OFF} $*" >&2
    exit 1
}

BASE_URL="${OPENDMO_INSTALL_URL:-}"
REGISTRY="${OPENDMO_NPM_REGISTRY:-https://registry.npmjs.org}"
VERSION=""
UNINSTALL=false
JOIN_FILE=""
EMPLOYEE=""
NAME=""

while [[ $# -gt 0 ]]; do
    case $1 in
        --url)
            BASE_URL="${2:?--url requires a value}"
            shift 2
            ;;
        --registry)
            REGISTRY="${2:?--registry requires a value}"
            shift 2
            ;;
        --version)
            VERSION="${2:?--version requires a value}"
            shift 2
            ;;
        --uninstall)
            UNINSTALL=true
            shift
            ;;
        --join)
            JOIN_FILE="${2:?--join requires a value}"
            shift 2
            ;;
        --employee)
            EMPLOYEE="${2:?--employee requires a value}"
            shift 2
            ;;
        --name)
            NAME="${2:?--name requires a value}"
            shift 2
            ;;
        *)
            die "unknown option: $1"
            ;;
    esac
done

if [[ -n "$JOIN_FILE" && ! -f "$JOIN_FILE" ]]; then
    die "join file not found: $JOIN_FILE"
fi

if [[ "$UNINSTALL" == "true" ]]; then
    rm -rf "$INSTALL_DIR"
    rm -f "$WRAPPER" "$DESKTOP_ENTRY"
    ok "opendmo removed ($INSTALL_DIR, $WRAPPER, and the app launcher entry)."
    say "Your office data in ~/.open-dmo was left alone."
    exit 0
fi

# This phase ships one platform.
if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]; then
    die "this installer covers Linux x86_64 only for now (found: $(uname -s) $(uname -m))"
fi

if ! command -v git >/dev/null 2>&1; then
    die "git is required (opendmo office sync is built on it). Install it first: sudo apt install git"
fi

fetch() {
    # fetch <url> <out-file>
    if command -v curl >/dev/null 2>&1; then
        curl -fsSL "$1" -o "$2"
    elif command -v wget >/dev/null 2>&1; then
        wget -qO "$2" "$1"
    else
        die "curl or wget is required. Install one first: sudo apt install curl"
    fi
}

TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

if [[ -n "$BASE_URL" ]]; then
    # Payload-dir mode: office-internal base serving the release file set.
    BASE_URL="${BASE_URL%/}"
    step "Downloading $PAYLOAD_TARBALL from $BASE_URL"
    if ! fetch "$BASE_URL/$PAYLOAD_TARBALL" "$TMP_DIR/$PAYLOAD_TARBALL" || ! fetch "$BASE_URL/SHA256SUMS" "$TMP_DIR/SHA256SUMS"; then
        die "download failed from $BASE_URL — is the office payload server up?"
    fi
    step "Verifying checksum"
    (cd "$TMP_DIR" && grep " $PAYLOAD_TARBALL\$" SHA256SUMS | sha256sum --check --quiet -) ||
        die "checksum mismatch for $PAYLOAD_TARBALL — corrupted or tampered download, not installed"
    ok "Checksum verified"
    TARBALL_PATH="$TMP_DIR/$PAYLOAD_TARBALL"
else
    # Registry mode: the public npm registry serves the platform package.
    command -v openssl >/dev/null 2>&1 ||
        die "openssl is required to verify the download. Install it first: sudo apt install openssl"
    VERSION="${VERSION#v}"
    META_URL="$REGISTRY/$PLATFORM_PKG/${VERSION:-latest}"
    step "Resolving $PLATFORM_PKG ${VERSION:-latest} from ${REGISTRY#*://}"
    fetch "$META_URL" "$TMP_DIR/meta.json" ||
        die "could not resolve $META_URL — bad --version, or no network? Offices with an internal payload server: set OPENDMO_INSTALL_URL."
    TARBALL_URL="$(grep -o '"tarball":"[^"]*"' "$TMP_DIR/meta.json" | head -1 | cut -d'"' -f4)"
    INTEGRITY="$(grep -o '"integrity":"sha512-[^"]*"' "$TMP_DIR/meta.json" | head -1 | cut -d'"' -f4)"
    RESOLVED_VERSION="$(grep -o '"version":"[^"]*"' "$TMP_DIR/meta.json" | head -1 | cut -d'"' -f4)"
    [[ -n "$TARBALL_URL" && -n "$INTEGRITY" ]] || die "unexpected registry metadata at $META_URL"
    step "Downloading $PLATFORM_PKG ${RESOLVED_VERSION:-$VERSION}"
    fetch "$TARBALL_URL" "$TMP_DIR/package.tgz" || die "download failed: $TARBALL_URL"
    step "Verifying checksum"
    COMPUTED="sha512-$(openssl dgst -sha512 -binary "$TMP_DIR/package.tgz" | base64 -w0)"
    [[ "$COMPUTED" == "$INTEGRITY" ]] ||
        die "checksum mismatch for $PLATFORM_PKG — corrupted or tampered download, not installed"
    ok "Checksum verified"
    TARBALL_PATH="$TMP_DIR/package.tgz"
fi

step "Installing to ${INSTALL_DIR/#$HOME/\~}"
rm -rf "$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
# Both sources carry one top-level dir (opendmo/ or package/) — strip it.
tar -xzf "$TARBALL_PATH" --strip-components=1 -C "$INSTALL_DIR"

# A wrapper, not a symlink: the binary resolves its assets (packs/, theme/,
# version) relative to dirname(process.execPath), which a symlink can break.
mkdir -p "$BIN_DIR"
printf '#!/bin/sh\nexec "%s/opendmo" "$@"\n' "$INSTALL_DIR" > "$WRAPPER"
chmod +x "$WRAPPER"

case ":$PATH:" in
    *":$BIN_DIR:"*) ;;
    *)
        # Both files: .bashrc for interactive shells, .profile for login shells
        # (root's .bashrc has an interactivity guard that skips non-interactive
        # `bash -lc`, and root's .profile lacks the ~/.local/bin block).
        for rc in "$HOME/.bashrc" "$HOME/.profile"; do
            if ! grep -qs 'PATH="\$HOME/.local/bin:\$PATH"' "$rc"; then
                printf '\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$rc"
            fi
        done
        note "Added $BIN_DIR to PATH in ~/.bashrc and ~/.profile — open a new shell (or: source ~/.bashrc)."
        ;;
esac

VERSION_LINE="$("$WRAPPER" --version 2>/dev/null || true)"
ok "Installed opendmo${VERSION_LINE:+ $VERSION_LINE}"

# One install = CLI + app: when the payload carries the desktop app, install
# it the way a .deb would — unpack the AppImage once into a real directory
# (no FUSE mount, faster starts), then give Chromium's sandbox helper its
# required root-owned setuid bit. AppImages themselves can never satisfy the
# helper (their mounts are nosuid), which is why unpacking comes first. The
# launch policy lands in ONE generated script ($INSTALL_DIR/open-dmo-app)
# that both the menu entry and `opendmo app` run.
if [[ -f "$INSTALL_DIR/open-dmo.AppImage" ]]; then
    chmod +x "$INSTALL_DIR/open-dmo.AppImage"
    APP_DIR="$INSTALL_DIR/app"
    APP_EXEC="$INSTALL_DIR/open-dmo-app"
    APP_TARGET_ARGS=""
    SANDBOX_ENV=""

    # Ubuntu 24.04+ (AppArmor) and hardened Debians block the sandbox's
    # unprivileged path; there Chromium needs the root-owned helper.
    USERNS_RESTRICTED=false
    if [[ "$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null)" == "1" ||
        "$(cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null)" == "0" ]]; then
        USERNS_RESTRICTED=true
    fi

    step "Unpacking the office app"
    rm -rf "$APP_DIR" "$INSTALL_DIR/squashfs-root"
    if (cd "$INSTALL_DIR" && ./open-dmo.AppImage --appimage-extract >/dev/null 2>&1) &&
        [[ -x "$INSTALL_DIR/squashfs-root/opendmo-desktop" ]]; then
        mv "$INSTALL_DIR/squashfs-root" "$APP_DIR"
        rm -f "$INSTALL_DIR/open-dmo.AppImage"
        APP_TARGET="$APP_DIR/opendmo-desktop"
        HELPER="$APP_DIR/chrome-sandbox"
        if [[ "$USERNS_RESTRICTED" == "true" ]]; then
            say ""
            say "This distro blocks Chromium's unprivileged sandbox, so the app's"
            say "sandbox helper needs a one-time root blessing — the same thing a"
            say "Chrome or VS Code .deb does during its install:"
            say "  sudo chown root:root $HELPER && sudo chmod 4755 $HELPER"
            if sudo -n true 2>/dev/null || { : </dev/tty; } 2>/dev/null; then
                sudo chown root:root "$HELPER" 2>/dev/null && sudo chmod 4755 "$HELPER" 2>/dev/null || true
            fi
            if [[ -u "$HELPER" && "$(stat -c '%u' "$HELPER" 2>/dev/null)" == "0" ]]; then
                ok "Sandbox helper installed (root-owned) — the app runs fully sandboxed"
            else
                SANDBOX_ENV="export ELECTRON_DISABLE_SANDBOX=1"
                note "sandbox helper not blessed — the app runs WITHOUT Chromium's sandbox"
                sub "fix later: run the sudo line above, then delete the"
                sub "ELECTRON_DISABLE_SANDBOX line from $APP_EXEC"
            fi
        fi
    else
        # Unpacking failed (rare: disk, odd runtime). Keep the AppImage and
        # launch it directly; without libfuse2 it self-extracts per launch. An
        # AppImage can't carry the setuid helper, so a restricted distro also
        # loses the sandbox on this path.
        note "could not unpack the app — keeping the AppImage (slower starts)"
        APP_TARGET="$INSTALL_DIR/open-dmo.AppImage"
        if ! ldconfig -p 2>/dev/null | grep -q 'libfuse\.so\.2'; then
            APP_TARGET_ARGS=" --appimage-extract-and-run"
        fi
        if [[ "$USERNS_RESTRICTED" == "true" ]]; then
            SANDBOX_ENV="export ELECTRON_DISABLE_SANDBOX=1"
            note "this distro blocks the sandbox for AppImages — the app runs WITHOUT Chromium's sandbox"
        fi
    fi

    {
        printf '#!/bin/sh\n'
        printf '# Generated by install.sh — the app launch policy decided at install time.\n'
        [[ -n "$SANDBOX_ENV" ]] && printf '%s\n' "$SANDBOX_ENV"
        printf 'exec "%s"%s "$@"\n' "$APP_TARGET" "$APP_TARGET_ARGS"
    } > "$APP_EXEC"
    chmod +x "$APP_EXEC"

    mkdir -p "$(dirname "$DESKTOP_ENTRY")"
    cat > "$DESKTOP_ENTRY" <<DESKTOP
[Desktop Entry]
Type=Application
Name=open-dmo
GenericName=Digital office
GenericName[ar]=المكتب الرقمي
Comment=Your office, aware.
Comment[ar]=مكتبك، على اطّلاع.
Exec=$APP_EXEC %U
Icon=$INSTALL_DIR/open-dmo-app-icon.png
Terminal=false
Categories=Office;
StartupWMClass=opendmo-desktop
DESKTOP
    command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$(dirname "$DESKTOP_ENTRY")" 2>/dev/null || true
    ok "Installed the office app (launcher entry + opendmo app)"
fi

if [[ -n "$JOIN_FILE" ]]; then
    step "Joining the office from $JOIN_FILE"
    "$WRAPPER" employee create --join "$JOIN_FILE" \
        ${EMPLOYEE:+--employee "$EMPLOYEE"} \
        ${NAME:+--name "$NAME"}
    section "Next"
    kv "work" "opendmo"
else
    section "Next"
    kv "verify" "opendmo --help"
    kv "join" "opendmo employee create"
    sub "auto-finds office-join.yaml · ask your manager for it"
    if [[ -f "$INSTALL_DIR/open-dmo.AppImage" ]]; then
        kv "app" "opendmo app"
        sub "or find open-dmo in your application launcher"
    fi
fi
