#!/usr/bin/env bash
# Manage the complete MercadoDeVida monolith on one development/LAN host.
# Usage: ./scripts/monolith.sh <dev|prod> <start|restart|status|stop|logs|urls>

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
MODE="${1:-}"
ACTION="${2:-}"
RUNTIME_ROOT="${MDV_RUNTIME_DIR:-$PROJECT_DIR/.runtime}"
RUNTIME_DIR="$RUNTIME_ROOT/$MODE"
BACKEND_PORT="${BACKEND_PORT:-3000}"
FRONTEND_PORT="${FRONTEND_PORT:-3003}"
ADMIN_PORT="${ADMIN_PORT:-3004}"
STOREFRONT_PORT="${STOREFRONT_PORT:-3005}"
START_TIMEOUT="${START_TIMEOUT:-90}"
WATCH_INTERVAL="${WATCH_INTERVAL:-5}"

SERVICES=(backend frontend admin storefront)

usage() {
  cat <<'EOF'
Usage: ./scripts/monolith.sh <dev|prod> <command>

Commands:
  start    Install/build when needed, migrate, and start every service
  restart  Stop managed processes, then start every service
  status   Show PID, process state, HTTP status, and URLs
  stop     Gracefully stop every managed HTTP process
  logs     Follow all service logs (Ctrl-C exits without stopping services)
  urls     Print localhost and LAN URLs
  watch    Auto-respawn any dead service every WATCH_INTERVAL seconds

Environment overrides:
  LAN_IP, BACKEND_PORT, FRONTEND_PORT, ADMIN_PORT, STOREFRONT_PORT
  MDV_RUNTIME_DIR, START_TIMEOUT
EOF
}

if [[ "$MODE" != "dev" && "$MODE" != "prod" ]]; then
  usage >&2
  exit 2
fi
case "$ACTION" in
  start|restart|status|stop|logs|urls|watch) ;;
  *) usage >&2; exit 2 ;;
esac

mkdir -p "$RUNTIME_DIR"

lan_ip() {
  if [[ -n "${LAN_IP:-}" ]]; then
    printf '%s\n' "$LAN_IP"
    return
  fi

  local iface ip
  if command -v route >/dev/null 2>&1 && command -v ipconfig >/dev/null 2>&1; then
    iface="$(route -n get default 2>/dev/null | awk '/interface:/{print $2; exit}')"
    if [[ -n "$iface" ]]; then
      ip="$(ipconfig getifaddr "$iface" 2>/dev/null || true)"
      if [[ -n "$ip" ]]; then printf '%s\n' "$ip"; return; fi
    fi
  fi
  if command -v hostname >/dev/null 2>&1; then
    ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
    if [[ -n "$ip" ]]; then printf '%s\n' "$ip"; return; fi
  fi
  printf '127.0.0.1\n'
}

LAN_ADDRESS="$(lan_ip)"
API_PUBLIC_URL="${API_PUBLIC_URL:-http://$LAN_ADDRESS:$BACKEND_PORT}"

service_port() {
  case "$1" in
    backend) echo "$BACKEND_PORT" ;;
    frontend) echo "$FRONTEND_PORT" ;;
    admin) echo "$ADMIN_PORT" ;;
    storefront) echo "$STOREFRONT_PORT" ;;
  esac
}

service_path() {
  case "$1" in
    backend) echo "/health" ;;
    *) echo "/" ;;
  esac
}

service_url() {
  local service="$1" host="${2:-127.0.0.1}"
  printf 'http://%s:%s%s\n' "$host" "$(service_port "$service")" "$(service_path "$service")"
}

pid_file() { printf '%s/%s.pid\n' "$RUNTIME_DIR" "$1"; }
log_file() { printf '%s/%s.log\n' "$RUNTIME_DIR" "$1"; }

read_pid() {
  local file
  file="$(pid_file "$1")"
  [[ -f "$file" ]] && tr -dc '0-9' < "$file" || true
}

is_running() {
  local pid
  pid="$(read_pid "$1")"
  [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}

port_pid() {
  local port="$1"
  if command -v lsof >/dev/null 2>&1; then
    lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -1 || true
  fi
}

assert_port_available() {
  local service="$1" port existing managed
  port="$(service_port "$service")"
  existing="$(port_pid "$port")"
  managed="$(read_pid "$service")"
  if [[ -n "$existing" && "$existing" != "$managed" ]]; then
    echo "[FAIL] $service port $port is already used by unmanaged PID $existing" >&2
    echo "       Stop it explicitly before retrying; this script never broad-kills processes." >&2
    return 1
  fi
}

wait_http() {
  local service="$1" url deadline code
  url="$(service_url "$service")"
  deadline=$((SECONDS + START_TIMEOUT))
  while (( SECONDS < deadline )); do
    if ! is_running "$service"; then
      echo "[FAIL] $service exited during startup. Last log lines:" >&2
      tail -40 "$(log_file "$service")" >&2 || true
      return 1
    fi
    code="$(curl --max-time 3 -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)"
    if [[ "$code" =~ ^[23] ]]; then
      echo "[OK] $service ready ($code) — $url"
      return 0
    fi
    sleep 1
  done
  echo "[FAIL] $service did not become healthy at $url in ${START_TIMEOUT}s" >&2
  tail -40 "$(log_file "$service")" >&2 || true
  return 1
}

ensure_env() {
  if [[ ! -f "$PROJECT_DIR/.env" ]]; then
    cat > "$PROJECT_DIR/.env" <<'EOF'
DATABASE_URL=postgres://mdv:mdv_dev_only@localhost:5432/mercadodevida
REDIS_URL=redis://localhost:6379
COOKIE_SECURE=false
EOF
    echo "[INFO] Created project/.env for local development"
  fi
}

start_infrastructure() {
  command -v docker >/dev/null 2>&1 || { echo '[FAIL] Docker is required' >&2; exit 1; }
  echo '[INFO] Starting PostgreSQL and Redis...'

  # Always go through Compose instead of plain `docker start` so changes such
  # as Redis AOF or explicit volume names are reconciled without losing named
  # volumes. Compose recreates containers when needed but keeps
  # project_mdv_pg_data/project_mdv_redis_data attached.
  (cd "$PROJECT_DIR" && docker compose up -d postgres redis)

  local deadline=$((SECONDS + 60))
  until docker exec mdv-dev-postgres pg_isready -U mdv -d mercadodevida >/dev/null 2>&1; do
    (( SECONDS < deadline )) || { echo '[FAIL] PostgreSQL did not become ready' >&2; exit 1; }
    sleep 1
  done
  until docker exec mdv-dev-redis redis-cli ping >/dev/null 2>&1; do
    (( SECONDS < deadline )) || { echo '[FAIL] Redis did not become ready' >&2; exit 1; }
    sleep 1
  done
}

install_dependencies() {
  local install_command=(npm install)
  [[ "$MODE" == "prod" ]] && install_command=(npm ci)
  for dir in "$PROJECT_DIR" "$PROJECT_DIR/apps/admin" "$PROJECT_DIR/frontend" "$PROJECT_DIR/storefront"; do
    echo "[INFO] ${install_command[*]} — ${dir#$PROJECT_DIR/}"
    (cd "$dir" && "${install_command[@]}")
  done
}

migrate() {
  echo '[INFO] Applying database migrations...'
  (cd "$PROJECT_DIR" && node --env-file-if-exists=.env node_modules/node-pg-migrate/bin/node-pg-migrate.js up --migrations-dir migrations)
}

build_prod() {
  echo '[INFO] Building backend...'
  (cd "$PROJECT_DIR" && npm run build)
  echo '[INFO] Building admin...'
  (cd "$PROJECT_DIR/apps/admin" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
  echo '[INFO] Building customer frontend...'
  (cd "$PROJECT_DIR/frontend" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
  echo '[INFO] Building SEO storefront...'
  (cd "$PROJECT_DIR/storefront" && API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build)
  sync_uploads
}

sync_uploads() {
  # Mirror admin's public/uploads to frontend and storefront so that product
  # images are reachable from any of the three apps.  Also mirrors every
  # generated thumbnail subdir (40/, 200/, ...) so the static cached files
  # reach all three apps in lockstep.
  local src="$PROJECT_DIR/apps/admin/public/uploads"
  local peers=(
    "$PROJECT_DIR/frontend/public/uploads"
    "$PROJECT_DIR/storefront/public/uploads"
  )
  if [[ ! -d "$src" ]]; then
    return 0
  fi
  for peer in "${peers[@]}"; do
    mkdir -p "$peer"
    find "$src" -maxdepth 2 -type f -print0 2>/dev/null | while IFS= read -r -d '' f; do
      local rel
      rel="${f#$src/}"
      if [[ ! -f "$peer/$rel" ]]; then
        mkdir -p "$(dirname "$peer/$rel")"
        cp "$f" "$peer/$rel" 2>/dev/null || true
      fi
    done
  done
  # Ensure thumbnails are up to date for the current image set.
  "$PROJECT_DIR/scripts/generate-thumbnails.sh" --quiet
}

spawn_service() {
  local service="$1" dir log pidfile port
  dir="$PROJECT_DIR"
  log="$(log_file "$service")"
  pidfile="$(pid_file "$service")"
  port="$(service_port "$service")"
  : > "$log"

  case "$MODE:$service" in
    dev:backend)
      (cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=development node --env-file=.env node_modules/tsx/dist/cli.mjs watch src/infrastructure/http/server.ts >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
      ;;
    prod:backend)
      (cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=production node --env-file=.env dist/infrastructure/http/server.js >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
      ;;
    dev:admin)
      (cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
      ;;
    prod:admin)
      (cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
      ;;
    dev:frontend)
      (cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
      ;;
    prod:frontend)
      (cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
      ;;
    dev:storefront)
      (cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
      ;;
    prod:storefront)
      (cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
      ;;
  esac
  sleep 1
  wait_http "$service"
  local listener_pid
  listener_pid="$(port_pid "$port")"
  if [[ -n "$listener_pid" ]]; then
    echo "$listener_pid" > "$pidfile"
  fi
}

start_all() {
  ensure_env
  for service in "${SERVICES[@]}"; do
    if is_running "$service"; then
      echo "[FAIL] $service is already managed in $MODE mode; use restart" >&2
      exit 1
    fi
    rm -f "$(pid_file "$service")"
    assert_port_available "$service"
  done
  start_infrastructure
  install_dependencies
  migrate
  [[ "$MODE" == "prod" ]] && build_prod
  sync_uploads
  for service in "${SERVICES[@]}"; do spawn_service "$service"; done
  echo
  print_urls
}

stop_service() {
  local service="$1" pid deadline
  pid="$(read_pid "$service")"
  if [[ -z "$pid" ]]; then
    rm -f "$(pid_file "$service")"
    return
  fi
  if ! kill -0 "$pid" 2>/dev/null; then
    rm -f "$(pid_file "$service")"
    return
  fi
  echo "[INFO] Stopping $service (PID $pid)..."
  kill -TERM "$pid" 2>/dev/null || true
  deadline=$((SECONDS + 15))
  while kill -0 "$pid" 2>/dev/null && (( SECONDS < deadline )); do sleep 1; done
  if kill -0 "$pid" 2>/dev/null; then
    echo "[WARN] $service did not stop gracefully; sending KILL"
    kill -KILL "$pid" 2>/dev/null || true
  fi
  rm -f "$(pid_file "$service")"
}

stop_all() {
  local i
  for ((i=${#SERVICES[@]}-1; i>=0; i--)); do stop_service "${SERVICES[$i]}"; done
}

status_all() {
  ensure_alive silent
  local service pid state code url lan_url failed=0
  printf '%-11s %-8s %-10s %-6s %s\n' SERVICE PID PROCESS HTTP URL
  for service in "${SERVICES[@]}"; do
    pid="$(read_pid "$service")"
    state='stopped'
    [[ -n "$pid" ]] || pid='-'
    if [[ "$pid" != '-' ]] && kill -0 "$pid" 2>/dev/null; then state='running'; fi
    url="$(service_url "$service")"
    code="$(curl --max-time 3 -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)"
    [[ -n "$code" && "$code" != '000' ]] || code='-'
    lan_url="$(service_url "$service" "$LAN_ADDRESS")"
    printf '%-11s %-8s %-10s %-6s %s\n' "$service" "$pid" "$state" "$code" "$lan_url"
    [[ "$state" == 'running' && "$code" =~ ^[23] ]] || failed=1
  done
  echo
  (cd "$PROJECT_DIR" && docker compose ps) || true
  return "$failed"
}

# ensure_alive [silent|verbose]
# Re-spawn any service whose tracked PID is not running. Used by `status`
# so an operator who runs `monolith.sh status` always sees live processes
# even after a backend crash with no error trace.
ensure_alive() {
  local verbose="${1:-silent}" service pid
  [[ "$verbose" == "verbose" ]] || verbose='silent'
  for service in "${SERVICES[@]}"; do
    pid="$(read_pid "$service")"
    if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
      continue
    fi
    if [[ "$verbose" == "verbose" ]]; then
      echo "[WATCHDOG] $service not running (last PID: ${pid:-none}); respawning..."
    fi
    rm -f "$(pid_file "$service")"
    # Only respawn if infrastructure is reachable; otherwise let `start` handle it.
    if (cd "$PROJECT_DIR" && docker ps --format '{{.Names}}' 2>/dev/null | grep -qE '^(mdv-dev-postgres|mdv-dev-redis)$'); then
      spawn_service "$service" || true
    fi
  done
}

watch_loop() {
  echo "[WATCHDOG] watching services every ${WATCH_INTERVAL}s (Ctrl+C to stop)"
  ensure_alive verbose
  while true; do
    sleep "$WATCH_INTERVAL"
    ensure_alive verbose
  done
}

print_urls() {
  cat <<EOF
Mode: $MODE
LAN IP: $LAN_ADDRESS

Customer frontend: http://$LAN_ADDRESS:$FRONTEND_PORT/
Admin backoffice:   http://$LAN_ADDRESS:$ADMIN_PORT/
SEO storefront:     http://$LAN_ADDRESS:$STOREFRONT_PORT/
Backend health:     http://$LAN_ADDRESS:$BACKEND_PORT/health
Swagger/OpenAPI:    http://$LAN_ADDRESS:$BACKEND_PORT/docs

Local equivalents use 127.0.0.1 with the same ports.
EOF
}

follow_logs() {
  local files=() service
  for service in "${SERVICES[@]}"; do
    touch "$(log_file "$service")"
    files+=("$(log_file "$service")")
  done
  tail -n 100 -F "${files[@]}"
}

case "$ACTION" in
  start) start_all ;;
  restart) stop_all; start_all ;;
  status) status_all ;;
  stop) stop_all ;;
  logs) follow_logs ;;
  urls) print_urls ;;
  watch) watch_loop ;;
esac
