#!/bin/bash
# ==============================================================================================
# ============================= Job Runner =====================================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Wraps every scheduled script execution with JSON status tracking and log
# management. Called by /etc/cron.d/varaverk for every scheduled job. The PHP
# dashboard polls the JSON files to show live job status without running scripts.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Invocation: bash run_job.sh <job_id> <script_path> [flags...]
#
# Writes three files per job to /var/log/varaverk/:
#   <id>.json       — status, timestamps, exit code, pid (polled by WebGUI)
#   <id>.log        — appended per run, trimmed to LOG_MAX_LINES lines
#   <id>.manual_ts  — sentinel: epoch of last manual completion (interval suppression)
#
# Status values: running → ok (exit 0) | warn (exit 1) | error (exit 2+)
#
# MANUAL FLAG
#   --manual marks a UI-triggered run. On completion, writes a manual_ts sentinel.
#   The next cron fire reads the sentinel and suppresses itself if the elapsed time
#   is within the job's own cron interval — prevents double-firing after a manual run.
#   Static schedules (e.g. "30 2 * * 0") are never suppressed — only */N intervals.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Suppress Double-Fire
#   When a user triggers a job from the UI, the next cron fire within the job's
#   own interval is skipped. A 30-minute cron job triggered at HH:14 won't fire
#   again at HH:30 — it waits for HH:44. Static schedules are never suppressed.
#
# Status as Ground Truth
#   The JSON file is overwritten atomically on every state change (start → end).
#   The WebGUI polls it directly — no additional IPC or database needed.
#
# Log Trim on Every Write
#   The log file is trimmed to LOG_MAX_LINES after every run. Never grows
#   unbounded regardless of how long the server runs or how often the job fires.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
#   --manual stripped   — flag consumed here, never passed to the wrapped script
#   mkdir -p            — log dir created if missing before any write
#   Log trim            — tail -n LOG_MAX_LINES via tmp file + mv (atomic)
#   Sentinel cleanup    — manual_ts removed after it's used or expired
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
#   LOG_DIR        /var/log/varaverk    (hardcoded — tmpfs on Unraid, cleared on reboot)
#   LOG_MAX_LINES  1000                 (hardcoded — trim threshold per job log)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# bash run_job.sh <job_id> <script_path> [script_flags...]
#     Normal cron-triggered run.
#
# bash run_job.sh <job_id> <script_path> --manual [script_flags...]
#     UI-triggered run. Suppresses next cron fire within the job's interval.
#
# ==============================================================================================

JOB_ID="$1"
SCRIPT="$2"
shift 2

LOG_DIR="/var/log/varaverk"
BASE="${JOB_ID%.sh}"
LOG_FILE="$LOG_DIR/$BASE.log"
STAT_FILE="$LOG_DIR/$BASE.json"
MANUAL_TS_FILE="$LOG_DIR/$BASE.manual_ts"
LOG_MAX_LINES=1000

# Strip --manual from script args — it's for run_job.sh only
MANUAL=false
SCRIPT_ARGS=()
for _arg in "$@"; do
    [[ "$_arg" == "--manual" ]] && MANUAL=true || SCRIPT_ARGS+=("$_arg")
done
unset _arg

mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$STAT_FILE")"

# Cron invocation: skip if a manual run completed recently within this job's own interval.
if [[ "$MANUAL" == false && -f "$MANUAL_TS_FILE" ]]; then
    LAST_MANUAL=$(cat "$MANUAL_TS_FILE" 2>/dev/null || echo 0)
    ELAPSED=$(( $(date +%s) - LAST_MANUAL ))

    # Only suppress for interval-based crons (*/N * * * *).
    # Static schedules like "30 2 * * 0" run at their appointed time and are never suppressed.
    INTERVAL=0
    _VV_CFG="/boot/config/plugins/varaverk/varaverk.cfg"
    _SCRIPTS_DIR=$(grep -m1 '^SCRIPTS_DIR' "$_VV_CFG" 2>/dev/null | cut -d= -f2 | tr -d '"'"'" | tr -d '[:space:]')
    SCHEDULE_FILE="${_SCRIPTS_DIR:-/boot/config/plugins/varaverk}/schedule.json"
    if [[ -f "$SCHEDULE_FILE" ]]; then
        CRON_EXPR=$(php -r "
            \$s = json_decode(file_get_contents('$SCHEDULE_FILE'), true) ?: [];
            echo \$s['$JOB_ID']['cron'] ?? '';
        " 2>/dev/null)
        if [[ "$CRON_EXPR" =~ ^\*/([0-9]+) ]]; then
            INTERVAL=$(( ${BASH_REMATCH[1]} * 60 ))
        fi
    fi

    if (( INTERVAL > 0 && ELAPSED < INTERVAL )); then
        printf '\n── %s [SKIPPED — ran manually %ds ago, interval %ds] ────────\n' \
            "$(date '+%Y-%m-%d %H:%M:%S')" "$ELAPSED" "$INTERVAL" >> "$LOG_FILE"
        printf '{"id":"%s","status":"skipped","manual_elapsed":%s,"interval":%s}\n' \
            "$JOB_ID" "$ELAPSED" "$INTERVAL" > "$STAT_FILE"
        rm -f "$MANUAL_TS_FILE"
        exit 0
    fi
    rm -f "$MANUAL_TS_FILE"
fi

START=$(date +%s)
printf '{"id":"%s","status":"running","start":%s,"pid":%s}\n' \
    "$JOB_ID" "$START" "$$" > "$STAT_FILE"

printf '\n── %s ────────────────────────────────────────────────\n' \
    "$(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"

bash "$SCRIPT" "${SCRIPT_ARGS[@]}" >> "$LOG_FILE" 2>&1
EC=$?

END=$(date +%s)
if   [ "$EC" -eq 0 ]; then STATUS="ok"
elif [ "$EC" -eq 1 ]; then STATUS="warn"
else                        STATUS="error"
fi

printf '{"id":"%s","status":"%s","start":%s,"end":%s,"exit":%s}\n' \
    "$JOB_ID" "$STATUS" "$START" "$END" "$EC" > "$STAT_FILE"

# Write manual sentinel so cron can skip the next fire within the interval
if [[ "$MANUAL" == true ]]; then
    echo "$END" > "$MANUAL_TS_FILE"
fi

# Trim to last LOG_MAX_LINES to prevent unbounded growth
line_count=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$line_count" -gt "$LOG_MAX_LINES" ]; then
    tail -n "$LOG_MAX_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp" && mv "${LOG_FILE}.tmp" "$LOG_FILE"
fi

exit $EC
