#!/usr/bin/env bash
# eval-doctor — read-only preflight for live MCP eval runs.
#
# Walks the dependency chain from tests/mcp_eval/README.md ("Live setup,
# step by step") and prints PASS/FAIL/WARN per check with the exact fix.
# Changes nothing. Exit 0 = ready for live runs (warnings allowed), 1 = not.
#
# Usage:
#   AFFINITY_API_KEY=<key> tests/mcp_eval/bin/eval-doctor [--fast]
#
# Env:
#   AFFINITY_API_KEY    dev API key for the eval org (some checks skip without it)
#   AFFINITY_MONOREPO   path to the affinity monorepo (default: ~/Documents/affinity/affinity)
#   EVAL_ORG_ID         eval org id (default: auto-detected via /v2/auth/whoami)
#   BIFROST_AWS_PROFILE AWS profile the local stack runs on (default: sso-test)
#   --fast              skip the slow feature-gate check (boots the Rails app)

set -u

MONOREPO="${AFFINITY_MONOREPO:-$HOME/Documents/affinity/affinity}"
API="https://api.dev.affinity.vc"
AWS_SSO_PROFILE="${BIFROST_AWS_PROFILE:-sso-test}"
FAST=0
[[ "${1:-}" == "--fast" ]] && FAST=1
[[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }

PASS=0 FAIL=0 WARN=0

ok()   { printf '  \033[32mPASS\033[0m  %s\n' "$1"; PASS=$((PASS+1)); }
bad()  { printf '  \033[31mFAIL\033[0m  %s\n      fix: %s\n' "$1" "$2"; FAIL=$((FAIL+1)); }
warn() { printf '  \033[33mWARN\033[0m  %s\n      %s\n' "$1" "$2"; WARN=$((WARN+1)); }
section() { printf '\n\033[1m%s\033[0m\n' "$1"; }

http_code() { curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$1" 2>/dev/null || echo 000; }
container_up() { docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^$1$"; }
proc_up() { pgrep -f "$1" >/dev/null 2>&1; }

# ---------------------------------------------------------------- step 1
section "Step 1 — machine setup"

if container_up traefik; then ok "1.1 traefik container running"
else bad "1.1 traefik container not running" "start the local docker stack (monorepo)"; fi

if container_up opensearch-node1; then ok "1.2 opensearch-node1 container running"
else bad "1.2 opensearch-node1 not running" "run 'dev setup-global-search' in the monorepo (README 1.2)"; fi

# ---------------------------------------------------------------- step 2
section "Step 2 — services (mprocs panes)"

# Unauthenticated v1 probe against /organizations (the create_company /
# update_company endpoint, also what the 6.2 keyword-search check below
# hits directly): 401 means the service is up; 502 means the external-api
# pane is down, which every v1 tool (search_opportunities, get_reminders,
# create_company, etc.) depends on.
code=$(http_code "$API/organizations")
case "$code" in
  401|200) ok "2.0 external-api (v1) answering" ;;
  000)     bad "2.0 external-api (v1) unreachable (traefik down?)" "see step 1.1" ;;
  *)       bad "2.0 external-api (v1) down -> $code" "start the 'external-api' mprocs pane — every v1 tool (search_opportunities, get_reminders, create_company, etc.) needs it (README 2.0)" ;;
esac

# v2 is a separate pane; the v2 read tools (get_company_info, get_list_entries,
# get_person_info, search_companies_top_matches) all route through it.
code=$(http_code "$API/v2/companies")
case "$code" in
  401|200) ok "2.0 external-api-v2 answering" ;;
  000)     bad "2.0 external-api-v2 unreachable (traefik down?)" "see step 1.1" ;;
  *)       bad "2.0 external-api-v2 down -> $code" "start the 'external-api-v2' mprocs pane — v2 read tools and search_companies_top_matches need it (README 2.0)" ;;
esac

code=$(http_code "http://0.0.0.0:8098/healthz")
if [[ "$code" == 200 ]]; then ok "2.1 gs-search-client healthy (:8098/healthz)"
elif [[ "$code" == 000 ]]; then bad "2.1 gs-search-client down" "start the 'gs-search-client' mprocs pane (README 2.1)"
else bad "2.1 gs-search-client unhealthy (healthz -> $code)" "check the 'gs-search-client' mprocs pane logs (README 2.1)"; fi

for c in a b; do
  if proc_up "run-kafka-consumer-$c"; then ok "2.1 gs-kafka-consumer-$c running"
  else bad "2.1 gs-kafka-consumer-$c not running" "start the 'gs-kafka-consumer-$c' mprocs pane (README 2.1)"; fi
done

bifrost_up=0
code=$(http_code "https://bifrost-admin.dev.affinity.vc/")
case "$code" in
  000) bad "2.2 bifrost unreachable (traefik down?)" "see step 1.1" ;;
  502) bad "2.2 ai-gateway-bifrost down (traefik 502)" "start the 'ai-gateway-bifrost' mprocs pane after 'aws sso login --profile $AWS_SSO_PROFILE' (README 2.2)" ;;
  *)   ok "2.2 ai-gateway-bifrost answering"; bifrost_up=1 ;;
esac

# The CLI session only matters when (re)starting bifrost — a running bifrost
# already holds its Bedrock credentials, so this is advisory.
if ! command -v aws >/dev/null 2>&1; then
  warn "1.3 aws CLI not found on PATH" "needed only to (re)start ai-gateway-bifrost"
elif AWS_PROFILE="$AWS_SSO_PROFILE" aws sts get-caller-identity >/dev/null 2>&1; then
  ok "1.3 AWS CLI session valid (profile $AWS_SSO_PROFILE)"
elif [[ $bifrost_up -eq 1 ]]; then
  warn "1.3 AWS CLI session expired (bifrost is running on earlier creds)" "run 'aws sso login --profile $AWS_SSO_PROFILE' before the next bifrost restart (README 1.3)"
else
  bad "1.3 AWS CLI session expired and bifrost is down" "run 'aws sso login --profile $AWS_SSO_PROFILE' in a plain terminal, then start bifrost (README 1.3/2.2)"
fi

code=$(http_code "https://ai-search-client.dev.affinity.vc/healthz")
case "$code" in
  200) ok "2.2 ai-search-client healthy (/healthz)" ;;
  000) bad "2.2 ai-search-client unreachable (traefik down?)" "see step 1.1" ;;
  502) bad "2.2 ai-search-client down (traefik 502)" "start the 'ai-search-client' mprocs pane (README 2.2)" ;;
  *)   bad "2.2 ai-search-client unhealthy (healthz -> $code)" "check the 'ai-search-client' mprocs pane logs (README 2.2)" ;;
esac

if proc_up "ai_search.consumer.consumer"; then ok "2.2 ai-search-consumer running"
else bad "2.2 ai-search-consumer not running" "start the 'ai-search-consumer' mprocs pane; if it dies with 'address already in use', kill the orphan holding the port (README troubleshooting)"; fi

if proc_up "run_outbox_processor"; then ok "2.3 kafka-outbox-primary running"
else bad "2.3 kafka-outbox-primary not running" "start the 'kafka-outbox-primary' mprocs pane — without it company events sit in the outbox forever (README 2.3)"; fi

# bin/run_eventing_consumers.rb execs into `karafka server`, so the script
# name never appears in the process table; karafka is unique to this pane.
if proc_up "karafka server"; then ok "2.3 eventing-consumers running (karafka server)"
else bad "2.3 eventing-consumers not running" "start the 'eventing-consumers' mprocs pane (README 2.3)"; fi

# ---------------------------------------------------------------- org id
ORG_ID="${EVAL_ORG_ID:-}"
ORG_SUBDOMAIN=""
if [[ -n "${AFFINITY_API_KEY:-}" ]]; then
  whoami_json=$(curl -s --max-time 5 -H "Authorization: Bearer $AFFINITY_API_KEY" "$API/v2/auth/whoami" 2>/dev/null)
  [[ -z "$ORG_ID" ]] && ORG_ID=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["tenant"]["id"])' <<<"$whoami_json" 2>/dev/null || true)
  ORG_SUBDOMAIN=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["tenant"]["subdomain"])' <<<"$whoami_json" 2>/dev/null || true)
fi

# ---------------------------------------------------------------- step 3
section "Step 3 — feature gates"

if [[ $FAST -eq 1 ]]; then
  warn "3.x gate check skipped (--fast)" "rerun without --fast to verify the domain-event gates"
elif [[ ! -d "$MONOREPO" ]]; then
  bad "3.x monorepo not found at $MONOREPO" "set AFFINITY_MONOREPO to your checkout"
elif [[ -z "$ORG_ID" ]]; then
  warn "3.x gate check skipped" "set AFFINITY_API_KEY (or EVAL_ORG_ID) so the org gate can be checked"
else
  gates_out=$(cd "$MONOREPO" && echo "
    puts \"GATE_FANOUT=#{Affinity::Gating.enabled_for_everyone?(Affinity::Gating::COMPANY_GLOBAL_DOMAIN_EVENT_PROCESSING_AND_FANOUT)}\"
    puts \"GATE_ORG=#{Affinity::Gating.enabled_for_org_id?($ORG_ID, Affinity::Gating::COMPANY_DOMAIN_EVENT_ENABLED_ORGS)}\"
    exit
  " | mise exec -- bin/repl 2>/dev/null)
  for g in GATE_FANOUT:company-global-domain-event-processing-and-fanout \
           GATE_ORG:company-domain-event-enabled-orgs; do
    key="${g%%:*}"; name="${g#*:}"
    if grep -q "${key}=true" <<<"$gates_out"; then ok "3.x gate '$name' enabled"
    else bad "3.x gate '$name' NOT enabled" "enable it in bin/repl, then restart eventing-consumers (README step 3)"; fi
  done
fi

# ---------------------------------------------------------------- steps 4+6
section "Steps 4 & 6 — org, seed, and search paths"

if [[ -z "${AFFINITY_API_KEY:-}" ]]; then
  warn "4.2 AFFINITY_API_KEY not set" "export it to check the org, seed data, and search paths (README 4.2)"
else
  body=$(curl -s --max-time 10 -H "Authorization: Bearer $AFFINITY_API_KEY" "$API/v2/companies?limit=100" 2>/dev/null)
  if grep -q "Meridian Biotech" <<<"$body"; then ok "4.3 seed data visible via API (org $ORG_ID)"
  elif grep -q '"data"' <<<"$body"; then bad "4.3 API key works but seed data missing" "seed the org (README 4.3)"
  else bad "4.2/6.1 API not answering with this key" "check the key, and that the stack is up (README 4.2 / 6.1)"; fi

  if [[ -d "$MONOREPO" && -n "$ORG_SUBDOMAIN" && ! -f "$MONOREPO/tmp/manifest-$ORG_SUBDOMAIN.json" ]]; then
    warn "4.3 seed manifest not found at tmp/manifest-$ORG_SUBDOMAIN.json" "teardown will not work without it; reseed with --manifest-file tmp/manifest-$ORG_SUBDOMAIN.json if you ever need to reset"
  fi

  kw=$(curl -s --max-time 10 -u ":$AFFINITY_API_KEY" "$API/organizations?term=Meridian+Biotech" 2>/dev/null)
  if grep -q "Meridian Biotech" <<<"$kw"; then ok "6.2 keyword search returns seeded org data"
  elif grep -qi "bad gateway" <<<"$kw"; then bad "6.2 keyword search: Bad Gateway (note: the live smoke test can still pass — the agent routes around this via search_companies_top_matches's semantic path)" "if check 2.0 failed, start the 'external-api' pane first; otherwise OpenSearch or gs-search-client is unhealthy (README 1.2 / 2.1) — and if the OpenSearch container was recreated its indices are empty (re-run 'dev setup-global-search', then teardown+reseed)"
  else bad "6.2 keyword search returns no seed hit" "gs consumers may not have indexed the seed; check the gs-kafka-consumer panes (README 2.1)"; fi

  sem=$(curl -s --max-time 30 -X POST -H "Authorization: Bearer $AFFINITY_API_KEY" -H "Content-Type: application/json" \
    -d '{"prompt":"biotech companies","limit":5,"entityType":"companies"}' "$API/v2/semantic-search" 2>/dev/null)
  if grep -q "Meridian Biotech" <<<"$sem"; then ok "6.3 semantic search returns seeded org data"
  elif grep -q '"data":\s*\[\]' <<<"$sem"; then bad "6.3 semantic search succeeds but is EMPTY" "embeddings missing: check gates (step 3), outbox/eventing panes (2.3), then run the backfill (README step 5)"
  elif grep -q '"data"' <<<"$sem"; then warn "6.3 semantic search answers but without the seeded biotech company" "embeddings may still be indexing; re-check in a minute"
  else bad "6.3 semantic search erroring" "ai-search-client or bifrost unhealthy (README 2.2)"; fi
fi

# ---------------------------------------------------------------- tooling
section "Tooling (this repo)"

command -v claude >/dev/null 2>&1 && ok "claude CLI on PATH" || bad "claude CLI not found" "install Claude Code (needed by the provider)"
command -v uv >/dev/null 2>&1 && ok "uv on PATH" || bad "uv not found" "install uv"

# ---------------------------------------------------------------- summary
printf '\n\033[1mSummary:\033[0m %d passed, %d failed, %d warnings\n' "$PASS" "$FAIL" "$WARN"
if [[ $FAIL -eq 0 ]]; then
  printf 'Ready for live runs: MCP_EVAL_LIVE=1 AFFINITY_API_KEY=<key> uv run pytest tests/mcp_eval/ -m live -s\n'
  exit 0
else
  printf 'Not ready — fix the FAILs above (top to bottom: each step depends on the previous).\n'
  exit 1
fi
