#!/usr/bin/env bash
#
# bt-update - OTA update with automatic rollback for ButtonTask.
#
# Usage:
#   bt-update stage <package.tar.gz>   # deploy + health-check + commit/rollback
#   bt-update rollback                 # revert to the previous version
#
# Layout (see paths.py):
#   $ROOT/versions/<ver>/   deployed versions (ButtonTask + webconfig/)
#   $ROOT/current -> versions/<ver>
#   $ROOT/run/update-status.json   progress for the web UI
#   $ROOT/run/qt.alive             heartbeat written by the Qt UI
#   $ROOT/.previous                path of the previously active version
#
# Launched via sudo by the web service. On first entry re-execs itself through
# systemd-run so it lives outside buttontask-web.service's cgroup and survives
# the web service restart performed mid-update (setsid alone is not enough).
set -uo pipefail

ROOT="${BUTTONTASK_ROOT:-/opt/buttontask}"

# Re-exec in a transient unit unless we are already running there.
if [ -z "${BT_UPDATE_DETACHED:-}" ]; then
    unit="buttontask-update-$(date +%s)"
    exec systemd-run --no-block --collect \
        "--unit=${unit}" \
        "--setenv=BT_UPDATE_DETACHED=1" \
        "--setenv=BUTTONTASK_ROOT=${ROOT}" \
        "$0" "$@"
fi

VERSIONS="$ROOT/versions"
CURRENT="$ROOT/current"
RUN="$ROOT/run"
STATUS="$RUN/update-status.json"
HEARTBEAT="$RUN/qt.alive"
PREV_MARK="$ROOT/.previous"
LOG="$RUN/update.log"
HEALTH_TIMEOUT="${BUTTONTASK_HEALTH_TIMEOUT:-30}"

mkdir -p "$RUN" "$VERSIONS"

log() {
    printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >> "$LOG"
}

write_status() {
    local state="$1"; local msg="${2:-}"
    msg="${msg//\"/}"
    log "status: $state ${msg}"
    printf '{"state":"%s","message":"%s","ts":%s}\n' "$state" "$msg" "$(date +%s)" > "$STATUS"
}

web_port() {
    python3 - <<'PY' 2>/dev/null || echo 8080
import json, os
root = os.environ.get("BUTTONTASK_ROOT", "/opt/buttontask")
try:
    with open(os.path.join(root, "config", "config.json")) as f:
        print(int(json.load(f).get("settings", {}).get("webPort", 8080)))
except Exception:
    print(8080)
PY
}

restart_services() {
    log "restart buttontask"
    systemctl restart buttontask 2>/dev/null || true
    log "restart buttontask-web"
    systemctl restart buttontask-web 2>/dev/null || true
}

# Returns 0 when the freshly started version looks healthy.
health_check() {
    local port deadline now hb_age
    port="$(web_port)"
    deadline=$(( $(date +%s) + HEALTH_TIMEOUT ))
    while [ "$(date +%s)" -lt "$deadline" ]; do
        # 1) web answers /healthz
        if curl -fsS "http://127.0.0.1:${port}/healthz" >/dev/null 2>&1; then
            # 2) Qt UI heartbeat is fresh (file touched in the last 20s)
            if [ -f "$HEARTBEAT" ]; then
                now=$(date +%s)
                hb_age=$(( now - $(stat -c %Y "$HEARTBEAT" 2>/dev/null || echo 0) ))
                if [ "$hb_age" -le 20 ]; then
                    # 3) Qt service is active
                    if systemctl is-active --quiet buttontask 2>/dev/null; then
                        return 0
                    fi
                fi
            fi
        fi
        sleep 2
    done
    return 1
}

switch_to() {
    # Atomically repoint the current symlink.
    ln -sfn "$1" "$CURRENT"
}

# Install privileged helpers + sudoers from the staged package so OTA can
# deliver new bt-* scripts (e.g. bt-brightness) without a full redeploy.
sync_helpers() {
    local dest="$1"
    local src="$dest/webconfig/scripts"
    local s name tmp_sudoers
    [ -d "$src" ] || { log "sync_helpers: no $src"; return 0; }
    mkdir -p "$ROOT/scripts"
    for s in "$src"/bt-*; do
        [ -f "$s" ] || continue
        name="$(basename "$s")"
        install -m 0755 "$s" "$ROOT/scripts/$name"
        chown root:root "$ROOT/scripts/$name" 2>/dev/null || true
        sed -i 's/\r$//' "$ROOT/scripts/$name" 2>/dev/null || true
        log "synced helper $name"
    done
    if [ -f "$src/buttontask.sudoers" ]; then
        tmp_sudoers="$(mktemp)"
        cp "$src/buttontask.sudoers" "$tmp_sudoers"
        sed -i 's/\r$//' "$tmp_sudoers" 2>/dev/null || true
        chmod 0440 "$tmp_sudoers"
        if visudo -cf "$tmp_sudoers" >/dev/null 2>&1; then
            install -m 0440 "$tmp_sudoers" /etc/sudoers.d/buttontask
            log "synced sudoers"
        else
            log "sudoers validation failed — kept previous /etc/sudoers.d/buttontask"
        fi
        rm -f "$tmp_sudoers"
    fi
}

cmd_stage() {
    local pkg="$1"
    [ -f "$pkg" ] || { write_status error "package not found"; exit 1; }

    write_status staging "распаковка"
    local tmp ver dest
    tmp="$(mktemp -d "$VERSIONS/.stage.XXXXXX")"
    if ! tar -xzf "$pkg" -C "$tmp"; then
        rm -rf "$tmp"; write_status error "bad archive"; exit 1
    fi
    # Some packages wrap content in a single top dir; flatten if so.
    if [ ! -f "$tmp/manifest.json" ]; then
        local inner
        inner="$(find "$tmp" -maxdepth 2 -name manifest.json | head -n1)"
        [ -n "$inner" ] && tmp="$(dirname "$inner")"
    fi
    [ -f "$tmp/manifest.json" ] || { rm -rf "$tmp"; write_status error "manifest.json missing"; exit 1; }
    [ -x "$tmp/ButtonTask" ] || chmod +x "$tmp/ButtonTask" 2>/dev/null || true

    ver="$(python3 -c "import json,sys;print(json.load(open('$tmp/manifest.json')).get('version','unknown'))" 2>/dev/null || echo unknown)"
    dest="$VERSIONS/$ver"

    # Remember the current version for rollback.
    if [ -L "$CURRENT" ]; then
        readlink -f "$CURRENT" > "$PREV_MARK"
    fi

    # Avoid clobbering the running version directory.
    if [ "$(readlink -f "$CURRENT" 2>/dev/null)" = "$(readlink -f "$dest" 2>/dev/null)" ]; then
        dest="${dest}-$(date +%s)"
    fi
    rm -rf "$dest"
    mv "$tmp" "$dest"
    chown -R buttontask:buttontask "$dest" 2>/dev/null || true
    chmod -R u=rwX,go=rX "$dest" 2>/dev/null || true
    rm -f "$pkg"

    write_status helpers "установка хелперов"
    sync_helpers "$dest"

    write_status switching "$ver"
    switch_to "$dest"
    restart_services

    write_status health_check "$ver"
    if health_check; then
        write_status success "$ver"
        exit 0
    fi

    # Rollback.
    if [ -s "$PREV_MARK" ]; then
        switch_to "$(cat "$PREV_MARK")"
        restart_services
        write_status rolledback "запуск $ver не удался"
    else
        write_status error "health-check failed, no previous version"
    fi
    exit 1
}

cmd_rollback() {
    [ -s "$PREV_MARK" ] || { write_status error "no previous version"; exit 1; }
    write_status switching "rollback"
    switch_to "$(cat "$PREV_MARK")"
    restart_services
    write_status rollback_done "откат выполнен"
}

cmd_sync_helpers() {
    local dest
    dest="$(readlink -f "$CURRENT" 2>/dev/null || true)"
    [ -n "$dest" ] && [ -d "$dest" ] || { echo "no current version" >&2; exit 1; }
    sync_helpers "$dest"
    echo "helpers synced from $dest"
}

cmd_remove() {
    local name="$1"
    [[ "$name" =~ ^[A-Za-z0-9._-]+$ ]] || { echo "invalid version name" >&2; exit 1; }
    local dest="$VERSIONS/$name"
    [ -d "$dest" ] || { echo "version not found" >&2; exit 1; }
    local cur prev target
    cur="$(readlink -f "$CURRENT" 2>/dev/null || true)"
    prev=""
    [ -s "$PREV_MARK" ] && prev="$(readlink -f "$(cat "$PREV_MARK")" 2>/dev/null || true)"
    target="$(readlink -f "$dest")"
    [ -n "$cur" ] && [ "$target" = "$cur" ] && { echo "cannot remove active version" >&2; exit 1; }
    [ -n "$prev" ] && [ "$target" = "$prev" ] && { echo "cannot remove rollback target" >&2; exit 1; }
    rm -rf "$dest"
    echo "removed $name"
}

case "${1:-}" in
    stage)         [ "$#" -ge 2 ] || { echo "usage: bt-update stage <pkg>"; exit 2; }; cmd_stage "$2" ;;
    rollback)      cmd_rollback ;;
    sync-helpers)  cmd_sync_helpers ;;
    remove)        [ "$#" -ge 2 ] || { echo "usage: bt-update remove <name>"; exit 2; }; cmd_remove "$2" ;;
    *) echo "usage: bt-update {stage <pkg>|rollback|sync-helpers|remove <name>}"; exit 2 ;;
esac
