#!/usr/bin/env bash
# 🔧 Development Environment Manager
# Clean, portable helper for local dev services & apps

set -Eeuo pipefail
IFS=$'\n\t'

########################################
# Config (override via env if you like)
########################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="${PROJECT_ROOT:-"$(cd "$SCRIPT_DIR/../.." && pwd)"}"

# Compose file selection
# Priority: explicit COMPOSE_FILE > DEV_STACK (full|services|auto) > default (full if present)
DEFAULT_COMPOSE_SERVICES="$PROJECT_ROOT/docker/dev/docker-compose.yml"
DEFAULT_COMPOSE_FULL="$PROJECT_ROOT/docker/dev/docker-compose.full.yml"

if [ -z "${COMPOSE_FILE:-}" ]; then
  case "${DEV_STACK:-services}" in
    full)
      COMPOSE_FILE="$DEFAULT_COMPOSE_FULL"
      ;;
    services)
      COMPOSE_FILE="$DEFAULT_COMPOSE_SERVICES"
      ;;
    auto)
      if [ -f "$DEFAULT_COMPOSE_FULL" ]; then
        COMPOSE_FILE="$DEFAULT_COMPOSE_FULL"
      else
        COMPOSE_FILE="$DEFAULT_COMPOSE_SERVICES"
      fi
      ;;
    *)
      COMPOSE_FILE="$DEFAULT_COMPOSE_FULL"
      ;;
  esac
fi

ENV_FILE="${ENV_FILE:-"$PROJECT_ROOT/.env"}"
ENV_EXAMPLE_FILE="${ENV_EXAMPLE_FILE:-"$PROJECT_ROOT/.env.example"}"
PROJECT_NAME="${PROJECT_NAME:-portal-dev}"

DB_USER_DEFAULT="dev"
DB_NAME_DEFAULT="portal_dev"
DB_WAIT_SECS="${DB_WAIT_SECS:-30}"

NEXT_PORT_DEFAULT=3000
BFF_PORT_DEFAULT=4000

########################################
# Colors (fallback if tput missing)
########################################
if command -v tput >/dev/null 2>&1 && [ -t 1 ]; then
  GREEN="$(tput setaf 2)"
  YELLOW="$(tput setaf 3)"
  RED="$(tput setaf 1)"
  NC="$(tput sgr0)"
else
  GREEN='\033[0;32m'
  YELLOW='\033[1;33m'
  RED='\033[0;31m'
  NC='\033[0m'
fi

log()   { echo -e "${GREEN}[DEV] $*${NC}"; }
warn()  { echo -e "${YELLOW}[DEV] $*${NC}"; }
fail()  { echo -e "${RED}[DEV] ERROR: $*${NC}"; exit 1; }

trap 'fail "Command failed (exit $?) at line $LINENO. See logs above."' ERR

########################################
# Docker Compose wrapper (v2 & v1)
########################################
detect_compose() {
  if docker compose version >/dev/null 2>&1; then
    echo "docker compose"
  elif command -v docker-compose >/dev/null 2>&1; then
    echo "docker-compose"
  else
    fail "Docker Compose not found. Install Docker Desktop or docker-compose."
  fi
}
COMPOSE_BIN="$(detect_compose)"

compose() {
  # shellcheck disable=SC2086
  eval $COMPOSE_BIN -f "$COMPOSE_FILE" -p "$PROJECT_NAME" "$@"
}

########################################
# Preflight checks
########################################
preflight() {
  command -v docker >/dev/null 2>&1 || fail "Docker is required."
  [ -f "$COMPOSE_FILE" ] || fail "Compose file not found: $COMPOSE_FILE (set DEV_STACK=services or COMPOSE_FILE to override)"

  # Suggest Docker running if ps fails
  if ! docker info >/dev/null 2>&1; then
    fail "Docker daemon not reachable. Is Docker running?"
  fi

  # pnpm required for app tasks
  if [[ "${1:-}" == "apps" || "${1:-}" == "migrate" ]]; then
    command -v pnpm >/dev/null 2>&1 || fail "pnpm is required for app commands."
  fi
}

########################################
# Env handling
########################################
ensure_env() {
  if [ ! -f "$ENV_FILE" ]; then
    warn "Environment file not found at $ENV_FILE"
    if [ -f "$ENV_EXAMPLE_FILE" ]; then
      log "Creating .env from example..."
      cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
      warn "Please edit $ENV_FILE with your actual values."
    else
      warn "No .env.example found at $ENV_EXAMPLE_FILE. Creating empty .env..."
      : > "$ENV_FILE"
    fi
  fi
}

load_env_exported() {
  # Export so child processes see env (compose, pnpm etc.)
  set +u
  set -a
  [ -f "$ENV_FILE" ] && . "$ENV_FILE" || true
  set +a
  set -u
}

########################################
# Helpers
########################################
services_running() {
  compose ps | grep -q "Up"
}

wait_for_postgres() {
  local user="${POSTGRES_USER:-$DB_USER_DEFAULT}"
  local db="${POSTGRES_DB:-$DB_NAME_DEFAULT}"
  local timeout="$DB_WAIT_SECS"

  log "⏳ Waiting for database ($db) to be ready (timeout: ${timeout}s)..."
  local elapsed=0
  local step=2
  until compose exec -T postgres pg_isready -U "$user" -d "$db" >/dev/null 2>&1; do
    sleep "$step"
    elapsed=$((elapsed + step))
    if (( elapsed >= timeout )); then
      fail "Database failed to become ready within ${timeout}s"
    fi
  done
  log "✅ Database is ready!"
}

kill_by_port() {
  local port="$1"
  # Prefer lsof on macOS; fall back to fuser on Linux
  if command -v lsof >/dev/null 2>&1; then
    if lsof -tiTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then
      log "   Killing process on port $port..."
      lsof -tiTCP:"$port" -sTCP:LISTEN | xargs -r kill -9 2>/dev/null || true
    fi
  elif command -v fuser >/dev/null 2>&1; then
    if fuser -n tcp "$port" >/dev/null 2>&1; then
      log "   Killing process on port $port..."
      fuser -k -n tcp "$port" 2>/dev/null || true
    fi
  else
    warn "Neither lsof nor fuser found; skipping port cleanup for $port."
  fi
}

# Check if a port is free using Node (portable)
is_port_free() {
  local port="$1"
  if ! command -v node >/dev/null 2>&1; then
    return 0 # assume free if node unavailable
  fi
  node -e "const net=require('net');const p=parseInt(process.argv[1],10);const s=net.createServer();s.once('error',()=>process.exit(1));s.once('listening',()=>s.close(()=>process.exit(0)));s.listen({port:p,host:'127.0.0.1'});" "$port"
}

# Find a free port starting from base, up to +50
find_free_port() {
  local base="$1"
  local limit=$((base+50))
  local p="$base"
  while [ "$p" -le "$limit" ]; do
    if is_port_free "$p"; then
      echo "$p"
      return 0
    fi
    p=$((p+1))
  done
  echo "$base"
}

########################################
# Commands
########################################
start_services() {
  preflight "start"
  cd "$PROJECT_ROOT"
  ensure_env
  load_env_exported

  log "🚀 Starting development services..."
  log "🧩 Using compose: $COMPOSE_FILE"
  compose up -d postgres redis
  wait_for_postgres

  local next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
  local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
  # Ensure desired ports are free; kill any listeners
  kill_by_port "$next"
  kill_by_port "$bff"
  # If still busy, either auto-shift (if allowed) or fail
  if ! is_port_free "$next"; then
    if [ "${ALLOW_PORT_SHIFT:-0}" = "1" ]; then
      local next_free
      next_free="$(find_free_port "$next")"
      warn "Port $next in use; assigning NEXT_PORT=$next_free"
      export NEXT_PORT="$next_free"
      next="$next_free"
    else
      fail "Port $next is in use. Stop the process or run '$0 cleanup'. Set ALLOW_PORT_SHIFT=1 to auto-assign another port."
    fi
  fi
  if ! is_port_free "$bff"; then
    if [ "${ALLOW_PORT_SHIFT:-0}" = "1" ]; then
      local bff_free
      bff_free="$(find_free_port "$bff")"
      warn "Port $bff in use; assigning BFF_PORT=$bff_free"
      export BFF_PORT="$bff_free"
      bff="$bff_free"
    else
      fail "Port $bff is in use. Stop the process or run '$0 cleanup'. Set ALLOW_PORT_SHIFT=1 to auto-assign another port."
    fi
  fi
  log "✅ Development services are running!"
  log "🔗 Database: postgresql://${POSTGRES_USER:-$DB_USER_DEFAULT}:${POSTGRES_PASSWORD:-${POSTGRES_PASSWORD:-dev}}@localhost:5432/${POSTGRES_DB:-$DB_NAME_DEFAULT}"
  log "🔗 Redis: redis://localhost:6379"
  log "🔗 BFF API (expected): http://localhost:${bff}/api"
  log "🔗 Frontend (expected): http://localhost:${next}"
}

start_with_tools() {
  preflight "tools"
  cd "$PROJECT_ROOT"
  ensure_env
  load_env_exported

  log "🛠️ Starting development services with admin tools..."
  log "🧩 Using compose: $COMPOSE_FILE"
  # Explicitly start only services + admin tools to avoid building app containers in the full compose
  compose --profile tools up -d postgres redis adminer redis-commander mailhog
  wait_for_postgres
  log "🔗 Database Admin: http://localhost:8080"
  log "🔗 Redis Commander: http://localhost:8081"
}

stop_services() {
  preflight "stop"
  cd "$PROJECT_ROOT"
  log "⏹️ Stopping development services..."
  compose down --remove-orphans
  log "✅ Services stopped"
}

show_status() {
  preflight "status"
  cd "$PROJECT_ROOT"
  log "📊 Development Services Status:"
  log "🧩 Using compose: $COMPOSE_FILE"
  compose ps
}

show_logs() {
  preflight "logs"
  cd "$PROJECT_ROOT"
  # Pass-through any service names after "logs"
  # e.g. ./dev.sh logs postgres redis
  log "🧩 Using compose: $COMPOSE_FILE"
  compose logs -f --tail=100 "${@:2}"
}

cleanup_dev() {
  log "🧹 Cleaning up all development processes and ports..."

  # Pull ports from env if present; include common defaults
  local ports=()
  ports+=("${NEXT_PORT:-$NEXT_PORT_DEFAULT}")
  ports+=("${BFF_PORT:-$BFF_PORT_DEFAULT}")
  ports+=(5555) # Prisma Studio default

  for p in "${ports[@]}"; do
    kill_by_port "$p"
  done

  # Kill common dev processes by name
  pkill -f "next dev"              2>/dev/null && log "   Stopped Next.js dev server" || true
  pkill -f "nest start --watch"    2>/dev/null && log "   Stopped NestJS watch server" || true
  pkill -f "next-server"           2>/dev/null && log "   Stopped Next.js server process" || true
  pkill -f "pnpm.*--parallel.*dev" 2>/dev/null && log "   Stopped parallel dev processes" || true
  pkill -f "prisma studio"         2>/dev/null && log "   Stopped Prisma Studio" || true

  sleep 1
  log "✅ Development cleanup completed"
}

start_apps() {
  preflight "apps"
  cd "$PROJECT_ROOT"

  cleanup_dev

  if ! services_running; then
    start_services
  fi

  load_env_exported

  # Build shared package first
  log "🔨 Building shared package..."
  pnpm --filter @customer-portal/domain build
  pnpm --filter @customer-portal/validation build

  # Build BFF before watch (ensures dist exists). Use Nest build for correct emit.
  log "🔨 Building BFF for initial setup (ts emit)..."
  (
    cd "$PROJECT_ROOT/apps/bff" \
      && pnpm clean \
      && rm -f tsconfig.build.tsbuildinfo \
      && pnpm build || pnpm exec tsc -b --force tsconfig.build.json
  )
  if [ ! -d "$PROJECT_ROOT/apps/bff/dist" ]; then
    warn "BFF dist not found after build; forcing TypeScript emit..."
    (cd "$PROJECT_ROOT/apps/bff" && pnpm exec tsc -b --force tsconfig.build.json)
  fi
  if [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.js" ] && [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.cjs" ]; then
    warn "BFF main output not found; will rely on watch to produce it."
  fi

  local next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
  local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
  log "🎯 Starting development applications..."
  log "🔗 BFF API: http://localhost:${bff}/api"
  log "🔗 Frontend: http://localhost:${next}"
  log "🔗 Database: postgresql://${POSTGRES_USER:-$DB_USER_DEFAULT}:${POSTGRES_PASSWORD:-${POSTGRES_PASSWORD:-dev}}@localhost:5432/${POSTGRES_DB:-$DB_NAME_DEFAULT}"
  log "🔗 Redis: redis://localhost:6379"
  log "📚 API Docs: http://localhost:${bff}/api/docs"
  log "Starting apps with hot-reload..."
  # Prisma Studio can be started manually with: pnpm db:studio

  # Run portal + bff in parallel with hot reload
  pnpm --parallel \
    --filter @customer-portal/domain \
    --filter @customer-portal/validation \
    --filter @customer-portal/portal \
    --filter @customer-portal/bff run dev
}

reset_env() {
  preflight "reset"
  cd "$PROJECT_ROOT"
  log "🔄 Resetting development environment..."
  compose down -v --remove-orphans
  docker system prune -f
  log "✅ Development environment reset"
}

migrate_db() {
  preflight "migrate"
  cd "$PROJECT_ROOT"

  if ! compose ps postgres | grep -q "Up"; then
    fail "Database service not running. Run '$0 start' or '$0 apps' first."
  fi

  load_env_exported
  log "🗄️ Running database migrations..."
  pnpm db:migrate
  log "✅ Database migrations completed"
}

usage() {
  cat <<EOF
🔧 Development Environment Manager

Usage: $0 {command}

Commands:
  start     - Start dev services (PostgreSQL + Redis)
  stop      - Stop all dev services
  restart   - Restart dev services
  status    - Show service status
  logs      - Tail logs (optionally: specify services, e.g. '$0 logs postgres redis')
  tools     - Start services with admin tools
  apps      - Start services + run dev apps (auto-cleanup)
  cleanup   - Clean up local dev processes and ports
  migrate   - Run database migrations
  reset     - Reset development environment
  help      - Show this help

Environment overrides:
  PROJECT_ROOT, COMPOSE_FILE, ENV_FILE, ENV_EXAMPLE_FILE, PROJECT_NAME
  DEV_STACK (full|services|auto)  # default: full
  POSTGRES_USER, POSTGRES_DB, POSTGRES_PASSWORD, DB_WAIT_SECS
  NEXT_PORT, BFF_PORT
EOF
}

########################################
# Main
########################################
cmd="${1:-help}"
case "$cmd" in
  start)    start_services ;;
  stop)     stop_services ;;
  restart)  stop_services; start_services ;;
  status)   show_status ;;
  logs)     show_logs "$@" ;;
  tools)    start_with_tools ;;
  apps)     start_apps ;;
  cleanup)  cleanup_dev ;;
  migrate)  migrate_db ;;
  reset)    reset_env ;;
  help|*)   usage; exit 0 ;;
esac
