#!/usr/bin/env bash
# app-update — unattended update check+execute, run by app-update.timer.
#
# Usage: app-update [--config <path>] [--show-config] [-h|--help]
#
# Gates: runs `app-deploy --auto` only when AUTOUPDATE=on in the config (default
# off = inert, so the timer can be installed everywhere and enabled per instance
# with one config line), and only OUTSIDE the AUTOUPDATE_WORKHOURS window
# (default 'Mo-Fr 06:00-18:00'; empty = any hour). The timer ticks hourly, so a
# newly confirmed version is picked up within the hour — evenings, nights and
# weekends by default. All update logic lives in app-deploy --auto: version source
# (CSC-confirmed when an account is configured, else the public channel file),
# pre-update backup, deploy, CSC reporting. AUTOUPDATE_BACKUP=off skips the
# pre-update backup for the update path (honored by app-deploy --auto itself).
#
# Scripts self-update: with SCRIPTS_AUTOUPDATE=on (default: follows AUTOUPDATE)
# each run — same working-hours window — first updates the management scripts
# themselves with the installed app-bootstrap.sh (remembered channel, default
# https://pkgs.lightcomp.com/get/<product>, published .sha256 mandatory), then goes on
# with the NEW copy, so a script fix reaches the host before the next app deploy.
# A failed self-update only warns; the app check still runs. SCRIPTS_AUTOUPDATE=on
# with AUTOUPDATE=off updates the scripts only.
#
# Exit: 0 when disabled, inside working hours, or already up to date; non-zero
# when the deploy fails — including a CSC account with NO confirmed version
# (strict, by design).
#
# The unit runs as root (no sudo setup needed). Concurrent manual deploys are
# excluded by app-deploy's own lock.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# help = the header comment block above, printed until the first non-# line. The
# generic command names (app-update, app-deploy, …) are rewritten to THIS script's
# own name/prefix, so a renamed <product>-update — or a custom CLI sourcing the
# libs — prints the right names with no install-time edit. pfx='app' (a no-op)
# when not renamed.
usage() {
    local cmd pfx; cmd="$(basename -- "$0")"; pfx="${cmd%-update}"
    sed -n '2,${/^#/!q;s/^# \{0,1\}//;s/^#$//;p}' "$0" \
        | sed -E "s/app-(deploy|backup|health|update)/$pfx-\1/g"
}

# day abbreviation (Mo Tu We Th Fr Sa Su, case-insensitive) -> 1..7; '' if unknown
dow_num() {
    case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
        mo) echo 1 ;; tu) echo 2 ;; we) echo 3 ;; th) echo 4 ;;
        fr) echo 5 ;; sa) echo 6 ;; su) echo 7 ;; *) echo '' ;;
    esac
}

# True when now falls inside AUTOUPDATE_WORKHOURS ('<Day>[-<Day>] HH:MM-HH:MM';
# empty = never). Dies on a malformed spec — a silent mis-parse could otherwise
# run updates during working hours. NB: dies happen here, in the function body,
# not inside $(...) where they would be swallowed by the subshell.
in_work_hours() {
    [ -n "$AUTOUPDATE_WORKHOURS" ] || return 1
    local spec="$AUTOUPDATE_WORKHOURS" days times d1 d2 t1 t2 dow now
    local hint="(expected e.g. 'Mo-Fr 06:00-18:00')"
    days="${spec%% *}"; times="${spec#* }"
    [ "$days" != "$spec" ] || de_die "invalid AUTOUPDATE_WORKHOURS '$spec' $hint"
    if [ "${days#*-}" != "$days" ]; then
        d1="$(dow_num "${days%-*}")"; d2="$(dow_num "${days#*-}")"
    else
        d1="$(dow_num "$days")"; d2="$d1"
    fi
    [ -n "$d1" ] && [ -n "$d2" ] || de_die "invalid day(s) in AUTOUPDATE_WORKHOURS '$spec' $hint"
    [ "$d1" -le "$d2" ] || de_die "day range must not wrap in AUTOUPDATE_WORKHOURS '$spec' (use e.g. 'Mo-Su')"
    t1="${times%-*}"; t2="${times#*-}"
    case "$t1" in [0-2][0-9]:[0-5][0-9]) ;; *) de_die "invalid time '$t1' in AUTOUPDATE_WORKHOURS '$spec' $hint" ;; esac
    case "$t2" in [0-2][0-9]:[0-5][0-9]) ;; *) de_die "invalid time '$t2' in AUTOUPDATE_WORKHOURS '$spec' $hint" ;; esac
    [ "$t1" \< "$t2" ] || de_die "time range must not wrap in AUTOUPDATE_WORKHOURS '$spec'"
    dow="$(date +%u)"; now="$(date +%H:%M)"
    [ "$dow" -ge "$d1" ] && [ "$dow" -le "$d2" ] || return 1
    # zero-padded HH:MM compares correctly as a string: t1 <= now < t2
    { [ "$now" = "$t1" ] || [ "$now" \> "$t1" ]; } && [ "$now" \< "$t2" ]
}

# Self-update the scripts before the app check, so a fix to the scripts lands
# before they are used for a deploy. Runs the installed bootstrap in strict mode
# (the published .sha256 is mandatory). On a new build the tree has been
# swapped, so re-exec the NEW app-update (once — DE_SCRIPTS_UPDATED guards the
# loop); bash must not keep reading a replaced file. A failure is only a warning:
# the app check then runs with the scripts already installed.
self_update_scripts() {
    local bs="$SCRIPT_DIR/$PRODUCT-bootstrap.sh" stamp="$SCRIPT_DIR/.bootstrap-sha256" before after
    [ -x "$bs" ] || { de_warn "no $bs; skipping the scripts self-update"; return 0; }
    before="$(cat "$stamp" 2>/dev/null || true)"
    if ! "$bs" --require-checksum; then
        de_warn "scripts self-update failed; continuing with the installed scripts"
        return 0
    fi
    after="$(cat "$stamp" 2>/dev/null || true)"
    [ "$before" = "$after" ] && return 0
    de_log "Scripts updated (${before:0:12} -> ${after:0:12}); continuing with the new $PRODUCT-update"
    export DE_SCRIPTS_UPDATED=1
    exec "$SCRIPT_DIR/$PRODUCT-update" "${ORIG_ARGS[@]}"
}

ORIG_ARGS=("$@")
SHOW_CONFIG=0
while [ $# -gt 0 ]; do
    case "$1" in
        --config)      CONFIG_FILE="$2"; shift ;;
        --config=*)    CONFIG_FILE="${1#*=}" ;;
        --show-config) SHOW_CONFIG=1 ;;
        -h|--help)     usage; exit 0 ;;
        *)             echo "Unknown option: $1" >&2; exit 2 ;;
    esac
    shift
done

. "$SCRIPT_DIR/common.sh"

[ "$SHOW_CONFIG" = 1 ] && { de_show_config; exit 0; }

app_on=0;     is_on "$AUTOUPDATE" AUTOUPDATE                 && app_on=1
scripts_on=0; is_on "$SCRIPTS_AUTOUPDATE" SCRIPTS_AUTOUPDATE && scripts_on=1

if [ "$app_on" -eq 0 ] && [ "$scripts_on" -eq 0 ]; then
    de_log "AUTOUPDATE is off; skipping (enable with AUTOUPDATE=on in the config)"
    exit 0
fi

if in_work_hours; then
    de_log "Inside working hours ($AUTOUPDATE_WORKHOURS); postponing update check"
    exit 0
fi

if [ "$scripts_on" -eq 1 ] && [ -z "${DE_SCRIPTS_UPDATED:-}" ]; then
    self_update_scripts
fi

if [ "$app_on" -eq 0 ]; then
    de_log "AUTOUPDATE is off; scripts checked only (SCRIPTS_AUTOUPDATE=on)"
    exit 0
fi

deploy_args=(--auto)
[ -n "${CONFIG_FILE:-}" ] && deploy_args+=(--config "$CONFIG_FILE")

de_log "Unattended update check ($(csc_enabled && echo 'CSC-confirmed version' || echo "public channel '$CHANNEL'"))"
exec "$SCRIPT_DIR/$PRODUCT-deploy" "${deploy_args[@]}"
