#!/usr/bin/env bash
# scripts/ci-installer-validation.sh
#
# Run inside an Oracle Linux 7/8 Docker container by .github/workflows/installer-validation.yml.
# Installs the TuneVault agent via install.sh --headless, runs the 7-probe self-test,
# and prints a JSON summary as the last line of stdout.
#
# Environment variables expected (injected by CI):
#   ORACLE_TEST_DSN      — host:port/service (e.g. 192.0.2.10:1521/ORCL)
#   ORACLE_TEST_USER     — Oracle username
#   ORACLE_TEST_PASSWORD — Oracle password
#   TEST_TENANT_EMAIL    — test tenant email (registered in TuneVault)
#   TEST_TENANT_API_KEY  — API key for that tenant
#   TUNEVAULT_APP_URL    — TuneVault app URL (for agent channel)
#
# Output: runs print to stdout; last line is JSON with probe results.

set -euo pipefail

RESULTS_DIR="/results"
OS_KEY="${OS_KEY:-unknown}"
START_MS=$(date +%s%3N)

log() { echo "[ci-validator] $*" >&2; }
die() { echo "[ci-validator] FATAL: $*" >&2; exit 1; }

# ── Install paths — single source of truth is install.sh ─────────────────────
# NEVER hardcode the agent install path here. install.sh owns it (INSTALL_DIR),
# and this script drifted silently for months when it kept its own copy.
# `|| true` keeps the failure recoverable: under `set -e` a failing command
# substitution aborts the script outright, so the die() below would never run.
INSTALL_DIR=$(grep -m1 '^INSTALL_DIR=' /tunevault/install.sh | cut -d'"' -f2 || true)
[ -n "$INSTALL_DIR" ] || die "could not read INSTALL_DIR from install.sh"
VENV_PYTHON="${INSTALL_DIR}/venv/bin/python3"
PROXY_SCRIPT="${INSTALL_DIR}/oracle-proxy.py"
log "Install paths from install.sh: INSTALL_DIR=${INSTALL_DIR}"

AGENT_ENV=$(grep -m1 '^ENV_FILE=' /tunevault/install.sh | cut -d'"' -f2 || true)
AGENT_ENV="${AGENT_ENV:-/etc/tunevault/agent.env}"

# ── OS key — derived, never "unknown" ────────────────────────────────────────
# PRE-EXISTING BUG (not introduced by tonight's changes): the workflow never
# passes -e OS_KEY into the container, so OS_KEY fell back to "unknown" on every
# run since this script was written. That misnamed agent-version-${OS_KEY}.txt,
# which the workflow's "Capture OS + agent metadata" step reads back as
# agent-version-<matrix.os_key>.txt — a filename that therefore NEVER matched,
# leaving agent_version empty in every reported payload.
# Derived here from /etc/os-release so it is correct with no yml change.
# Oracle Linux reports ID="ol" in os-release; install.sh normalises to "oracle"
# off /etc/oracle-release, so accept both.
if [ "$OS_KEY" = "unknown" ] && [ -r /etc/os-release ]; then
  # grep rather than `. /etc/os-release`: sourcing would import every field into
  # this shell (NAME, VERSION, …) and trips `set -u` on absent fields.
  _os_id=$(grep -m1 '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"' || true)
  _os_ver=$(grep -m1 '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"' || true)
  _os_major="${_os_ver%%.*}"
  if [ -n "$_os_major" ]; then
    case "$_os_id" in
      ol|oracle) OS_KEY="ol${_os_major}" ;;
      *)         OS_KEY="${_os_id}${_os_major}" ;;
    esac
  fi
fi
log "OS key: ${OS_KEY}"

# ── 1. Run install.sh --headless ─────────────────────────────────────────────
log "Starting install.sh --headless..."
bash /tunevault/install.sh --headless 2>&1 || {
  log "install.sh exited non-zero"
  END_MS=$(date +%s%3N)
  cat <<EOF
{"probes":[{"n":1,"status":"fail","ms":null,"error":"install.sh exited non-zero"}],"overall":"fail","duration_total_ms":$((END_MS - START_MS))}
EOF
  exit 1
}
log "install.sh --headless completed"

# ── 2. Capture agent version ──────────────────────────────────────────────────
AGENT_VER=""
if [ -f /usr/local/bin/tunevault-agent ]; then
  AGENT_VER=$(tunevault-agent --version 2>/dev/null || echo "")
fi
if [ -n "$AGENT_VER" ]; then
  echo "$AGENT_VER" > "${RESULTS_DIR}/agent-version-${OS_KEY}.txt" 2>/dev/null || true
fi

# ── 3. Start oracle-proxy.py directly (headless — no systemd) ────────────────
log "Starting oracle-proxy.py in background..."
PROXY_PID=""
if [ -f "$PROXY_SCRIPT" ]; then
  # Build proxy.env for self-test
  cat > /tmp/proxy.env <<ENVEOF
ORACLE_TEST_DSN=${ORACLE_TEST_DSN:-}
ORACLE_TEST_USER=${ORACLE_TEST_USER:-}
ORACLE_TEST_PASSWORD=${ORACLE_TEST_PASSWORD:-}
TUNEVAULT_APP_URL=${TUNEVAULT_APP_URL:-}
TEST_TENANT_API_KEY=${TEST_TENANT_API_KEY:-}
ENVEOF

  # oracle-proxy.py demands TUNEVAULT_API_KEY at MODULE scope (line 406-414) —
  # before main() — and sys.exit(1)s without it. install.sh already wrote one to
  # agent.env (headless: "headless-test-key"), so read it back rather than
  # inventing a second value here.
  # Targeted grep, not `source`: agent.env is a systemd EnvironmentFile, whose
  # unquoted values are not guaranteed to be safe to evaluate as shell.
  if [ -r "$AGENT_ENV" ]; then
    TUNEVAULT_API_KEY=$(grep -m1 '^TUNEVAULT_API_KEY=' "$AGENT_ENV" | cut -d= -f2- || true)
    export TUNEVAULT_API_KEY
    if [ -n "${TUNEVAULT_API_KEY:-}" ]; then
      log "TUNEVAULT_API_KEY loaded from ${AGENT_ENV} (${#TUNEVAULT_API_KEY} chars)"
    else
      log "WARNING: no TUNEVAULT_API_KEY in ${AGENT_ENV} — proxy will refuse to start"
    fi
  else
    log "WARNING: ${AGENT_ENV} not readable — proxy will refuse to start"
  fi

  # Port comes from the PORT env var (oracle-proxy.py:399, default 3100).
  # main() (oracle-proxy.py:9127) parses ONLY --no-auto-update / --update-now /
  # --validate-only and silently ignores anything else — so the old
  # `--port 8765 --headless` was accepted and discarded, leaving the proxy on
  # 3100 while probes 2 and 7 knocked on 8765.
  #
  # Capture stdout AND stderr: the proxy prints its fatal startup errors to
  # stdout, so a stderr-only redirect left the decisive line out of the artifact.
  PORT=8765 "$VENV_PYTHON" "$PROXY_SCRIPT" >/tmp/proxy.log 2>&1 &
  PROXY_PID=$!
  sleep 3  # allow startup
  log "oracle-proxy.py started (PID $PROXY_PID) on port 8765"
else
  log "oracle-proxy.py not found at ${PROXY_SCRIPT} — probe results will reflect this"
fi

# ── 4. Run 7-probe self-test ──────────────────────────────────────────────────
log "Running 7-probe self-test..."

PROBE_RESULTS=()
ALL_PASS=true
TOTAL_FAIL=0

run_probe() {
  local N=$1
  local NAME=$2
  local CMD=$3
  local PROBE_START=$(date +%s%3N)

  log "  [${N}/7] ${NAME}..."
  set +e
  OUTPUT=$(eval "$CMD" 2>&1)
  local EXIT=$?
  set -e
  local PROBE_END=$(date +%s%3N)
  local PROBE_MS=$((PROBE_END - PROBE_START))

  if [ $EXIT -eq 0 ]; then
    PROBE_RESULTS+=("{\"n\":${N},\"status\":\"pass\",\"ms\":${PROBE_MS},\"error\":null}")
    log "  [${N}/7] ${NAME} ............... PASS (${PROBE_MS}ms)"
  else
    ALL_PASS=false
    TOTAL_FAIL=$((TOTAL_FAIL + 1))
    # Truncate error to 200 chars, escape JSON
    local ERR=$(echo "$OUTPUT" | tail -3 | tr -d '"\\' | head -c 200)
    PROBE_RESULTS+=("{\"n\":${N},\"status\":\"fail\",\"ms\":${PROBE_MS},\"error\":\"${ERR}\"}")
    log "  [${N}/7] ${NAME} ............... FAIL (${PROBE_MS}ms)"
    log "  → $ERR"
  fi
}

skip_probe() {
  local N=$1
  local NAME=$2
  local REASON=$3
  PROBE_RESULTS+=("{\"n\":${N},\"status\":\"skip\",\"ms\":null,\"error\":\"${REASON}\"}")
  log "  [${N}/7] ${NAME} ............... SKIP (${REASON})"
}

# Probe 1: Python env — python-oracledb import.
# install.sh has NEVER installed cx_Oracle; it installs python-oracledb in thin
# mode only (install.sh:765). Asserting cx_Oracle here failed unconditionally.
run_probe 1 "Python env (python-oracledb)" \
  "\"$VENV_PYTHON\" -c 'import oracledb; print(oracledb.__version__)' 2>&1"

# Probe 2: Agent connectivity (proxy /health)
if [ -n "$PROXY_PID" ]; then
  run_probe 2 "Agent connectivity" \
    "curl -sf --max-time 15 http://localhost:8765/health | grep -q '\"status\"'"
else
  skip_probe 2 "Agent connectivity" "proxy not started"
fi

# Probe 3: TNS / listener (requires ORACLE_TEST_DSN)
if [ -n "${ORACLE_TEST_DSN:-}" ]; then
  DSN_HOST=$(echo "$ORACLE_TEST_DSN" | cut -d: -f1)
  DSN_PORT=$(echo "$ORACLE_TEST_DSN" | cut -d: -f2 | cut -d/ -f1)
  run_probe 3 "TNS/listener resolution" \
    "timeout 15 bash -c \"echo '' > /dev/tcp/${DSN_HOST}/${DSN_PORT:-1521}\" 2>&1"
else
  skip_probe 3 "TNS/listener resolution" "ORACLE_TEST_DSN not configured"
fi

# Probe 4: Oracle credentials (requires DSN + credentials)
PROBE4_OK=false
if [ -n "${ORACLE_TEST_DSN:-}" ] && [ -n "${ORACLE_TEST_USER:-}" ] && [ -n "${ORACLE_TEST_PASSWORD:-}" ]; then
  run_probe 4 "Oracle credentials" \
    "\"$VENV_PYTHON\" -c \"
import oracledb
conn = oracledb.connect(user='${ORACLE_TEST_USER}', password='${ORACLE_TEST_PASSWORD}', dsn='${ORACLE_TEST_DSN}')
cur = conn.cursor()
cur.execute('SELECT 1 FROM DUAL')
conn.close()
print('ok')
\" 2>&1"
  # Check if probe 4 passed
  LAST_RESULT="${PROBE_RESULTS[-1]}"
  if echo "$LAST_RESULT" | grep -q '"status":"pass"'; then
    PROBE4_OK=true
  fi
else
  skip_probe 4 "Oracle credentials" "credentials not configured"
fi

# Probe 5: SSH bastion (skip if no SSH config — not required for basic validation)
skip_probe 5 "SSH bastion" "SSH bastion not applicable in headless CI mode"

# Probe 6: End-to-end query (only if probe 4 passed)
if [ "$PROBE4_OK" = "true" ]; then
  run_probe 6 "End-to-end query (V\$INSTANCE)" \
    "\"$VENV_PYTHON\" -c \"
import oracledb
conn = oracledb.connect(user='${ORACLE_TEST_USER}', password='${ORACLE_TEST_PASSWORD}', dsn='${ORACLE_TEST_DSN}')
cur = conn.cursor()
cur.execute('SELECT STATUS, DATABASE_STATUS FROM V\\\$INSTANCE')
row = cur.fetchone()
conn.close()
print('status:', row[0], 'db_status:', row[1])
\" 2>&1"
else
  skip_probe 6 "End-to-end query" "skipped because probe 4 (credentials) did not pass"
fi

# Probe 7: Proxy version current — /api/test must return 410 (not 200/stale)
# In headless CI mode the proxy was started on port 8765 (not default 3100).
# PASS if response is 410 Gone; FAIL if 200 (legacy endpoint still active).
if [ -n "$PROXY_PID" ]; then
  PROBE7_HTTP=$(curl -sS -o /dev/null -w '%{http_code}' \
    -X POST http://localhost:8765/api/test \
    -H 'X-Api-Key: dummy' -d '{}' --max-time 5 2>/dev/null || echo "000")
  if [ "$PROBE7_HTTP" = "410" ]; then
    PROBE7_MS=0
    PROBE_RESULTS+=("{\"n\":7,\"status\":\"pass\",\"ms\":${PROBE7_MS},\"error\":null}")
    log "  [7/7] Proxy version current ............... PASS (410 Gone — proxy current)"
  else
    ALL_PASS=false
    TOTAL_FAIL=$((TOTAL_FAIL + 1))
    PROBE_ERR="Stale proxy: /api/test returned ${PROBE7_HTTP} (expected 410 Gone)"
    PROBE_RESULTS+=("{\"n\":7,\"status\":\"fail\",\"ms\":0,\"error\":\"${PROBE_ERR}\"}")
    log "  [7/7] Proxy version current ............... FAIL (http_code=${PROBE7_HTTP})"
    log "  → ${PROBE_ERR}"
  fi
else
  skip_probe 7 "Proxy version current" "proxy not started"
fi

# ── 5. Capture proxy state + log, then kill proxy ────────────────────────────
# Runs win or lose: nothing between here and the JSON echo can exit early, and
# every step below is failure-tolerant under `set -e`.
PROXY_STATE="not started"
if [ -n "$PROXY_PID" ]; then
  if kill -0 "$PROXY_PID" 2>/dev/null; then
    PROXY_STATE="alive after probes (pid ${PROXY_PID})"
    kill "$PROXY_PID" 2>/dev/null || true
  else
    # Already dead — recover its exit code so a startup abort is diagnosable.
    # oracle-proxy.py exit map: 3=HTTP server, 4=env/API key, 5=deps,
    # 6=cloud DNS, 7=cloud /api/health, 8=poll thread.
    set +e
    wait "$PROXY_PID" 2>/dev/null
    PROXY_EXIT=$?
    set -e
    PROXY_STATE="exited before probes finished (exit ${PROXY_EXIT})"
  fi
fi
log "oracle-proxy.py state: ${PROXY_STATE}"

# Ship the proxy's stderr with the run — without this a startup abort is
# invisible in CI, since /tmp/proxy.log never left the container.
{
  echo "=== oracle-proxy.py state: ${PROXY_STATE} ==="
  echo "=== stdout+stderr (/tmp/proxy.log) ==="
  cat /tmp/proxy.log 2>/dev/null || echo "(no /tmp/proxy.log — proxy never launched)"
} > "${RESULTS_DIR}/proxy-${OS_KEY}.log" 2>/dev/null || log "could not write proxy-${OS_KEY}.log"

# ── 6. Build JSON result ──────────────────────────────────────────────────────
END_MS=$(date +%s%3N)
DURATION=$((END_MS - START_MS))
PROBES_JSON=$(IFS=,; echo "[${PROBE_RESULTS[*]}]")

if [ "$ALL_PASS" = "true" ]; then
  OVERALL="pass"
elif [ $TOTAL_FAIL -gt 0 ]; then
  OVERALL="fail"
else
  OVERALL="pass"  # all skipped = pass (no oracle target configured)
fi

log "Result: overall=${OVERALL} probes=${#PROBE_RESULTS[@]} failed=${TOTAL_FAIL} duration=${DURATION}ms"

# JSON summary — must be the last line of stdout for CI to parse
echo "{\"probes\":${PROBES_JSON},\"overall\":\"${OVERALL}\",\"duration_total_ms\":${DURATION}}"

# Exit status must reflect OVERALL. Without this the script ended on `echo`,
# so overall=fail still exited 0 and the workflow went green on a failed run.
# Stays AFTER the echo so the JSON remains the last line of stdout.
[ "$OVERALL" = "pass" ] || exit 1
