#!/usr/bin/env bash
# The .env a local universe needs, written once by `unoverse create`.

# install_to_path REMOVED 2026-07-31. The global command is the npm package `unoverse`
# (`npm i -g unoverse`); symlinking a per-project bash script into /usr/local/bin was the
# second CLI we set out to delete.

# ── The image download, as an experience ─────────────────────────────────────
#
# The pull starts the moment the token is accepted, so the minute spent answering
# questions is also the minute the images arrive. NOTHING writes to the terminal
# asynchronously — a progress bar racing a readline prompt corrupts what the developer
# is typing. The background half writes one line of STATE to a temp file; the wizard
# prints a status line synchronously between question groups, where output composes
# with input; the end of setup attaches with docker's own bars for whatever remains.
PULL_STATE=""
start_background_pull() {
  PULL_STATE=$(mktemp)
  (
    imgs=$(docker compose -f "$ROOT/docker-compose.yml" config --images 2>/dev/null | sort -u)
    total=$(printf '%s\n' "$imgs" | grep -c .)
    n=0
    for img in $imgs; do
      short="${img##*/}"; short="${short%%:*}"
      if docker image inspect "$img" >/dev/null 2>&1; then
        n=$((n+1)); echo "$n $total ready $short" > "$PULL_STATE"; continue
      fi
      echo "$n $total pulling $short" > "$PULL_STATE"
      docker pull "$img" >/dev/null 2>&1
      n=$((n+1)); echo "$n $total pulled $short" > "$PULL_STATE"
    done
    echo "$total $total done -" > "$PULL_STATE"
  ) &
}

# One dim line, printed only when the wizard is printing anyway. Never a redraw.
pull_status_line() {
  [ -n "$PULL_STATE" ] && [ -s "$PULL_STATE" ] || return 0
  local n total verb what
  read -r n total verb what < "$PULL_STATE" 2>/dev/null || return 0
  if [ "$verb" = "done" ]; then
    echo -e "  ${GREEN}⬇${NC} ${DIM}platform images: all $total downloaded${NC}"
  elif [ "$verb" = "pulling" ]; then
    echo -e "  ${CYAN}⬇${NC} ${DIM}platform images: $n of $total · pulling $what…${NC}"
  else
    echo -e "  ${CYAN}⬇${NC} ${DIM}platform images: $n of $total${NC}"
  fi
  echo ""
}

cmd_setup() {
  echo ""
  echo -e "  ${BOLD}${CYAN}⬡ Unoverse Setup${NC}"
  echo -e "  ${DIM}─────────────────────────────────${NC}"
  echo ""

  # (The studio/platform mode interview was removed 2026-07-28 — Studio is a
  # separate app, and this CLI only sets up the platform.)
  timer_start

  # DOCKER IS NEEDED TO START, NOT TO CONFIGURE. This used to exit here, which threw
  # away a scaffold and a validated token because Docker Desktop happened to be closed.
  # Writing .env needs nothing running, so record the state and carry on; the steps that
  # genuinely need a daemon skip themselves, and `start` is where it becomes an error.
  DOCKER_OK=0
  if ! command -v docker &>/dev/null; then
    warn "Docker is not installed. Configuration will finish; install it before ${BOLD}unoverse start${NC}"
    info "Install: https://docs.docker.com/get-docker/"
  elif ! docker info &>/dev/null; then
    warn "Docker is not running. Configuration will finish; start Docker Desktop before ${BOLD}unoverse start${NC}"
  else
    DOCKER_OK=1
    ok "Docker is installed and running"
  fi

  # Apple Silicon check
  if [ "$(uname -m)" = "arm64" ]; then
    ok "Apple Silicon detected: multi-arch images will run natively"
    echo ""
  fi

  # RE-RUNNING EDITS. There is no "overwrite? [y/N]" gate any more: every existing value
  # becomes its question's default, so Enter keeps a setting and typing replaces it.
  # Walking through changes only what you change — which makes this the way to change
  # one env var, not a destructive restart.
  _env_cur() { grep "^$1=" "$ROOT/.env" 2>/dev/null | head -1 | cut -d= -f2-; }

  echo ""
  echo -e "  ${BOLD}Configure your environment:${NC}"
  if [ -f "$ROOT/.env" ]; then
    echo -e "  ${DIM}(Existing .env found. Enter keeps each current value)${NC}"
  else
    echo -e "  ${DIM}(Press Enter to use defaults)${NC}"
  fi
  echo ""

  # DOCR Token. `unoverse create` has already asked for this and VALIDATED it against
  # the registry, so it hands it over rather than making you type the same credential
  # twice minutes apart. Typed here only when init is run on its own.
  if [ -n "${UNOVERSE_DOCR_TOKEN:-}" ]; then
    DOCR_TOKEN="$UNOVERSE_DOCR_TOKEN"
    DOCR_USER="${UNOVERSE_DOCR_USER:-$UNOVERSE_DOCR_TOKEN}"
    ok "Registry token carried over from create"
  else
    local cur_token
    cur_token=$(_env_cur DOCR_TOKEN)
    DOCR_USER=$(_env_cur DOCR_USER)
    while true; do
      if [ -n "$cur_token" ]; then
        read -p "  DOCR Token [keep current]: " DOCR_TOKEN || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
        DOCR_TOKEN="${DOCR_TOKEN:-$cur_token}"
      else
        read -p "  DOCR Token (from your Unoverse admin): " DOCR_TOKEN || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
      fi
      # Accept the credential in any shape, AS A PAIR. The downloaded Docker
      # credentials wrap base64("email:token") — the username is the email, and
      # logging in token-as-username gets "unauthorized" for exactly that shape.
      DOCR_USER=""
      if [[ "$DOCR_TOKEN" != dop_v1_* ]]; then
        local decoded
        decoded=$(printf '%s' "$DOCR_TOKEN" | sed -E 's/.*"auth"[^"]*"([A-Za-z0-9+\/=]+)".*/\1/' | base64 -d 2>/dev/null | tr -d '\0')
        [ -n "$decoded" ] || decoded=$(printf '%s' "$DOCR_TOKEN" | base64 -d 2>/dev/null | tr -d '\0')
        case "$decoded" in
          *:dop_v1_*) DOCR_USER="${decoded%%:*}"; DOCR_TOKEN="dop_v1_${decoded##*dop_v1_}";;
        esac
      fi
      DOCR_USER="${DOCR_USER:-$DOCR_TOKEN}"
      if [[ "$DOCR_TOKEN" == dop_v1_* ]]; then
        break
      fi
      fail "That does not look like a registry credential. Paste it exactly as it was sent"
    done
  fi

  # THE DOWNLOAD STARTS NOW (see the block at the top of this file).
  if [ "$DOCKER_OK" = "1" ]; then
    local login_err
    if login_err=$(echo "$DOCR_TOKEN" | docker login "$DOCR_REGISTRY" -u "${DOCR_USER:-$DOCR_TOKEN}" --password-stdin 2>&1 >/dev/null); then
      start_background_pull
      echo ""
      echo -e "  ${CYAN}⬇${NC} ${DIM}Platform images are downloading in the background while you configure${NC}"
      echo ""
    else
      # The REASON, not just the fact: a swallowed docker error left "login failed"
      # undiagnosable when create had just validated the same credential.
      warn "Registry login failed. Images will not pull until it is fixed"
      echo "$login_err" | grep -vi "warning" | head -3 | sed 's/^/      /'
    fi
  fi

  # A DEFAULT, because "from your admin" is meaningless when you are the admin. The
  # platform ships no database (docker-compose has no postgres), so this points at one
  # you run. Enter takes the conventional local one.
  DB_DEFAULT=$(_env_cur DATABASE_URL)
  DB_DEFAULT="${DB_DEFAULT:-postgres://postgres:postgres@localhost:5432/unoverse}"
  while true; do
    read -p "  DATABASE_URL [${DB_DEFAULT}]: " DATABASE_URL || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
    DATABASE_URL="${DATABASE_URL:-$DB_DEFAULT}"
    if [ -n "$DATABASE_URL" ] && [[ "$DATABASE_URL" != *"user:password"* ]]; then
      break
    fi
    fail "DATABASE_URL is required. Get it from your Unoverse admin"
  done

  # Auto-add SSL params if missing
  if [[ "$DATABASE_URL" != *"sslmode="* ]] && [[ "$DATABASE_URL" != *"ssl="* ]]; then
    local sep="?"
    [[ "$DATABASE_URL" == *"?"* ]] && sep="&"
    if [[ "$DATABASE_URL" == *"localhost"* ]] || \
       [[ "$DATABASE_URL" == *"127.0.0.1"* ]] || \
       [[ "$DATABASE_URL" == *"host.docker.internal"* ]]; then
      DATABASE_URL="${DATABASE_URL}${sep}sslmode=disable"
      ok "Local database detected. Added sslmode=disable"
    else
      DATABASE_URL="${DATABASE_URL}${sep}sslmode=require"
      ok "Managed database detected. Added sslmode=require"
    fi
  fi

  pull_status_line
  # Redis, current values as defaults
  local rd
  rd=$(_env_cur REDIS_HOST); rd="${rd:-host.docker.internal}"
  read -p "  REDIS_HOST [$rd]: " REDIS_HOST
  REDIS_HOST="${REDIS_HOST:-$rd}"

  rd=$(_env_cur REDIS_PORT); rd="${rd:-6379}"
  read -p "  REDIS_PORT [$rd]: " REDIS_PORT
  REDIS_PORT="${REDIS_PORT:-$rd}"

  rd=$(_env_cur REDIS_PASSWORD)
  if [ -n "$rd" ]; then
    read -p "  REDIS_PASSWORD [keep current]: " REDIS_PASSWORD
    REDIS_PASSWORD="${REDIS_PASSWORD:-$rd}"
  else
    read -p "  REDIS_PASSWORD (blank for none): " REDIS_PASSWORD
  fi

  rd=$(_env_cur REDIS_TLS); rd="${rd:-false}"
  read -p "  REDIS_TLS [$rd]: " REDIS_TLS
  REDIS_TLS="${REDIS_TLS:-$rd}"

  # BRING YOUR OWN, or take the one the deploy makes. The question used to be "do you have
  # an identity provider (Auth0/OIDC) to connect?", which reads as a requirement a
  # developer must go and satisfy. It is not one: AWS provisions a Cognito user pool and
  # writes AUTH_ISSUER, AUTH_CLIENT_ID and AUTH_AUDIENCE into .env.production itself
  # (infra/aws/outputs.tf). Only DigitalOcean needs an issuer from you, and only at deploy
  # time (deploy.sh _ensure_ground_config), where it asks.
  #
  # So the honest question is whether you are bringing one, and the default is no.
  #
  # Answering either way is a LOCAL switch only. INFRASTRUCTURE.md is explicit that there
  # is no auth-off deployment, and the platform enforces that rather than trusting this
  # wizard: authConfig.ts refuses to start with auth off when NODE_ENV=production.
  pull_status_line
  local cur_auth idp_prompt
  cur_auth=$(_env_cur AUTH_ENABLED)
  idp_prompt="[y/N]"
  [ "$cur_auth" = "true" ] && idp_prompt="[Y/n]"
  echo ""
  info "Sign-in: deploying provides one. ${DIM}AWS creates a Cognito user pool; DigitalOcean asks for an issuer then${NC}"
  read -r -p "  Are you bringing your own (Auth0/OIDC)? $idp_prompt " HAS_IDP
  echo ""
  if [ -z "$HAS_IDP" ] && [ "$cur_auth" = "true" ]; then HAS_IDP=y; fi

  if [[ "$HAS_IDP" =~ ^[Yy]$ ]]; then
    AUTH_ENABLED=true
    local cur_iss cur_cid cur_aud
    cur_iss=$(_env_cur AUTH_ISSUER)
    cur_cid=$(_env_cur AUTH_CLIENT_ID)
    cur_aud=$(_env_cur AUTH_AUDIENCE)

    while true; do
      if [ -n "$cur_iss" ]; then
        read -p "  AUTH_ISSUER [$cur_iss]: " AUTH_ISSUER || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
        AUTH_ISSUER="${AUTH_ISSUER:-$cur_iss}"
      else
        read -p "  AUTH_ISSUER (e.g. https://your-tenant.auth0.com): " AUTH_ISSUER || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
      fi
      if [ -n "$AUTH_ISSUER" ] && [[ "$AUTH_ISSUER" == https://* ]]; then
        break
      fi
      fail "AUTH_ISSUER must be an https:// URL from your identity provider"
    done

    while true; do
      if [ -n "$cur_cid" ]; then
        read -p "  AUTH_CLIENT_ID [$cur_cid]: " AUTH_CLIENT_ID || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
        AUTH_CLIENT_ID="${AUTH_CLIENT_ID:-$cur_cid}"
      else
        read -p "  AUTH_CLIENT_ID: " AUTH_CLIENT_ID || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
      fi
      if [ -n "$AUTH_CLIENT_ID" ] && [[ "$AUTH_CLIENT_ID" != *"your-"* ]]; then
        break
      fi
      fail "AUTH_CLIENT_ID is required"
    done

    cur_aud="${cur_aud:-gravity-api}"
    read -p "  AUTH_AUDIENCE [$cur_aud]: " AUTH_AUDIENCE
    AUTH_AUDIENCE="${AUTH_AUDIENCE:-$cur_aud}"
    ok "Your provider will be used, here and when you deploy"
  else
    AUTH_ENABLED=false
    AUTH_ISSUER=""
    AUTH_CLIENT_ID=""
    AUTH_AUDIENCE="gravity-api"
    info "No sign-in locally. The deploy makes one when you ship this universe"
  fi


  # NOT ASKED. 4105 is the port docker-compose publishes, so locally this is a fact the
  # CLI already knows, and confirming it is not a question. A DEPLOYED universe gets its
  # own API_URL rendered into .env.production by Terraform, which never comes through here.
  API_URL="http://localhost:4105"

  # DERIVED FROM THE FOLDER, not hardcoded. Every key the memory server writes is
  # prefixed with this, so two universes sharing one Redis and the same namespace mix
  # their streams and caches. It used to be written as a literal `gravity` for every
  # local universe, while Terraform rendered `universe` for deployed ones: same Redis,
  # same prefix, silently interleaved.
  REDIS_NAMESPACE=$(basename "$ROOT" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/-*$//')
  REDIS_NAMESPACE="${REDIS_NAMESPACE:-universe}"
  ok "Redis namespace: ${BOLD}${REDIS_NAMESPACE}${NC}"

  # OpenAI (for Memory Server)
  pull_status_line
  local cur_oai
  cur_oai=$(_env_cur OPENAI_API_KEY)
  # NOT just the memory server: compose hands this to the unoverse service itself —
  # agents, embeddings and memory all run on it. A universe boots without it, but its AI
  # does not, so skipping gets a warning rather than silence.
  if [ -n "$cur_oai" ]; then
    read -p "  OPENAI_API_KEY [keep current]: " OPENAI_API_KEY
    OPENAI_API_KEY="${OPENAI_API_KEY:-$cur_oai}"
  else
    read -p "  OPENAI_API_KEY (powers the platform's AI): " OPENAI_API_KEY
    [ -z "$OPENAI_API_KEY" ] && warn "No OpenAI key: the universe starts, but agents, embeddings and memory will not work until one is in .env"
  fi

  # Node vendor keys, OPTIONAL: only the matching nodes need them, nothing platform-level
  # does. Asked so compose stops warning about them and so they land in .env with names.
  local cur_hb
  cur_hb=$(_env_cur HYPERBROWSER_API_KEY)
  if [ -n "$cur_hb" ]; then
    read -p "  HYPERBROWSER_API_KEY [keep current]: " HYPERBROWSER_API_KEY
    HYPERBROWSER_API_KEY="${HYPERBROWSER_API_KEY:-$cur_hb}"
  else
    read -p "  HYPERBROWSER_API_KEY (browser nodes, blank to skip): " HYPERBROWSER_API_KEY
  fi


  # A silent fact, not a question: the encryption key is GENERATED (a human never types
  # one) and kept verbatim on re-runs — a new key orphans every stored credential.
  CREDENTIAL_ENCRYPTION_KEY=$(_env_cur CREDENTIAL_ENCRYPTION_KEY)
  if [ -z "$CREDENTIAL_ENCRYPTION_KEY" ]; then
    CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -hex 32 2>/dev/null || head -c32 /dev/urandom | xxd -p -c64)
    ok "Credential encryption key generated"
  fi
  # Write .env
  cat > "$ROOT/.env" << ENVEOF
# Written by unoverse create
# LOCAL DEVELOPMENT, and it goes WITH AUTH_ENABLED below. Unset means production, where
# auth-off is refused (authConfig.ts:63) and docker-compose.yml defaults NODE_ENV to
# production — so omitting this line left a local universe that could not boot. A deployed
# universe sets neither: Terraform renders .env.production with auth on.
NODE_ENV=development
DOCR_TOKEN=${DOCR_TOKEN}
DOCR_USER=${DOCR_USER:-${DOCR_TOKEN}}
DATABASE_URL=${DATABASE_URL}
REDIS_HOST=${REDIS_HOST}
REDIS_PORT=${REDIS_PORT}
REDIS_PASSWORD=${REDIS_PASSWORD}
REDIS_TLS=${REDIS_TLS}
REDIS_NAMESPACE=${REDIS_NAMESPACE}
AUTH_ENABLED=${AUTH_ENABLED}
AUTH_ISSUER=${AUTH_ISSUER}
AUTH_CLIENT_ID=${AUTH_CLIENT_ID}
AUTH_AUDIENCE=${AUTH_AUDIENCE}
API_URL=${API_URL}
OPENAI_API_KEY=${OPENAI_API_KEY}
# Generated at setup: encrypts credentials stored by nodes. Losing it orphans them —
# back it up with the database. Kept on re-runs.
CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY}
HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY}
# Marketplace catalogue to install from (docs/architecture/authoring/MARKETPLACE.md).
# Empty = local items only. No default: a URL is never hardcoded.
UNOVERSE_MARKETPLACE_URL=
DOMAIN=
ENVEOF

  ok ".env created"

  # Attach to the background pull started when the token was accepted: layers already
  # down show as complete, the rest stream docker's progress bars. Without a daemon the
  # token is in .env, and `unoverse start` pulls on its first run.
  if [ "$DOCKER_OK" = "1" ]; then
    pull_missing_images
    [ -n "$PULL_STATE" ] && rm -f "$PULL_STATE"
  else
    echo ""
    info "Skipped the image download. ${BOLD}unoverse start${NC} does it once Docker is up"
  fi

  # Install to PATH
  
  # Done
  echo ""
  echo -e "  ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo -e "  ${GREEN}${BOLD}  ✓ Setup Complete!${NC} ${DIM}($(timer_elapsed))${NC}"
  echo -e "  ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  # MIGRATIONS RUN HERE, so `db-setup` is not a command anyone has to know about. Deploy
  # already ran them on the server (playbooks/db-setup.yml); this is the local half.
  # A database that is not reachable yet is not a failed setup — .env is written and
  # correct, so say so and move on rather than unwinding everything.
  echo ""
  # SUBSHELL, deliberately: cmd_db_setup exits on "services not up yet", and an exit in a
  # sourced function would take all of init with it — setup then reported failure for a
  # situation that just means "migrations run on start". Which they now do (start.sh).
  if grep -q '^DATABASE_URL=' "$ROOT/.env" 2>/dev/null; then
    # Quietly: db-setup narrates its own advice ("start services first, then re-run"),
    # which contradicts and duplicates the one line that is true here.
    if (cmd_db_setup) >/dev/null 2>&1; then
      ok "Database schema is up to date"
    else
      info "Migrations wait for the platform: ${BOLD}unoverse start${NC} applies them once services are up"
    fi
  fi

  echo ""
  echo -e "  ${BOLD}Next steps:${NC}"
  echo ""
  if [ "$DOCKER_OK" = "1" ]; then
    echo -e "    ${GREEN}unoverse start${NC}     Start the platform"
  else
    echo -e "    ${DIM}1.${NC} Start Docker Desktop"
    echo -e "    ${DIM}2.${NC} ${GREEN}unoverse start${NC}"
  fi
  echo -e "    ${GREEN}unoverse where${NC}     Links to your Canvas and API"
  echo ""
  info "Run ${BOLD}unoverse check${NC} anytime to see if it is healthy"
  info "Change a setting any time: edit ${BOLD}.env${NC} directly, or re-run ${BOLD}unoverse create${NC} here (Enter keeps each current value)"
  echo ""
}
