#!/usr/bin/env bash

# Resources this universe is BILLED FOR that terraform does not know it owns.
#   _cloud_orphans <ground> <universe-name>
# Echoes nothing when everything is tracked, "unknown" when the provider could not be
# reached (never a failure — an offline laptop is not an orphaned resource), or one line
# per untracked resource.
#
# It works because every resource is labelled with the universe it belongs to: AWS through
# default_tags (Universe=<name>), DigitalOcean through project membership. Without that
# label there is no question to ask, which is why the Canvas load balancer landing in the
# wrong DO project was worth fixing rather than tidying.
_cloud_orphans() {
  local g="$1" uname_="$2" dir="$ROOT/infra/$1"
  [ -n "$uname_" ] || { echo unknown; return 0; }

  local state
  state=$(terraform -chdir="$dir" show -json 2>/dev/null) || { echo unknown; return 0; }
  [ -n "$state" ] || { echo unknown; return 0; }

  local live
  if [ "$g" = "aws" ]; then
    command -v aws >/dev/null 2>&1 || { echo unknown; return 0; }
    live=$(aws resourcegroupstaggingapi get-resources \
             --tag-filters "Key=Universe,Values=$uname_" \
             --query 'ResourceTagMappingList[].ResourceARN' --output text 2>/dev/null | tr '\t' '\n')
  else
    [ -n "${DIGITALOCEAN_TOKEN:-}" ] || { echo unknown; return 0; }
    local pid
    pid=$(doctl projects list --format ID,Name --no-header 2>/dev/null | awk -v n="$uname_" '$2==n{print $1}')
    [ -n "$pid" ] || { echo unknown; return 0; }
    live=$(doctl projects resources list "$pid" --format URN --no-header 2>/dev/null)
  fi
  [ -n "$live" ] || { echo ""; return 0; }

  printf '%s' "$state" | node -e '
    let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{
      const live=(process.argv[1]||"").split("\n").map(x=>x.trim()).filter(Boolean);
      let ids=new Set();
      try {
        const st=JSON.parse(s);
        const walk=(m)=>{ (m.resources||[]).forEach(r=>{ if(!r.values) return;
          ["id","arn","urn"].forEach(k=>r.values[k]&&ids.add(String(r.values[k]))); });
          (m.child_modules||[]).forEach(walk); };
        if (st.values && st.values.root_module) walk(st.values.root_module);
      } catch { process.exit(0); }          // unreadable state is not an orphan claim
      const known=[...ids];
      const orphans=live.filter(a=>!ids.has(a) && !known.some(i=>i && a.includes(i)));
      orphans.forEach(o=>console.log(o));
    });
  ' "$live"
}

# unoverse check

cmd_check() {
  echo ""
  echo -e "  ${BOLD}Unoverse Platform Health Check${NC}"
  echo ""
  local pass=0 total=0

  # 1. Services
  for svc in unoverse canvas umap documents memory; do
    total=$((total + 1))
    local status
    status=$(docker compose -f "$ROOT/docker-compose.yml" ps --format '{{.Status}}' "$svc" 2>/dev/null | head -1)
    if echo "$status" | grep -qi "up"; then
      ok "$svc"
      pass=$((pass + 1))
    elif echo "$status" | grep -qi "created"; then
      fail "$svc ${DIM}(stuck in Created, the container never started)${NC}"
    elif [ -z "$status" ]; then
      fail "$svc ${DIM}(no container found)${NC}"
    else
      fail "$svc ${DIM}($status)${NC}"
    fi
  done
  echo ""

  # 1b. THE NODE RUNTIME THIS UNIVERSE IS ACTUALLY RUNNING.
  #
  # base ships INSIDE the image (MARKETPLACE.md §5a, reversed 2026-09-03), so this number
  # cannot disagree with the platform version above — and printing it is how you can tell.
  # It used to be installed from npm at startup and compared against the registry here,
  # because the two moved independently; they do not any more, and a line that still
  # offered "npm has newer" would be advertising a lane that no longer exists.
  # Read out of the RUNNING container rather than off this machine's disk, because the
  # container is the only thing whose answer counts.
  local base_installed
  base_installed=$(docker compose -f "$ROOT/docker-compose.yml" exec -T unoverse \
    node -p "require('/app/node_modules/@unoverse-platform/base/package.json').version" 2>/dev/null | tr -d '\r')
  if [ -n "$base_installed" ]; then
    ok "node runtime ${BOLD}$base_installed${NC} ${DIM}(in the image)${NC}"
    echo ""
  fi

  # 2. Health endpoints
  for endpoint in 4105:unoverse 4101:engine 5001:umap 5002:documents 4104:memory; do
    local port="${endpoint%%:*}" name="${endpoint##*:}"
    total=$((total + 1))
    local code
    code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$port/health" 2>/dev/null)
    if [ "$code" = "200" ]; then
      ok "$name health ${DIM}:$port${NC}"
      pass=$((pass + 1))
    else
      fail "$name health ${DIM}:$port → $code${NC}"
    fi
  done
  echo ""

  # 3. Packages built
  local built=0 pkg_total=0
  for pkg in "$ROOT"/packages/*/; do
    [ -f "$pkg/package.json" ] || continue
    local name
    name=$(basename "$pkg")
    case "$name" in marketplace|gravity-client|plugin-base|skills|prompt-blocks) continue;; esac
    pkg_total=$((pkg_total + 1))
    if [ -f "$pkg/dist/index.js" ]; then
      built=$((built + 1))
    else
      fail "$name ${DIM}(missing dist/index.js)${NC}"
    fi
  done
  total=$((total + 1))
  if [ "$built" -eq "$pkg_total" ]; then
    ok "$built/$pkg_total packages built"
    pass=$((pass + 1))
  else
    fail "$built/$pkg_total packages built"
  fi

  # 4. Unoverse node catalog
  total=$((total + 1))
  local plugin_count
  # Catalog lives on unoverse; :4106 is Docker-internal and :4105 /plugins is JWT-gated,
  # so count nodes from inside the container (node:20-slim has no curl → use node fetch).
  plugin_count=$(docker compose -f "$ROOT/docker-compose.yml" exec -T unoverse node -e "fetch('http://127.0.0.1:4106/nodes').then(r=>r.json()).then(d=>console.log((d.nodes||[]).length)).catch(()=>console.log(0))" 2>/dev/null | tr -d ' \r')
  if [ "$plugin_count" -gt "0" ]; then
    ok "$plugin_count nodes loaded"
    pass=$((pass + 1))
  else
    fail "0 nodes loaded ${DIM}(check unoverse logs)${NC}"
  fi

  # 5. Canvas
  total=$((total + 1))
  local canvas_code
  canvas_code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001" 2>/dev/null)
  if [ "$canvas_code" = "200" ]; then
    ok "Canvas ${DIM}http://localhost:3001${NC}"
    pass=$((pass + 1))
  else
    fail "Canvas ${DIM}http://localhost:3001 → $canvas_code${NC}"
  fi

  # 6. NO PUBLISHED ITEM IS AN ORPHAN OF A RENAME.
  #
  # Publishing is a pure per-item upsert and the publisher cannot read this universe's
  # rows, so a RENAME is invisible to it: the new name lands as a create and the old row is
  # never touched. A caller picking the old name then gets an older vintage of the same UI,
  # and nothing anywhere goes red. That is a property of THIS universe's data, which is why
  # it is asked here and not in the release gate — a stale row on one box must never stop
  # anybody shipping code (server/tests/items/orphaned-rows.check.ts carries the history).
  if [ -d "$ROOT/apps/unoverse" ] && [ -d "$ROOT/node_modules" ]; then
    total=$((total + 1))
    if (cd "$ROOT/apps/unoverse" && npm run --silent check:universe >/dev/null 2>&1); then
      ok "No orphaned item rows ${DIM}(every published item is still authored)${NC}"
      pass=$((pass + 1))
    else
      fail "Orphaned item rows ${DIM}(a rename left a stale row behind)${NC}"
      (cd "$ROOT/apps/unoverse" && npm run --silent check:universe 2>&1) | grep -E '^\s+better-|^\s+[a-z0-9-]+/' | while read -r o; do
        [ -n "$o" ] && echo -e "      ${DIM}$o${NC}"
      done
      info "  Retire each row: ${BOLD}POST /marketplace/uninstall {kind, name}${NC} — deleting the folder does nothing"
    fi
  fi

  # 7. NOTHING BILLING THAT TERRAFORM HAS LOST SIGHT OF.
  #
  # Terraform records what it creates as it creates it, so an interrupted apply does not
  # duplicate anything — the next run continues from state. Two cases break that, and both
  # cost money silently: a crash between the API call and the state write, and a lost or
  # deleted state file, which orphans everything at once and makes it invisible to destroy.
  #
  # This is answerable because every resource is labelled with the universe it belongs to:
  # AWS through default_tags, DigitalOcean through project membership. Ask the provider
  # what it thinks is ours, ask terraform what it knows it made, and compare. A number on
  # the health check is the difference between trusting that and verifying it.
  local _g
  for _g in aws digitalocean; do
    [ -f "$ROOT/infra/$_g/terraform.tfvars" ] || continue
    [ -d "$ROOT/infra/$_g/.terraform" ] || continue
    total=$((total + 1))
    local uname_ orphans
    uname_=$(grep -E '^name[[:space:]]*=' "$ROOT/infra/$_g/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
    orphans=$(_cloud_orphans "$_g" "$uname_")
    case "$orphans" in
      "") ok "Cloud resources ${DIM}($_g — every resource is tracked)${NC}"; pass=$((pass + 1)) ;;
      unknown) ok "Cloud resources ${DIM}($_g — could not reach the provider, skipped)${NC}"; pass=$((pass + 1)) ;;
      *)
        fail "Cloud resources ${DIM}($_g — billing but NOT tracked by terraform)${NC}"
        echo "$orphans" | while read -r o; do [ -n "$o" ] && echo -e "      ${DIM}$o${NC}"; done
        info "  These will not be removed by ${BOLD}unoverse destroy $_g${NC}. Delete them in the console"
        ;;
    esac
  done

  # Summary
  echo ""
  if [ "$pass" -eq "$total" ]; then
    echo -e "  ${GREEN}${BOLD}All $total checks passed${NC}"
  else
    echo -e "  ${YELLOW}${BOLD}$pass/$total checks passed${NC}"
  fi
  echo ""
}
