# Shared OTA helpers (DyzineFrame + EtherGold). Source from sync/*.sh.
# shellcheck shell=bash

ota_read_version() {
  local f="${1:-}/VERSION.txt"
  if [[ -f "${f}" ]]; then
    tr -d '\r' < "${f}" | head -n 1 | tr -d '[:space:]'
  fi
}

# Kill python3/python PIDs whose args contain needle. Never pkill -f (matches ssh).
ota_kill_python_cmd() {
  local needle="$1"
  local pid args
  while read -r pid args; do
    [[ -n "${pid}" ]] || continue
    case " ${args} " in
      *" ${needle}"*|*"/${needle}"*|*" ${needle} "*)
        kill -9 "${pid}" 2>/dev/null || true
        ;;
    esac
  done < <(ps -C python3 -o pid=,args= 2>/dev/null; ps -C python -o pid=,args= 2>/dev/null)
}

ota_python_has_cmd() {
  local needle="$1"
  local pid args
  while read -r pid args; do
    [[ -n "${pid}" ]] || continue
    case " ${args} " in
      *" ${needle}"*|*"/${needle}"*|*" ${needle} "*)
        return 0
        ;;
    esac
  done < <(ps -C python3 -o pid=,args= 2>/dev/null; ps -C python -o pid=,args= 2>/dev/null)
  return 1
}

ota_restart_dyzineframe() {
  local root="${1:-${HOME}/DyzineFrame}"
  # Kill DyzineFrame.py only. Leave dyzineframe_supervisor.py (and UI) running so
  # they can respawn HDMI. OTA HTTP often has no DISPLAY; killing the supervisor
  # left nothing to restart DF.
  ota_kill_python_cmd "DyzineFrame.py"
  rm -f "${root}/run/dyzineframe.lock" "${root}/run/supervisor.hold" 2>/dev/null || true
  if ota_python_has_cmd "dyzineframe_supervisor.py"; then
    echo "ota_restart_dyzineframe: supervisor still up — it will respawn DyzineFrame"
    return 0
  fi
  echo "ota_restart_dyzineframe: no supervisor — starting one"
  export DISPLAY="${DISPLAY:-:0}"
  if [[ -z "${XAUTHORITY:-}" && -f "${HOME}/.Xauthority" ]]; then
    export XAUTHORITY="${HOME}/.Xauthority"
  fi
  if [[ -x "${root}/run_supervisor.sh" ]]; then
    nohup "${root}/run_supervisor.sh" >> "${root}/log/supervisor.log" 2>&1 &
  elif [[ -x "${root}/runit.sh" ]]; then
    nohup "${root}/runit.sh" >> "${root}/log/log.txt" 2>&1 &
  fi
}

ota_restart_ethergold_listen() {
  ota_kill_python_cmd "pog422_listen.py"
  ota_kill_python_cmd "ethergold_listen_supervisor.py"
  ota_kill_python_cmd "pog422_fixture_listen.py"
  # Supervisor (if present) respawns listen. If a systemd unit owns it, restart that.
  if systemctl --user is-active --quiet ethergold-listen.service 2>/dev/null; then
    systemctl --user restart ethergold-listen.service || true
  elif systemctl is-active --quiet ethergold-listen.service 2>/dev/null; then
    systemctl restart ethergold-listen.service || true
  fi
}

ota_json_get() {
  local file="$1"
  local key="$2"
  python3 - "$file" "$key" <<'PY'
import json, sys
path, key = sys.argv[1], sys.argv[2]
try:
    with open(path, encoding="utf-8") as f:
        data = json.load(f)
except Exception:
    sys.exit(1)
val = data.get(key, "")
if val is None:
    val = ""
print(val)
PY
}

ota_promote_held() {
  local live_root="$1"
  local staging_root="$2"
  local hold="${live_root}/run/promote-hold"
  [[ -f "${hold}" ]] || return 1
  local from=""
  from="$(grep '^from_version=' "${hold}" 2>/dev/null | head -n 1 | cut -d= -f2- | tr -d '\r')"
  [[ -n "${from}" ]] || return 1
  local staged
  staged="$(ota_read_version "${staging_root}")"
  [[ "${staged}" == "${from}" ]]
}

ota_write_promote_hold() {
  local live_root="$1"
  local from_ver="$2"
  mkdir -p "${live_root}/run"
  {
    echo "from_version=${from_ver}"
    echo "held_at=$(date -Is)"
  } > "${live_root}/run/promote-hold"
}

ota_clear_promote_hold() {
  rm -f "${1}/run/promote-hold"
}

ota_trim() {
  tr -d '\r' < "${1}" 2>/dev/null | tr -d '[:space:]'
}

ota_live_version() {
  local live="$1"
  local lv
  lv="$(ota_read_version "${live}")"
  [[ -z "${lv}" ]] && lv="$(ota_read_version "${live}/prod")"
  printf '%s' "${lv}"
}

# Compare A.B.C.D stamps. Prints -1 / 0 / 1. Empty if either is not dotted-numeric
# (dated USB stamps like 2026-07-08-h stay incomparable — do not auto-park).
ota_version_cmp() {
  python3 -c '
import sys
def parse(s):
    s = (s or "").strip()
    if not s:
        return None
    parts = []
    for tok in s.split("."):
        if not tok.isdigit():
            return None
        parts.append(int(tok))
    if not parts:
        return None
    while len(parts) < 4:
        parts.append(0)
    return tuple(parts[:4])
a, b = parse(sys.argv[1]), parse(sys.argv[2])
if a is None or b is None:
    raise SystemExit(0)
print(-1 if a < b else 1 if a > b else 0)
' "$1" "$2" 2>/dev/null || true
}

ota_version_is_older() {
  local c
  c="$(ota_version_cmp "$1" "$2")"
  [[ "${c}" == "-1" ]]
}

ota_staging_differs_by_stamp() {
  local staging="$1"
  local live="$2"
  local sv lv sm lm
  sv="$(ota_read_version "${staging}")"
  lv="$(ota_read_version "${live}")"
  [[ -z "${lv}" ]] && lv="$(ota_read_version "${live}/prod")"
  sm="$(ota_trim "${staging}/MANIFEST.total" 2>/dev/null || true)"
  lm="$(ota_trim "${live}/MANIFEST.total" 2>/dev/null || true)"
  [[ -z "${lm}" ]] && lm="$(ota_trim "${live}/prod/MANIFEST.total" 2>/dev/null || true)"
  if [[ -n "${sv}" && "${sv}" != "${lv}" ]]; then
    return 0
  fi
  if [[ -n "${sm}" && "${sm}" != "${lm}" ]]; then
    return 0
  fi
  if [[ -n "${sm}" || -n "${sv}" ]]; then
    return 1
  fi
  return 2
}

ota_write_pending_flag() {
  local flag="$1"
  local staging="$2"
  local live="$3"
  mkdir -p "$(dirname "${flag}")"
  {
    echo "staged_at=$(date -Is)"
    echo "staged_version=$(ota_read_version "${staging}")"
    echo "live_version=$(ota_read_version "${live}")"
    echo "staged_manifest=$(ota_trim "${staging}/MANIFEST.total" 2>/dev/null || true)"
    echo "live_manifest=$(ota_trim "${live}/MANIFEST.total" 2>/dev/null || true)"
  } > "${flag}"
}

ota_record_directive() {
  local live_root="$1"
  local id="$2"
  mkdir -p "${live_root}/run"
  {
    echo "id=${id}"
    echo "applied_at=$(date -Is)"
  } > "${live_root}/run/last-rollback-directive"
}

ota_last_directive_id() {
  local live_root="$1"
  grep '^id=' "${live_root}/run/last-rollback-directive" 2>/dev/null | head -n 1 | cut -d= -f2- | tr -d '\r'
}

# After rclone: apply SGVI ROLLBACK.json once by id. rollback_script must restore live.
ota_apply_rollback_directive() {
  local live_root="$1"
  local staging_root="$2"
  local rollback_root="$3"
  local rollback_script="$4"
  local json="${staging_root}/ROLLBACK.json"
  [[ -f "${json}" ]] || return 0
  local id to_ver from_ver last_id live_ver rb_ver
  id="$(ota_json_get "${json}" id 2>/dev/null || true)"
  to_ver="$(ota_json_get "${json}" to_version 2>/dev/null || true)"
  from_ver="$(ota_json_get "${json}" from_version 2>/dev/null || true)"
  if [[ -z "${id}" || -z "${to_ver}" ]]; then
    echo "ROLLBACK.json: missing id or to_version — skip"
    return 0
  fi
  last_id="$(ota_last_directive_id "${live_root}")"
  if [[ "${id}" == "${last_id}" ]]; then
    echo "ROLLBACK.json id=${id} already applied — skip"
    return 0
  fi
  live_ver="$(ota_read_version "${live_root}")"
  [[ -z "${live_ver}" ]] && live_ver="$(ota_read_version "${live_root}/prod")"
  if [[ "${live_ver}" == "${to_ver}" ]]; then
    echo "ROLLBACK.json: live already ${to_ver} — record id, skip"
    ota_record_directive "${live_root}" "${id}"
    return 0
  fi
  rb_ver="$(ota_read_version "${rollback_root}")"
  [[ -z "${rb_ver}" ]] && rb_ver="$(ota_read_version "${rollback_root}/prod")"
  if [[ "${rb_ver}" != "${to_ver}" ]]; then
    echo "ROLLBACK.json skip: rollback snapshot '${rb_ver}' != to_version '${to_ver}'"
    return 0
  fi
  echo "ROLLBACK.json applying id=${id} live ${live_ver} -> ${to_ver}"
  RESTART="${RESTART:-1}" bash "${rollback_script}"
  if [[ -n "${from_ver}" ]]; then
    ota_write_promote_hold "${live_root}" "${from_ver}"
  fi
  ota_record_directive "${live_root}" "${id}"
}
