#!/usr/bin/env bash
set -euo pipefail

NAME="fixed-phase"
CYCLE_SECS="${PHASE_CYCLE_SECS:-900}"
OFFSET_SECS="${PHASE_OFFSET_SECS:-0}"
LOCK_FILE="${RPKI_HEAVY_LOCK:-/var/lock/rpki-heavy-run.lock}"
LOCK_WAIT_SECS="${LOCK_WAIT_SECS:-30}"

usage() {
  cat <<'USAGE'
Usage:
  fixed_phase_loop.sh [--name <name>] [--cycle-secs <seconds>] [--offset-secs <seconds>]
                      [--lock-file <path>] [--lock-wait-secs <seconds>] -- <command> [args...]

Runs one command at fixed wall-clock phases. Missed phases are skipped rather than caught up,
which keeps independent RP jobs from drifting into each other. A shared flock protects against
unexpected overruns.
USAGE
}

die() {
  echo "error: $*" >&2
  exit 2
}

is_non_negative_int() {
  [[ "$1" =~ ^[0-9]+$ ]]
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --name)
      shift
      NAME="${1:?--name requires a value}"
      ;;
    --cycle-secs)
      shift
      CYCLE_SECS="${1:?--cycle-secs requires a value}"
      ;;
    --offset-secs)
      shift
      OFFSET_SECS="${1:?--offset-secs requires a value}"
      ;;
    --lock-file)
      shift
      LOCK_FILE="${1:?--lock-file requires a value}"
      ;;
    --lock-wait-secs)
      shift
      LOCK_WAIT_SECS="${1:?--lock-wait-secs requires a value}"
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    --)
      shift
      break
      ;;
    *)
      die "unknown argument: $1"
      ;;
  esac
  shift
done

[[ $# -gt 0 ]] || die "missing command after --"
is_non_negative_int "$CYCLE_SECS" || die "--cycle-secs must be a non-negative integer"
is_non_negative_int "$OFFSET_SECS" || die "--offset-secs must be a non-negative integer"
is_non_negative_int "$LOCK_WAIT_SECS" || die "--lock-wait-secs must be a non-negative integer"
(( CYCLE_SECS > 0 )) || die "--cycle-secs must be > 0"
(( OFFSET_SECS < CYCLE_SECS )) || die "--offset-secs must be < --cycle-secs"

mkdir -p "$(dirname "$LOCK_FILE")"

timestamp_utc() {
  date -u +%Y-%m-%dT%H:%M:%SZ
}

format_epoch_utc() {
  date -u -d "@$1" +%Y-%m-%dT%H:%M:%SZ
}

LAST_TARGET_EPOCH=-1

next_phase_epoch() {
  local now="$1"
  local shifted=$((now - OFFSET_SECS))
  local remainder=$((shifted % CYCLE_SECS))
  if (( remainder < 0 )); then
    remainder=$((remainder + CYCLE_SECS))
  fi
  local sleep_secs=$((CYCLE_SECS - remainder))
  if (( sleep_secs == CYCLE_SECS )); then
    sleep_secs=0
  fi
  printf '%s\n' "$((now + sleep_secs))"
}

while true; do
  now_epoch="$(date +%s)"
  target_epoch="$(next_phase_epoch "$now_epoch")"
  if (( target_epoch <= LAST_TARGET_EPOCH )); then
    target_epoch=$((LAST_TARGET_EPOCH + CYCLE_SECS))
  fi
  LAST_TARGET_EPOCH="$target_epoch"
  sleep_secs=$((target_epoch - now_epoch))
  echo "[$(timestamp_utc)] $NAME next_phase=$(format_epoch_utc "$target_epoch") sleep=${sleep_secs}s cycle=${CYCLE_SECS}s offset=${OFFSET_SECS}s" >&2
  if (( sleep_secs > 0 )); then
    sleep "$sleep_secs"
  fi

  started_epoch="$(date +%s)"
  echo "[$(timestamp_utc)] $NAME phase_start target=$(format_epoch_utc "$target_epoch") lock=$LOCK_FILE wait=${LOCK_WAIT_SECS}s" >&2
  set +e
  flock -w "$LOCK_WAIT_SECS" "$LOCK_FILE" "$@"
  code=$?
  set -e
  ended_epoch="$(date +%s)"
  if (( code == 0 )); then
    echo "[$(timestamp_utc)] $NAME phase_done exit=0 elapsed=$((ended_epoch - started_epoch))s" >&2
  else
    echo "[$(timestamp_utc)] $NAME phase_done exit=$code elapsed=$((ended_epoch - started_epoch))s skipped_or_failed=1" >&2
  fi
done
