#!/usr/bin/env bash
# Public, read-only Optuna Dashboard for optimize_fin presets_fin.db.
# Lives in dashboard/; project root is one level up (venv + DB).
#
# Opens the *live* DB and rejects write HTTP methods (POST/PUT/DELETE/PATCH)
# at the app layer. That blocks fail/tell/delete/note from the network.
#
# Note: plain SQLite "?mode=ro" is NOT enough — SQLAlchemy/Optuna ignore it and
# still allow writes (e.g. POST /api/trials/<id>/tell → 204).
#
# Env:
#   OPTUNA_DASHBOARD_PORT   listen port (default: 8080)
#   OPTUNA_DASHBOARD_HOST   bind address (default: 0.0.0.0)
#   OPTUNA_DASHBOARD_DB     path to sqlite (default: ../presets_fin.db)
#
# GCP: firewall must allow tcp on the port (e.g. tag optuna-dashboard).
#
# Privacy: visitors can still *see* all trials/params.
set -euo pipefail

DASH_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || realpath "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")" && pwd)"
ROOT="$(cd "$DASH_DIR/.." && pwd)"
cd "$ROOT"

# Re-open in a terminal when launched from a GUI file manager
if [[ -z "${RUN_DASHBOARD_IN_TERM:-}" ]] && { [[ ! -t 1 ]] || [[ -z "${TERM:-}" ]]; }; then
  export RUN_DASHBOARD_IN_TERM=1
  if command -v gnome-terminal >/dev/null 2>&1; then
    exec gnome-terminal --working-directory="$DASH_DIR" -- bash "$DASH_DIR/dashboard-public.sh" "$@"
  elif command -v xfce4-terminal >/dev/null 2>&1; then
    exec xfce4-terminal --working-directory="$DASH_DIR" -e "bash \"$DASH_DIR/dashboard-public.sh\""
  elif command -v konsole >/dev/null 2>&1; then
    exec konsole --workdir "$DASH_DIR" -e bash "$DASH_DIR/dashboard-public.sh" "$@"
  elif command -v x-terminal-emulator >/dev/null 2>&1; then
    exec x-terminal-emulator -e bash -lc "cd \"$DASH_DIR\" && exec bash \"$DASH_DIR/dashboard-public.sh\" $*"
  elif command -v xterm >/dev/null 2>&1; then
    exec xterm -e bash -lc "cd \"$DASH_DIR\" && exec bash \"$DASH_DIR/dashboard-public.sh\" $*"
  fi
fi

hold() {
  local code=${1:-0}
  echo
  if [[ $code -ne 0 ]]; then
    echo "FAILED (exit $code)."
  else
    echo "Done."
  fi
  if [[ -n "${RUN_DASHBOARD_IN_TERM:-}" ]] || [[ ! -t 0 ]]; then
    read -r -p "Press Enter to close…" _ || true
  fi
  exit "$code"
}
trap 'hold $?' EXIT

echo "=== Optuna Dashboard (PUBLIC, HTTP READ-ONLY, live DB) ==="
echo "Project: $ROOT"
echo "Dashboard scripts: $DASH_DIR"
echo

DB="${OPTUNA_DASHBOARD_DB:-$ROOT/presets_fin.db}"
if [[ ! -f "$DB" ]]; then
  echo "MISSING: $DB"
  echo "Run the optimizer first (./run.sh) so a study database exists."
  echo "Override path with OPTUNA_DASHBOARD_DB=/path/to.db if needed."
  exit 1
fi

LAUNCHER="$DASH_DIR/dashboard_public_ro.py"
if [[ ! -f "$LAUNCHER" ]]; then
  echo "MISSING: dashboard_public_ro.py (in dashboard/)"
  exit 1
fi

# System packages
need_apt=()
command -v python3 >/dev/null 2>&1 || need_apt+=(python3)
python3 -m venv -h >/dev/null 2>&1 || need_apt+=(python3-venv)
python3 -c "import ensurepip" 2>/dev/null || need_apt+=(python3-venv)
command -v pip3 >/dev/null 2>&1 || python3 -m pip -h >/dev/null 2>&1 || need_apt+=(python3-pip)

if [[ ${#need_apt[@]} -gt 0 ]]; then
  echo "Installing system packages: ${need_apt[*]}"
  if ! command -v apt-get >/dev/null 2>&1; then
    echo "apt-get not found; install Python 3 + venv manually."
    exit 1
  fi
  if [[ "$(id -u)" -eq 0 ]]; then
    apt-get update -y
    DEBIAN_FRONTEND=noninteractive apt-get install -y "${need_apt[@]}"
  elif command -v sudo >/dev/null 2>&1; then
    sudo apt-get update -y
    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "${need_apt[@]}"
  else
    echo "Need root for: ${need_apt[*]}"
    exit 1
  fi
fi

VENV="$ROOT/.venv"
if [[ ! -d "$VENV" ]]; then
  echo "Creating virtualenv at $VENV"
  python3 -m venv "$VENV"
fi
# shellcheck disable=SC1091
source "$VENV/bin/activate"

echo "Ensuring optuna-dashboard is installed…"
python -m pip install --upgrade pip setuptools wheel >/dev/null
python -m pip install "optuna>=4.0" optuna-dashboard

HOST="${OPTUNA_DASHBOARD_HOST:-0.0.0.0}"
PORT="${OPTUNA_DASHBOARD_PORT:-8080}"

# Live file (normal read/write URL). Writes are blocked only by HTTP middleware.
STORAGE="sqlite:///${DB}"

# Stop any previous public (or same-port) dashboard so this start owns the port.
kill_other_instances() {
  local pids=()
  local pid
  echo "Stopping other dashboard instances on port ${PORT}…"

  # 1) Anything listening on the target port
  if command -v fuser >/dev/null 2>&1; then
    fuser -k "${PORT}/tcp" 2>/dev/null || true
  fi
  if command -v lsof >/dev/null 2>&1; then
    while read -r pid; do
      [[ -n "$pid" ]] && pids+=("$pid")
    done < <(lsof -t -iTCP:"${PORT}" -sTCP:LISTEN 2>/dev/null || true)
  fi
  if command -v ss >/dev/null 2>&1; then
    while read -r pid; do
      [[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]] && pids+=("$pid")
    done < <(
      ss -tlnp "sport = :${PORT}" 2>/dev/null \
        | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' || true
    )
  fi

  # 2) Known launcher / CLI processes
  while read -r pid; do
    [[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]] && pids+=("$pid")
  done < <(
    pgrep -f "dashboard_public_ro\.py" 2>/dev/null || true
    pgrep -f "optuna-dashboard.*--port[= ]*${PORT}" 2>/dev/null || true
    pgrep -f "optuna-dashboard.*:${PORT}" 2>/dev/null || true
    if [[ "$PORT" == "8080" ]]; then
      pgrep -f "[o]ptuna-dashboard" 2>/dev/null || true
    fi
  )

  local me=$$
  local uniq=()
  for pid in "${pids[@]+"${pids[@]}"}"; do
    [[ "$pid" == "$me" || "$pid" == "$PPID" ]] && continue
    local seen=0
    for u in "${uniq[@]+"${uniq[@]}"}"; do
      [[ "$u" == "$pid" ]] && { seen=1; break; }
    done
    [[ $seen -eq 0 ]] && uniq+=("$pid")
  done

  if [[ ${#uniq[@]} -eq 0 ]]; then
    echo "  (none found)"
    return 0
  fi

  echo "  sending TERM to: ${uniq[*]}"
  kill -TERM "${uniq[@]}" 2>/dev/null || true
  local i
  for i in 1 2 3 4 5 6 7 8 9 10; do
    local still=0
    for pid in "${uniq[@]}"; do
      if kill -0 "$pid" 2>/dev/null; then
        still=1
        break
      fi
    done
    [[ $still -eq 0 ]] && break
    sleep 0.2
  done
  for pid in "${uniq[@]}"; do
    if kill -0 "$pid" 2>/dev/null; then
      echo "  sending KILL to: $pid"
      kill -KILL "$pid" 2>/dev/null || true
    fi
  done
  sleep 0.3
  echo "  done."
}

kill_other_instances

external_ip() {
  curl -fsS -m 2 \
    -H "Metadata-Flavor: Google" \
    "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip" \
    2>/dev/null || true
}

ext="$(external_ip)"
echo
echo "Mode:    PUBLIC — live presets_fin.db; POST/PUT/DELETE/PATCH → 403"
echo "Storage: $STORAGE"
echo "Bind:    http://${HOST}:${PORT}/"
echo "Local:   http://127.0.0.1:${PORT}/"
if [[ -n "$ext" ]]; then
  echo "Public:  http://${ext}:${PORT}/"
else
  echo "Public:  http://<EXTERNAL_IP>:${PORT}/  (see GCP console if unknown)"
fi
echo
echo "Live:    new trials appear without restarting (UI polls)."
echo "Writes:  blocked in dashboard_public_ro.py (not via SQLite mode=ro)."
echo "Firewall: GCP must allow tcp:${PORT} to this VM."
echo "Security: visitors can *see* trials; they cannot tell/fail/delete via UI/API."
echo "Ctrl+C to stop."
echo

exec python "$LAUNCHER" --host "$HOST" --port "$PORT" "$STORAGE"
