#!/usr/bin/env bash
# dev-up.sh — stand up the whole VENI-AI dev platform with one command.
#
# Clones the Shell and veni-builder, brings up the Shell's backing services from
# its OWN docker-compose.yml, then configures and starts each — waiting until it's
# actually healthy and surfacing the real error if a service fails to boot.
# Re-runnable: skips anything already done. Run it from an empty workspace folder
# (repos are cloned side by side here).
#
# OS: macOS and Linux run it natively. On Windows use WSL2 (recommended) or Git Bash
# — native cmd/PowerShell can't run this (needs docker compose, make, POSIX paths).
#
#   bash dev-up.sh
#
set -euo pipefail

say()  { printf '\n\033[1;34m▸ %s\033[0m\n' "$1"; }
ok()   { printf '\033[1;32m%s\033[0m\n' "$1"; }
die()  { printf '\n\033[1;31m✗ %s\033[0m\n' "$1" >&2; exit 1; }

SHELL_REPO="https://github.com/VENIZIA-AI/veni-shell.git"
BUILDER_REPO="https://github.com/VENIZIA-AI/veni-builder.git"

# Set APP_ENV_JWT_SECRET to a real value. The committed .env.example ships it BLANK,
# and an empty string bypasses the schema's .default() and fails .min(32) → the API
# crash-loops on "Environment validation failed". This is the whole required-env gap.
set_jwt_secret() {  # $1 = path to .env
  sed -i.bak '/^APP_ENV_JWT_SECRET=/d' "$1" && rm -f "$1.bak"
  echo "APP_ENV_JWT_SECRET=$(openssl rand -hex 32)" >> "$1"
}

# Poll an API's /api/health; on timeout, print its log and explain the likely cause.
wait_healthy() {  # $1 = name, $2 = health url, $3 = logfile
  say "Waiting for $1 to be healthy"
  for i in $(seq 1 90); do
    curl -fsS "$2" >/dev/null 2>&1 && { ok "  $1 is up."; return 0; }
    sleep 2
    if [ "$i" -eq 90 ]; then
      printf '\n\033[1;31m✗ %s did not become healthy in 180s.\033[0m\n' "$1" >&2
      if [ -f "$3" ]; then echo "── last 25 lines of $3 ──" >&2; tail -25 "$3" >&2; fi
      die "$1 failed to start. Check the log above — common causes: a blank required env (set it in its .env), a port already in use, or a stale process. Then re-run."
    fi
  done
}

# `make dev` skips a service whose .pid file exists ("already running"). A .pid left
# by a crashed/killed process therefore makes it skip a service that is actually DOWN
# — the classic "dev-up says healthy but localhost:5173 is refused". Remove any .pid
# whose process is gone, so a re-run truly restarts it.
clear_dead_pids() {  # $1 = repo dir (e.g. veni-shell)
  for p in "$1/api/.pid" "$1/ui/.pid"; do
    [ -f "$p" ] && ! kill -0 "$(cat "$p" 2>/dev/null)" 2>/dev/null && rm -f "$p"
  done
  return 0
}

# Find the PID listening on a TCP port, portably. `ss` (iproute2) is Linux-only and
# absent on macOS/BSD; `lsof` ships on macOS and most Linux. Try lsof first, fall back
# to ss — so the port-freeing in stop_stale actually works on both, instead of silently
# no-op'ing on a Mac (which is how a stale :5173 kept serving old code). Prints nothing
# when the port is free or neither tool exists — callers guard for empty.
port_pid() {  # $1 = port
  if command -v lsof >/dev/null 2>&1; then
    lsof -ti "tcp:$1" -sTCP:LISTEN 2>/dev/null | head -1
  elif command -v ss >/dev/null 2>&1; then
    ss -ltnp 2>/dev/null | grep ":$1 " | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2
  fi
  return 0
}

# Stop leftover API/UI/vite processes from a previous run — INCLUDING zombies whose
# workspace was deleted + re-cloned (their .pid files are gone, so clear_dead_pids
# can't reach them, yet they keep holding the ports and serving stale code — the
# "Failed to resolve @/utils/logger" / "localhost:5173 refused" symptoms). Match by
# command-line path so a deleted-cwd process is still caught. Makes a re-run clean.
stop_stale() {
  # Free the platform's dev ports — kill whatever holds them. Match by PORT, not by
  # command line: the API runs `bun run …` with the repo path only in its cwd (not
  # its argv), so `pkill -f veni-shell/api` misses it — a port lookup catches both
  # the API and the UI, and even a process whose workspace was already deleted.
  for port in 3000 5173 3010 3011; do
    pid=$(port_pid "$port")
    [ -n "${pid:-}" ] && kill "$pid" 2>/dev/null || true
  done
  # Drop stale pidfiles + Vite caches so `make dev` restarts fresh (not "already running").
  for r in veni-shell veni-builder; do
    rm -f  "$r"/api/.pid "$r"/ui/.pid  2>/dev/null || true
    rm -rf "$r"/ui/node_modules/.vite  2>/dev/null || true
  done
  return 0
}

# ── prerequisites ─────────────────────────────────────────────────────────
# node is here because vite runs under it: node_modules/.bin/vite is a node script,
# so bun starts the dev server but node executes it.
for c in docker git bun node make openssl curl; do
  command -v "$c" >/dev/null 2>&1 || die "'$c' is required — install it and re-run."
done
docker compose version >/dev/null 2>&1 || die "docker compose v2 is required."

# Not fatal: freeing stale ports needs lsof (macOS/Linux) or ss (Linux). Neither is on
# a bare Git Bash — there stop_stale just can't reclaim ports, so warn instead of dying.
command -v lsof >/dev/null 2>&1 || command -v ss >/dev/null 2>&1 \
  || printf '\033[1;33m! neither lsof nor ss found — stale dev ports (3000/5173/3010/3011) won'\''t be auto-freed; close them yourself if a start fails.\033[0m\n' >&2

# Ask node for the one export that decides this, rather than parsing a version number:
# @module-federation/dts-plugin imports `styleText` from node:util, which landed in
# 20.12 (and 21.7). On anything older the Shell UI dies with "does not provide an export
# named 'styleText'" — but only after vite has loaded, so without this check it surfaces
# as a 180s health-check timeout with the real cause buried in ui.log.
node -e 'process.exit(require("node:util").styleText ? 0 : 1)' >/dev/null 2>&1 \
  || die "node $(node -v) lacks util.styleText, which the Shell UI's build imports. It shipped in 20.12 and 22.0 but reached the 21.x line only in 21.7 — so 21.0-21.6 misses it despite the higher version number. Install node 22 LTS (or 21.7+) and re-run."

# Clean slate: stop any leftover/zombie API·UI from an earlier run so this one
# starts fresh (avoids stale processes holding :3000/:5173/:3010/:3011).
say "Clearing any leftover processes from a previous run"
stop_stale
ok "  clean."

# ── 1 · Clone the Shell — its repo ships the compose AND the app ───────────
say "Cloning the Shell (veni-shell)"
[ -d veni-shell ] || git clone "$SHELL_REPO"

# ── 2 · Backing services — from the Shell's OWN docker-compose.yml ─────────
# Don't hand-author a compose: veni-shell ships the canonical service defs, with
# ports/creds already matching its api/.env and healthchecks built in. Local
# sign-in is seeded, so bring up only its DB + Redis (skip Keycloak/MinIO); each
# other app owns its own DB in its own repo. `--wait` blocks on the healthchecks.
say "Starting the Shell's Postgres + Redis (from veni-shell/docker-compose.yml)"
( cd veni-shell && docker compose up -d --wait redis shell-db ) \
  || die "The Shell's Postgres/Redis didn't come up — check 'cd veni-shell && docker compose logs redis shell-db'."

# ── 3 · Configure & run the Shell ─────────────────────────────────────────
say "Configuring the Shell"
if [ ! -f veni-shell/api/.env ]; then
  # neither repo commits .env — only .env.example — and `make install` won't copy it.
  cp veni-shell/api/.env.example veni-shell/api/.env
  set_jwt_secret veni-shell/api/.env
  # also drop the invalid placeholder RSA key and author a real one, so remote-app
  # token exchange (RS256 JWKS) works — not just local password login.
  sed -i.bak '/^APP_ENV_JWT_PRIVATE_KEY=/,/END PRIVATE KEY-----"/d' veni-shell/api/.env && rm -f veni-shell/api/.env.bak
  # Collapse the PEM to one line with literal \n escapes. Use awk, not the classic
  # `sed ':a;N;$!ba;…'`: BSD/macOS sed reads the whole `:a;N;$!ba` as the label name
  # and mangles the key — awk behaves identically on GNU and BSD.
  printf 'APP_ENV_JWT_PRIVATE_KEY="%s"\n' \
    "$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null \
       | openssl pkcs8 -topk8 -nocrypt 2>/dev/null | awk '{printf "%s\\n", $0}')" >> veni-shell/api/.env
fi
clear_dead_pids veni-shell
( cd veni-shell && make install && make migrate && make seed && make dev )
wait_healthy "Shell API" "http://localhost:3000/api/health" veni-shell/api.log
wait_healthy "Shell UI"  "http://localhost:5173"            veni-shell/ui.log

# ── 3 · veni-builder (serves the veni CLI) ────────────────────────────────
say "Setting up veni-builder"
[ -d veni-builder ] || git clone "$BUILDER_REPO"
if [ ! -f veni-builder/api/.env ]; then
  cp veni-builder/api/.env.example veni-builder/api/.env   # sets BUILDER_ENABLED=true
  set_jwt_secret veni-builder/api/.env                     # same blank-secret gap as the Shell
fi
[ -f veni-builder/ui/.env ] || cp veni-builder/ui/.env.example veni-builder/ui/.env
clear_dead_pids veni-builder
( cd veni-builder && make install && make dev )
wait_healthy "veni-builder API" "http://localhost:3010/api/health" veni-builder/api.log
wait_healthy "veni-builder UI"  "http://localhost:3011"            veni-builder/ui.log

# ── done ──────────────────────────────────────────────────────────────────
cat <<'DONE'

──────────────────────────────────────────────────────────────
✅ Platform is up and healthy.
   Shell         http://localhost:5173   (UI · API on :3000)
   veni-builder  http://localhost:3011   (UI · API on :3010 — serves the CLI)

   Seeded sign-ins:
     admin@venizia.local     / Admin123!       — platform admin
     developer@venizia.local / Developer123!   — self-service: build & register apps (use this below)
     guest@venizia.local     / Guest123!       — end-user (the plain signed-in view)

Next — get the CLI, then build your app:
  1. curl -fsSL http://localhost:3010/api/cli/install.sh | bash          # :3010 = builder API
     No token needed: it prints a code, opens your browser, and waits while you
     approve — sign in as developer@venizia.local. Already hold a veni_ token?
     Add -H "X-Veni-Token: <token>" and it skips the browser.
  2. export PATH="$HOME/.local/bin:$PATH"    # add to ~/.bashrc to keep it
     veni doctor                             # bun/curl/tar, builder, shell, token, PATH
  3. veni create calendar --api --db    # picks free ports; the :5173 in its .env is VITE_SHELL_URL (points back at the Shell)
  4. cd calendar
     docker compose up -d calendar-db        # `create --db` writes the files; nothing is running yet
     (cd api && bun run db:migrate)          # identity tables + seeded roles
  5. veni install
  6. veni register                           # tells the Shell your app exists, at the ports it picked
     Uses the Shell you signed in to. It also rewrites api/.env (JWKS · gRPC · CORS),
     so run it BEFORE step 7 — otherwise restart the API to pick the changes up.
     A personal token registers through developer self-service, so the app lands as a
     PRIVATE DRAFT: you see it, others don't. Publish it as admin@venizia.local.
  7. veni dev                                # runs api + ui in the foreground; leave it running
     Then open http://localhost:5173 and your app is in the Shell — signed in already,
     no second login. Not there? `veni status` shows the ports, theme and Shell it registered with.
──────────────────────────────────────────────────────────────
DONE
