#!/usr/bin/env bash
# DyzineFrame dependency / desktop refresh on an existing Pi install.
#
#   cd ~/DyzineFrame && bash update.sh
#   DF_APT=1 bash update.sh              # apt-get install missing deps only
#   FORCE_PIP_UPGRADE=1 bash update.sh   # pip --upgrade (can break pygame)
#   SKIP_SUPERVISOR_KILL=1 bash update.sh
#   SKIP_SUPERVISOR_LAUNCH=1 bash update.sh
#   FORCE_SUPERVISOR_KILL=1 bash update.sh
#
# This does NOT pull code from Dropbox and does NOT apt full-upgrade.
# Bookworm python3.11 only — see pi_python_policy.sh.
#   Files:     cd ~/EtherGold/sync && bash pull_all_from_dropbox.sh
#   EG tip:    cd ~/EtherGold/sync && bash update.sh
#
# After pull + this script: if supervisor*.py / run_supervisor.sh is newer than
# the running supervisor process, that process is replaced (kill + launch new
# run_supervisor.sh when DISPLAY/:0 is available).
set -euo pipefail

if [[ "${EUID}" -eq 0 ]]; then
  echo "DO NOT RUN THIS SCRIPT AS ROOT!" >&2
  exit 1
fi

DF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=pi_python_policy.sh
source "${DF}/pi_python_policy.sh"
# Old SKIP_APT=0 meant "do apt"; that used to full-upgrade. Now it only installs missing deps.
if [[ "${SKIP_APT:-}" == "0" ]]; then
  DF_APT=1
fi

echo "=== DyzineFrame update (deps + desktop) ==="
echo "tree: ${DF}"
df_require_supported_python

if [[ "${DF_APT:-0}" == "1" ]]; then
  df_apt_install_df_deps
else
  echo "DF_APT=0 — leaving apt alone (default). Missing OS libs: DF_APT=1 bash update.sh"
fi

df_pip_ensure_df_modules

if [[ -f "${DF}/ensure_pi_desktop.sh" ]]; then
  bash "${DF}/ensure_pi_desktop.sh"
else
  echo "WARN: ensure_pi_desktop.sh missing — skip desktop refresh" >&2
fi

if [[ -f "${DF}/install_pogcc_permdev.sh" ]]; then
  echo "Refreshing POG-CC PermDevMapper..."
  bash "${DF}/install_pogcc_permdev.sh" || {
    echo "WARN: PermDevMapper refresh failed — check FTDI 0403:6001 / udev ser_DT1" >&2
  }
fi

# Ensure kill-switch key exists (default dyzineframe — does not cut over Heltec).
CFG="${DF}/config.ini"
if [[ -f "${CFG}" ]]; then
  if grep -qiE '^[[:space:]]*pogcc_com_owner[[:space:]]*=' "${CFG}"; then
    echo "config: pogcc_com_owner already present"
  else
    echo "config: adding pogcc_com_owner = dyzineframe (safe default)"
    if grep -qiE '^[[:space:]]*test_mode_no_serial[[:space:]]*=' "${CFG}"; then
      # Insert after test_mode_no_serial line.
      python3 - "${CFG}" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
text = path.read_text(encoding="utf-8", errors="replace")
lines = text.splitlines(keepends=True)
out = []
inserted = False
block = (
    "# STRING - Who owns Heltec USB CDC + P3 /credits GAMESTATS:\n"
    "#   dyzineframe = DF thread_usb_serial (default)\n"
    "#   ethergold   = ethergold-pogcc-com.service (DF display-only for POG-CC COM)\n"
    "pogcc_com_owner = dyzineframe\n"
)
for line in lines:
    out.append(line)
    if (not inserted) and line.lstrip().lower().startswith("test_mode_no_serial"):
        out.append(block)
        inserted = True
if not inserted:
    out2 = []
    for line in out:
        out2.append(line)
        if (not inserted) and line.strip().lower() == "[topbox]":
            out2.append(block)
            inserted = True
    out = out2
if not inserted:
    out.append("\n" + block)
path.write_text("".join(out), encoding="utf-8")
print(f"wrote {path}")
PY
    else
      python3 - "${CFG}" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
text = path.read_text(encoding="utf-8", errors="replace")
block = (
    "\n# STRING - Who owns Heltec USB CDC + P3 /credits GAMESTATS:\n"
    "#   dyzineframe = DF thread_usb_serial (default)\n"
    "#   ethergold   = ethergold-pogcc-com.service\n"
    "pogcc_com_owner = dyzineframe\n"
)
path.write_text(text.rstrip() + block, encoding="utf-8")
print(f"wrote {path}")
PY
    fi
  fi
else
  echo "WARN: ${CFG} missing — skip pogcc_com_owner ensure" >&2
fi

# Sync EG COM unit when tip is present (auto: active only if owner=ethergold).
# Field runtime is ~/EtherGold/prod — never require workshop ~/EtherGold/dev.
EG_INSTALL=""
for cand in \
  "${HOME}/EtherGold/prod/install_pogcc_com_service.sh" \
  "${HOME}/EtherGold/install_pogcc_com_service.sh" \
  "${HOME}/EtherGold/dev/install_pogcc_com_service.sh"
do
  if [[ -f "${cand}" ]]; then
    EG_INSTALL="${cand}"
    break
  fi
done
if [[ "${SKIP_POGCC_COM:-0}" == "1" ]]; then
  echo "SKIP_POGCC_COM=1 — leave ethergold-pogcc-com unchanged"
elif [[ -n "${EG_INSTALL}" ]]; then
  echo "Syncing ethergold-pogcc-com (auto; gated by pogcc_com_owner) from ${EG_INSTALL}..."
  bash "${EG_INSTALL}" || {
    echo "WARN: install_pogcc_com_service.sh failed — non-fatal" >&2
  }
else
  echo "NOTE: no install_pogcc_com_service.sh under ~/EtherGold/{prod,.,dev} — skip"
fi

# If Dropbox/pull already replaced supervisor code while an old supervisor is
# still running, that process keeps the in-memory copy. Detect mtime > process
# start, SIGKILL stale supervisor (+ UI + DF child), then launch the new one
# when a display is available (desktop session or :0).
_supervisor_proc_start_epoch() {
  local pid="$1" etimes
  etimes="$(ps -o etimes= -p "${pid}" 2>/dev/null | tr -d ' ' || true)"
  [[ -n "${etimes}" && "${etimes}" =~ ^[0-9]+$ ]] || return 1
  echo "$(( $(date +%s) - etimes ))"
}

_supervisor_code_newer_than_proc() {
  local pid="$1" start mt f
  start="$(_supervisor_proc_start_epoch "${pid}")" || return 1
  for f in \
    "${DF}/dyzineframe_supervisor.py" \
    "${DF}/dyzineframe_supervisor_ui.py" \
    "${DF}/run_supervisor.sh"
  do
    [[ -f "${f}" ]] || continue
    mt="$(stat -c %Y "${f}" 2>/dev/null || true)"
    [[ -n "${mt}" ]] || continue
    if (( mt > start )); then
      return 0
    fi
  done
  return 1
}

_ensure_display_for_supervisor() {
  if [[ -n "${DISPLAY:-}" || -n "${WAYLAND_DISPLAY:-}" ]]; then
    return 0
  fi
  if [[ -S /tmp/.X11-unix/X0 ]]; then
    export DISPLAY=:0
    if [[ -z "${XAUTHORITY:-}" && -f "${HOME}/.Xauthority" ]]; then
      export XAUTHORITY="${HOME}/.Xauthority"
    fi
    return 0
  fi
  return 1
}

_launch_df_supervisor() {
  if [[ "${SKIP_SUPERVISOR_LAUNCH:-0}" == "1" ]]; then
    echo "SKIP_SUPERVISOR_LAUNCH=1 — not starting supervisor"
    echo "  Start from Pi desktop:  cd ~/DyzineFrame && ./run_supervisor.sh"
    return 0
  fi
  if ! _ensure_display_for_supervisor; then
    echo "WARN: no DISPLAY/WAYLAND/:0 — cannot launch supervisor from this session" >&2
    echo "  Start from Pi desktop:  cd ~/DyzineFrame && ./run_supervisor.sh"
    return 0
  fi
  if [[ ! -x "${DF}/run_supervisor.sh" ]]; then
    echo "WARN: ${DF}/run_supervisor.sh missing/not executable — skip launch" >&2
    return 0
  fi
  mkdir -p "${DF}/log" "${DF}/run"
  # Mid-update restart should not wait for cold-boot delay.
  export DYZINEFRAME_BOOT_DELAY_SEC="${DYZINEFRAME_BOOT_DELAY_SEC:-0}"
  echo "supervisor: launching (DISPLAY=${DISPLAY:-} WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-})"
  nohup "${DF}/run_supervisor.sh" >> "${DF}/log/supervisor.log" 2>&1 &
  disown || true
  sleep 1
  if pgrep -f '[d]yzineframe_supervisor\.py' >/dev/null 2>&1; then
    echo "supervisor: started pid=$(pgrep -f '[d]yzineframe_supervisor\.py' | head -1)"
  else
    echo "WARN: supervisor did not stay up — check ${DF}/log/supervisor.log" >&2
  fi
}

_refresh_stale_df_supervisor() {
  local pid killed=0
  if [[ "${SKIP_SUPERVISOR_KILL:-0}" == "1" ]]; then
    echo "SKIP_SUPERVISOR_KILL=1 — leave running DyzineFrame supervisor alone"
    return 0
  fi
  mapfile -t pids < <(pgrep -f '[d]yzineframe_supervisor\.py' 2>/dev/null || true)
  if [[ "${#pids[@]}" -eq 0 ]]; then
    echo "supervisor: none running"
    return 0
  fi
  for pid in "${pids[@]}"; do
    if _supervisor_code_newer_than_proc "${pid}"; then
      echo "supervisor: code on disk newer than pid=${pid} — replacing"
      killed=1
    elif [[ "${FORCE_SUPERVISOR_KILL:-0}" == "1" ]]; then
      echo "supervisor: FORCE_SUPERVISOR_KILL=1 — replacing pid=${pid}"
      killed=1
    else
      echo "supervisor: pid=${pid} already matches on-disk code (leave running)"
    fi
  done
  if [[ "${killed}" != "1" ]]; then
    return 0
  fi
  # Stop UI, supervisor, and DF child so the new supervisor can start cleanly.
  pkill -KILL -f '[d]yzineframe_supervisor_ui\.py' 2>/dev/null || true
  pkill -KILL -f '[d]yzineframe_supervisor\.py' 2>/dev/null || true
  pkill -KILL -f '[Dd]yzineFrame\.py' 2>/dev/null || true
  rm -f "${DF}/run/dyzineframe.lock" "${DF}/run/supervisor.hold" 2>/dev/null || true
  sleep 0.5
  if pgrep -f '[d]yzineframe_supervisor\.py' >/dev/null 2>&1; then
    echo "WARN: old supervisor still present after kill" >&2
    return 0
  fi
  echo "supervisor: stale processes cleared"
  _launch_df_supervisor
}

_refresh_stale_df_supervisor

echo ""
echo "DyzineFrame update complete (python check + pip ensure + desktop + PermDevMapper)."
echo "  Did NOT apt full-upgrade or pip --upgrade (Bookworm python 3.11 must stay)."
echo "  Code/files from Dropbox are NOT updated by this script."
echo "  Pull files:  cd ~/EtherGold/sync && bash pull_all_from_dropbox.sh"
echo "  EG tip:      cd ~/EtherGold/sync && bash update.sh"
echo "  Missing OS libs:  DF_APT=1 bash update.sh"
echo "  Heltec cutover stays opt-in: pogcc_com_owner=ethergold + DF restart + EG unit enable."
echo "  Stale supervisor: auto-replaced (kill + launch) when scripts newer than running process."
echo "    SKIP_SUPERVISOR_KILL=1 / SKIP_SUPERVISOR_LAUNCH=1 / FORCE_SUPERVISOR_KILL=1"
