#!/usr/bin/env bash
# unoverse start + Docker file sharing check

check_docker_file_sharing() {
  # Detect macOS restricted directories that Docker cannot mount by default
  local restricted=false
  local location=""

  case "$ROOT" in
    "$HOME/Desktop"*)
      restricted=true
      location="Desktop"
      ;;
    "$HOME/Downloads"*)
      restricted=true
      location="Downloads"
      ;;
  esac

  if [ "$restricted" = true ]; then
    echo ""
    warn "Project is in a restricted folder: ${BOLD}$ROOT${NC}"
    echo ""
    echo -e "  Docker cannot mount volumes from ${BOLD}$location${NC} without extra permissions."
    echo -e "  This will cause services to stay in ${BOLD}Created${NC} state and not start."
    echo ""
    echo -e "  ${BOLD}Fix (choose one):${NC}"
    echo -e "    1. Move project to ${BOLD}~/dev/${NC} or ${BOLD}~/projects/${NC} and re-run ${BOLD}unoverse start${NC}"
    echo -e "    2. Docker Desktop → Settings → Resources → File Sharing → add ${BOLD}$HOME/$location${NC}"
    echo ""
    return 1
  fi
  return 0
}

# Pull whatever images are missing, VISIBLY. docker's own per-layer progress bars are the
# loading indication — a developer staring at a silent screen while gigabytes download
# assumes the tool is hung. Exists because init used to call cmd_pull (deleted with
# update.sh), so nothing pulled and nothing printed, and start buried compose's pull
# inside a log file.
pull_missing_images() {
  local img missing=""
  for img in $(docker compose -f "$ROOT/docker-compose.yml" config --images 2>/dev/null | sort -u); do
    docker image inspect "$img" >/dev/null 2>&1 || missing="$missing $img"
  done
  [ -n "$missing" ] || return 0
  echo ""
  info "Pulling platform images (first run — a few minutes, progress below)..."
  echo ""
  docker compose -f "$ROOT/docker-compose.yml" --env-file "$ROOT/.env" pull     || { fail "Image pull failed. Check the registry token (re-run unoverse create to change it)"; exit 1; }
  echo ""
}

cmd_start() {
  # NO FIREWALL CHANGE HERE. Starting a dev server briefly did this automatically, so a
  # laptop that had moved could always reach its database. It also meant every `start` on
  # café or hotel Wi-Fi silently added THAT network's shared egress address to a production
  # cluster's trusted sources, where it stayed until the next run. An ACL change is a
  # deliberate act: `unoverse db-allow`, typed, when you move network.

  # Login to registry if DOCR_TOKEN is set
  local docr_token
  docr_token=$(grep "^DOCR_TOKEN=" "$ROOT/.env" 2>/dev/null | cut -d'=' -f2-)
  local docr_user
  docr_user=$(grep "^DOCR_USER=" "$ROOT/.env" 2>/dev/null | cut -d'=' -f2-)
  if [ -n "$docr_token" ]; then
    echo "$docr_token" | docker login registry.digitalocean.com -u "${docr_user:-$docr_token}" --password-stdin >/dev/null 2>&1 || true
  fi

  # `start --pull` refreshes the images first. This is where refreshing a LOCAL universe
  # lives now: `update` is the CLI's word and means only the CLI, wherever you type it.
  # A server is refreshed by `unoverse deploy` from your machine, not on the box.
  pull_missing_images

  if [ "${1:-}" = "--pull" ]; then
    info "Pulling the latest images..."
    docker compose -f "$ROOT/docker-compose.yml" pull || { fail "Image pull failed"; exit 1; }
    echo ""
  fi

  check_docker_file_sharing || exit 1
  # NO SETUP WIZARD. `unoverse create` writes the .env; there is nothing for start to
  # interview about, and an interactive wizard on the way to "start the platform" was a
  # command inside a command. Say what is missing and name the one that fixes it.
  if [ ! -f "$ROOT/.env" ]; then
    echo ""
    fail "No .env here. This folder is not a universe"
    info "Run ${BOLD}unoverse create${NC} in an empty folder to make one"
    echo ""
    exit 1
  fi
  banner "Starting Unoverse Platform"
  timer_start

  # Ensure node_modules exists (required for package deps inside containers)
  if [ ! -d "$ROOT/node_modules" ]; then
    printf "  ${DIM}●${NC} Installing dependencies..."
    (cd "$ROOT" && npm install --silent >/dev/null 2>&1) || true
    printf "\r\033[2K"
    ok "Dependencies installed"
  fi

  # Auto-start Redis if needed and not running
  local redis_host
  redis_host=$(grep "^REDIS_HOST=" "$ROOT/.env" 2>/dev/null | cut -d'=' -f2-)
  if [ "$redis_host" = "host.docker.internal" ] || [ "$redis_host" = "localhost" ]; then
    if ! docker ps --format "{{.Names}}" 2>/dev/null | grep -qi redis; then
      echo -e "  ${DIM}Starting Redis...${NC}"
      docker start gravity-redis 2>/dev/null || \
        docker run -d --name gravity-redis -p 6379:6379 redis:alpine &>/dev/null || true
    fi
  fi

  # Start services silently, show our own progress
  local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
  local compose_log
  compose_log=$(mktemp)

  docker compose -f "$ROOT/docker-compose.yml" --env-file "$ROOT/.env" up -d --remove-orphans >"$compose_log" 2>&1 &
  local up_pid=$!
  local i=0
  while kill -0 "$up_pid" 2>/dev/null || false; do
    local c="${spin:i%${#spin}:1}"
    i=$((i + 1))
    printf "\r  ${CYAN}%s${NC} ${DIM}Starting services...${NC} " "$c"
    sleep 0.1
  done
  local up_exit=0
  wait "$up_pid" 2>/dev/null || up_exit=$?
  if [ $up_exit -ne 0 ]; then
    printf "\r  ${RED}✗${NC} Failed to start services\n"
    echo ""
    echo -e "  ${DIM}──── docker compose output ────${NC}"
    sed 's/^/  /' "$compose_log"
    echo -e "  ${DIM}───────────────────────────────${NC}"
    echo ""
    echo -e "  ${YELLOW}Common fixes:${NC}"
    echo -e "    • Not logged into registry?  check DOCR_TOKEN in ${GREEN}.env${NC}"
    echo -e "    • Missing .env?              ${GREEN}cp .env.example .env${NC} and fill in values"
    echo -e "    • Images not pulled?         ${GREEN}docker compose pull${NC}"
    echo ""
    rm -f "$compose_log"
    exit 1
  else
    printf "\r  ${GREEN}✓${NC} ${DIM}Starting services${NC}  \n"
  fi
  rm -f "$compose_log"

  # Wait for services to be healthy
  local attempts=0
  local running=0
  local total=0
  i=0
  # A RESTARTING CONTAINER IS NOT A RUNNING ONE. `--filter status=running` counts a container
  # that is crash-looping, because between crashes it genuinely is running — so `start`
  # announced "All 5 services running" while unoverse and memory were dying on boot every few
  # seconds, and the developer went looking at their browser instead of the logs.
  #
  # Counting restarts as failures also means the loop keeps waiting instead of breaking early
  # on a lie, which gives a slow-but-healthy boot the time it needs.
  local restarting=0
  while [ $attempts -lt 15 ]; do
    running=$(docker compose -f "$ROOT/docker-compose.yml" ps --format "{{.Name}}" --filter "status=running" 2>/dev/null | wc -l | tr -d ' ')
    restarting=$(docker compose -f "$ROOT/docker-compose.yml" ps -a --format "{{.Status}}" 2>/dev/null | grep -ci "restarting" | tr -d ' ')
    total=$(docker compose -f "$ROOT/docker-compose.yml" ps --format "{{.Name}}" 2>/dev/null | wc -l | tr -d ' ')
    if [ "$running" -eq "$total" ] && [ "$total" -gt 0 ] && [ "$restarting" -eq 0 ]; then
      break
    fi
    local c="${spin:i%${#spin}:1}"
    i=$((i + 1))
    printf "\r  ${CYAN}%s${NC} ${DIM}Waiting for health checks...${NC} ${DIM}(%d/%d)${NC} " "$c" "$running" "$total"
    sleep 2
    attempts=$((attempts + 1))
  done

  echo ""
  # NAME WHAT IS CRASHING, AND WHY. A restart loop is the one failure whose cause is always
  # in the logs and never on screen, so print the last line of each one rather than leaving
  # the developer to find `docker compose logs` themselves.
  if [ "${restarting:-0}" -gt 0 ]; then
    printf "\r  ${RED}✗${NC} $restarting service(s) crashing on boot       \n"
    echo ""
    # ASK COMPOSE FOR THE SERVICE NAME. `${svc##*-}` on a container called
    # unoversedevtest-unoverse-1 yields "1", so the log lookup asked for a service that does
    # not exist and printed nothing — the crash was named and its cause silently dropped.
    # `ps --format {{.Service}}` gives compose's own name for it.
    docker compose -f "$ROOT/docker-compose.yml" ps -a --format "{{.Service}}\t{{.Status}}" 2>/dev/null \
      | grep -i restarting | awk -F'\t' '{print $1}' | while read -r svc; do
        reason=$(docker compose -f "$ROOT/docker-compose.yml" logs --tail=60 "$svc" 2>/dev/null \
          | grep -iE "error" | grep -viE "at [A-Za-z_]+ \(|node:internal" | tail -1 \
          | sed -E 's/^[^|]*\| *//')
        echo -e "  ${RED}•${NC} ${BOLD}$svc${NC}"
        [ -n "$reason" ] && echo -e "    ${DIM}${reason}${NC}"
      done
    echo ""
    info "Full output: ${BOLD}unoverse logs <service>${NC}"
    echo ""
    return 1
  fi
  if [ "$running" -eq "$total" ] && [ "$total" -gt 0 ]; then
    printf "\r  ${GREEN}✓${NC} ${DIM}Health checks passed${NC}       \n"
    echo ""
    echo -e "  ${GREEN}${BOLD}All $running services running${NC} ${DIM}($(timer_elapsed))${NC}"
    echo ""
    # Auto-compile component (design) nodes from the design tree — same step prod's deploy runs.
    # Generates + tsc-builds nodes/components, then reloads unoverse so they load.
    # Non-fatal: build_component_nodes returns (doesn't exit) if it can't run, so start still finishes.
    build_component_nodes
    # Migrations, now that the unoverse service is up (they run from inside its
    # container). This is what makes start the resume point after any interrupted
    # setup: pull, boot, migrate, no third command to know. Subshell because cmd_db_setup exits
    # rather than returns on failure.
    if ! (cmd_db_setup) >/dev/null 2>&1; then
      warn "Migrations did not apply. Run ${BOLD}unoverse check${NC} to see why"
    fi
    print_access_urls
    echo ""
    info "Run ${BOLD}unoverse open${NC}    to open Canvas in your browser"
  elif [ "$total" -eq 0 ]; then
    printf "\r  ${RED}✗${NC} No services found\n"
    echo ""
    echo -e "  ${YELLOW}No containers were created. Possible causes:${NC}"
    echo -e "    • Images not pulled yet?     ${GREEN}docker compose pull${NC}"
    echo -e "    • Not logged into registry?  check DOCR_TOKEN in ${GREEN}.env${NC}"
    echo ""
  else
    printf "\r  ${YELLOW}!${NC} ${DIM}$running/$total services running${NC}\n"
    echo ""
    echo -e "  ${YELLOW}${BOLD}$running/$total services up${NC} ${DIM}($(timer_elapsed))${NC}"
    echo ""
    # Show which services are stuck and why
    local created_svcs
    created_svcs=$(docker compose -f "$ROOT/docker-compose.yml" ps -a --format '{{.Name}}\t{{.Status}}' 2>/dev/null | grep -i 'created' | awk -F'\t' '{print $1}')
    if [ -n "$created_svcs" ]; then
      echo -e "  ${RED}Services stuck in 'Created' state (never started):${NC}"
      echo "$created_svcs" | while read -r svc_name; do
        echo -e "    ${DIM}• $svc_name${NC}"
      done
      echo ""
      echo -e "  ${YELLOW}Common causes:${NC}"
      echo -e "    • Docker cannot mount project directory (restricted folder)"
      echo -e "    • Try: ${GREEN}unoverse check${NC} to diagnose"
      echo -e "    • Try: ${GREEN}docker compose down && unoverse start${NC} to recreate"
    else
      info "Run ${BOLD}unoverse status${NC} to see which services failed"
      info "Run ${BOLD}unoverse logs <service>${NC} to check logs"
    fi
  fi
  echo ""
}

# ── unoverse build + the component-node package build (merged from build.sh 2026-07-28) ──

cmd_build() {
  local package="$1"
  banner "Build & Restart"

  if [ -z "$package" ]; then
    echo "  Building all packages..."
    (cd "$ROOT" && npm run build)
    ok "All packages built"
  else
    echo "  Building $package..."
    (cd "$ROOT" && npm run build -w "$package")
    ok "$package built"
  fi

  echo "  Restarting services..."
  docker compose -f "$ROOT/docker-compose.yml" restart unoverse
  ok "Services restarted. Changes are live"
  echo ""
}

# Internal helper (called by start/dev — not a user-facing command): builds the
# universal component-node package LOCALLY (never in the container — deploys and
# dev alike ship/serve locally built dists) and restarts unoverse. Design
# DEFINITION changes never need this — they synthesize from design/ at boot; a plain
# restart suffices. This exists for the PACKAGE-source case.
build_component_nodes() {
  # MONOREPO ONLY. packages/marketplace exists in the platform checkout and nowhere else, so
  # in a universe this printed a heading, failed a `cd` into a directory that has never been
  # there, and told the developer to "re-sync/update the starter" — advice that could not
  # help, for a step that should not have run. A universe gets its components as rows, from
  # the marketplace or a Studio publish; there is nothing local to build.
  [ -d "$ROOT/packages/marketplace" ] || return 0

  banner "Component Nodes (definition-backed)"

  echo "  Building the marketplace package locally (bundle-defs + tsc → dist)..."
  if (cd "$ROOT/packages/marketplace" && node scripts/bundle-defs.mjs && npx tsc 2>/dev/null); then
    ok "Marketplace package built → packages/marketplace/dist"
  elif [ -d "$ROOT/packages/marketplace/dist" ]; then
    # Starters can't build it (no workspace plugin-base) — the dist SHIPS with the
    # platform sync/deploy; use the shipped artifact.
    ok "Using shipped component-node dist (local build unavailable. Expected on starters)"
  else
    fail "no component-node dist and local build failed: re-sync/update the starter (the dist ships prebuilt)"
    return
  fi

  echo "  Restarting unoverse (definitions re-synthesize from design/components)..."
  docker compose -f "$ROOT/docker-compose.yml" restart unoverse 2>/dev/null || true
  ok "Restarted. Component changes are live"
  echo ""
}
