#!/usr/bin/env bash

# =============================================================================
# Unoverse Platform CLI
# =============================================================================
# Zero-dependency developer tool. Works immediately after git clone.
#
# NOT TYPED BY A HUMAN. The developer's command is the npm package `unoverse`, which
# finds a universe folder and calls this. There is no `./unoverse` any more: one binary,
# everywhere (2026-07-31). This file is the operator half of it.
#
# The surface is deliberately small. Commands that were separate ways to ask the same
# question got folded, not renamed:
#   doctor, db-verify, status  →  check
#   db-setup                   →  runs inside create and deploy
#   open                       →  where (in the npm CLI; it already prints the addresses)
# Kept, but not advertised: ground, dev, build, publish, update nodes.
# =============================================================================

# Resolve lib/ directory relative to this script
GRAVITY_LIB="$(cd "$(dirname "$0")" && pwd)"
# If script is at project root (not in scripts/), lib is at scripts/lib
# If script is in scripts/, lib is at scripts/lib
if [ -d "$GRAVITY_LIB/lib" ]; then
  GRAVITY_LIB="$GRAVITY_LIB/lib"
elif [ -d "$GRAVITY_LIB/scripts/lib" ]; then
  GRAVITY_LIB="$GRAVITY_LIB/scripts/lib"
else
  echo "Error: Cannot find scripts/lib/ directory" >&2
  exit 1
fi

# True when the CURRENT DIRECTORY sits inside the platform monorepo. Deliberately keyed to
# $PWD and not to this script's location: the case it exists for is the INSTALLED CLI being
# run from inside this repo, where the script lives in node_modules and only the cwd says
# where the user actually is. The root package name is the marker, and no starter kit
# carries it.
_in_platform_monorepo() {
  local d="$PWD"
  while [ "$d" != "/" ] && [ -n "$d" ]; do
    if [ -f "$d/package.json" ] && grep -q '"name"[[:space:]]*:[[:space:]]*"unoverse-platform"' "$d/package.json" 2>/dev/null; then
      return 0
    fi
    d="$(dirname "$d")"
  done
  return 1
}

# ── Source all modules ──────────────────────────────────────────────
source "$GRAVITY_LIB/find-root.sh"
source "$GRAVITY_LIB/common.sh"
source "$GRAVITY_LIB/start.sh"
source "$GRAVITY_LIB/stop.sh"
source "$GRAVITY_LIB/logs.sh"
source "$GRAVITY_LIB/doctor.sh"
source "$GRAVITY_LIB/dev.sh"
source "$GRAVITY_LIB/check.sh"
source "$GRAVITY_LIB/help.sh"
source "$GRAVITY_LIB/db-setup.sh"
source "$GRAVITY_LIB/db-verify.sh"
source "$GRAVITY_LIB/setup.sh"
source "$GRAVITY_LIB/deploy.sh"
source "$GRAVITY_LIB/destroy.sh"
source "$GRAVITY_LIB/ground.sh"
# Authoring tools (lint, new, node test/hash, studio) moved to _legacy/scripts-lib
# 2026-07-28 — authoring lives in Studio now. The CLI is the OPERATOR tool.
# Owner-only module — absent in the starter kit by design.
[ -f "$GRAVITY_LIB/publish.sh" ] && source "$GRAVITY_LIB/publish.sh"

# ── Dispatch ───────────────────────────────────────────────────────
# (The old "studio mode" gate is gone — 2026-07-28. This CLI has ONE job:
# operate a universe. Studio is a separate app; authoring happens there.)
case "${1:-}" in
  # NOT A COMMAND ANYONE TYPES. `unoverse create` calls it with the registry token it has
  # already validated, and it writes the .env a local universe needs. It was deleted with
  # `unoverse init` (2026-08-02) on the reading that create wrote the .env; create never
  # did, it called this. What the deletion was RIGHT about is kept: it is not advertised,
  # and `start` no longer launches it, so starting the platform is never an interview.
  setup)     cmd_setup ;;
  start)     shift; cmd_start "$@" ;;
  stop)      cmd_stop ;;
  logs)      cmd_logs "${2:-}" ;;
  # ONE question, one command. `check` runs the health check, then the schema check,
  # then the deeper environment diagnosis — the three things that used to be `check`,
  # `db-verify` and `doctor`, which nobody could pick between.
  check)     cmd_check && cmd_db_verify && cmd_doctor ;;
  dev)       cmd_dev ;;
  ground)    shift; cmd_ground "$@" ;;
  destroy)   cmd_destroy "${2:-}" ;;
  db-allow)  cmd_db_allow "${2:-}" ;;
  refresh-images)
    # internal: `unoverse update` runs this after updating the CLI. Pull newer images
    # if the registry has them, and recreate only what is already running — an update
    # never boots a stopped universe.
    if [ ! -f "$ROOT/.env" ]; then
      : # unconfigured: nothing to refresh
    elif ! docker info >/dev/null 2>&1; then
      info "Docker is not running. Images refresh on the next unoverse start"
    else
      # The ground definitions first: they are code, and they update like code.
      # NO `local` HERE: a case branch in the dispatch is not a function body, and bash
      # errors "local: can only be used in a function" straight to the developer's screen.
      # THE COMPOSE FILE UPDATES LIKE THE GROUND DOES. A universe's docker-compose.yml was
      # written once at creation and never again, so every platform change to it — a new
      # service, a new environment variable — reached the monorepo and the starter and
      # stopped there. UNOVERSE_MARKETPLACE_URL was rendered into .env by the ground and
      # shipped by the deploy, and the container still never saw it, because the compose on
      # that universe had no line to pass it through.
      #
      # Their .env is untouched: compose reads values from it and holds none.
      _vcompose="$GRAVITY_LIB/../docker-compose.yml"
      if [ -f "$_vcompose" ] && [ -f "$ROOT/docker-compose.yml" ]; then
        if ! cmp -s "$_vcompose" "$ROOT/docker-compose.yml"; then
          cp "$_vcompose" "$ROOT/docker-compose.yml"
          ok "Refreshed docker-compose.yml ${DIM}(your .env is untouched)${NC}"
        fi
      fi

      # .env.example UPDATES LIKE THE COMPOSE FILE, and for the same reason: it is the
      # reference a developer reads when they add a setting by hand, and it was written once
      # at creation and never again. It went stale the moment CREDENTIAL_ENCRYPTION_KEY
      # became required — an old copy still describes it as optional with a built-in default,
      # which is now the shape that refuses to boot.
      #
      # THE EXAMPLE ONLY. Their .env is never touched by anything here.
      _vexample="$GRAVITY_LIB/../.env.example"
      if [ -f "$_vexample" ] && [ -f "$ROOT/.env.example" ]; then
        if ! cmp -s "$_vexample" "$ROOT/.env.example"; then
          cp "$_vexample" "$ROOT/.env.example"
          ok "Refreshed .env.example ${DIM}(your .env is untouched)${NC}"
        fi
      fi

      _vinfra="$GRAVITY_LIB/../infra"
      if [ -d "$_vinfra" ]; then
        for _g in digitalocean aws; do
          [ -d "$ROOT/infra/$_g" ] && [ -d "$_vinfra/$_g" ] || continue
          _changed=0
          for _f in "$_vinfra/$_g"/*.tf "$_vinfra/$_g"/*.example; do
            [ -f "$_f" ] || continue
            _base=$(basename "$_f")
            cmp -s "$_f" "$ROOT/infra/$_g/$_base" || { cp "$_f" "$ROOT/infra/$_g/$_base"; _changed=1; }
          done
          [ "$_changed" = "1" ] && ok "Refreshed the $_g ground ${DIM}(your terraform.tfvars and state untouched)${NC}"
        done
      fi

      info "Checking for newer platform images..."
      if ! docker compose -f "$ROOT/docker-compose.yml" --env-file "$ROOT/.env" pull; then
        # A failed pull must not be followed by "everything is up to date". Timeouts
        # here are almost always a network blip; the retry costs one command.
        echo ""
        warn "An image did not download (network blip, usually). Run ${BOLD}unoverse update${NC} again"
        echo ""
        exit 1
      fi
      if [ -n "$(docker compose -f "$ROOT/docker-compose.yml" ps -q 2>/dev/null)" ]; then
        # THE NODE RUNTIME ARRIVES WITH THE IMAGE. base is baked in, not installed
        # (MARKETPLACE.md §5a, reversed 2026-09-03), so pulling the image IS updating the
        # runtime and there is nothing extra to ask for. `UNOVERSE_BASE_REFRESH=1` used to
        # ride this line to mean "take the newest base in range"; the range is gone with it.
        docker compose -f "$ROOT/docker-compose.yml" --env-file "$ROOT/.env" up -d --remove-orphans >/dev/null 2>&1 \
          && ok "Running services moved to the new images"
        # MIGRATIONS RUN ON UPDATE, same as deploy and start. An update is exactly when a
        # new migration arrives (it ships inside the image just pulled), and this was the
        # one path that skipped db-setup: a universe could run new code against a schema
        # missing the columns that code expects, silently, until something read them.
        # Additive-by-rule until Retire and idempotent, so re-running costs nothing.
        # 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"
        else
          ok "Database migrations are current"
        fi
        echo ""
        echo -e "  ${GREEN}unoverse where${NC}     Links to your Canvas and API"
        echo ""
      else
        # An update never boots a stopped universe — but it should say what would.
        echo ""
        echo -e "  ${BOLD}Everything is up to date.${NC} Next:"
        echo ""
        echo -e "    ${GREEN}unoverse start${NC}     Start the platform"
        echo -e "    ${GREEN}unoverse where${NC}     Links to your Canvas and API"
        echo ""
      fi
    fi ;;
  deploy)
    # `deploy marketplace` is PLATFORM-OWNER only: it publishes the marketplace package
    # to npm, which a starter developer never does (they INSTALL from the marketplace).
    # It lives in publish.sh, the owner-only module sync-starter.sh strips, so on a
    # starter kit this branch simply reports that it is not their command.
    if [ "${2:-}" = "marketplace" ]; then
      if type cmd_deploy_marketplace >/dev/null 2>&1; then shift 2; cmd_deploy_marketplace "$@"
      else echo "deploy marketplace is a platform-owner command. Not available in the starter kit"; exit 1; fi
    else
      shift; cmd_deploy "$@"
    fi
    ;;
  publish)
    # TWO MEANINGS, decided by which kit you are in.
    #   monorepo  `publish` is the PLATFORM RELEASE (publish.sh, owner-only), so a
    #             developer publish is `publish assets <project>`.
    #   starter   sync-starter.sh strips publish.sh, so the name is free and `publish`
    #             means the only publish a developer has.
    if [ "${2:-}" = "assets" ]; then
      shift 2; node --import tsx "$GRAVITY_LIB/publish-assets.mjs" "$@"
    elif type cmd_publish >/dev/null 2>&1; then
      shift; cmd_publish "$@"
    elif _in_platform_monorepo; then
      # THE INSTALLED CLI CANNOT RELEASE THE PLATFORM, and inside this repo the fallthrough
      # below is never what anyone meant: it would start publishing design assets to a universe
      # while the operator believes they are shipping the platform. It used to do exactly
      # that, silently, which is the whole reason this branch exists. Refuse and say where
      # to go. A starter kit never reaches here (no `unoverse-platform` package above it),
      # so the developer publish keeps working untouched.
      echo "" >&2
      echo "  The unoverse CLI does not release the platform." >&2
      echo "" >&2
      echo "  You are inside the platform monorepo, where \`publish\` would publish rx" >&2
      echo "  ASSETS to a universe — not ship the platform. The release lives in this" >&2
      echo "  repo's own script, which the published CLI does not carry:" >&2
      echo "" >&2
      echo "    scripts/operator.sh publish" >&2
      echo "" >&2
      echo "  See docs/architecture/platform/DEVELOPER_GUIDE.md § Releasing." >&2
      echo "" >&2
      exit 1
    else
      shift; node --import tsx "$GRAVITY_LIB/publish-assets.mjs" "$@"
    fi
    ;;
  build)     cmd_build "${2:-}" ;;
  help|--help|-h) cmd_help ;;
  "")        cmd_help ;;
  *)
    echo "Unknown command: $1"
    echo "Run unoverse help for usage"
    exit 1
    ;;
esac

