# resolve-account-dir.sh — identity-aware account resolver + orphan sweeper
# Sourced by setup-account.sh and seed-neo4j.sh. Defines `resolve_and_sweep_account_dir`.
#
# Invariant: the TypeScript `resolveAccount()` at platform/ui/app/lib/claude-agent.ts
# is READ-ONLY. This bash helper is the SOLE sweep surface, because seed-time is
# the only safe moment to mutate filesystem layout. Any future "share the logic"
# refactor must preserve that asymmetry.
#
# Contract (inputs via env vars):
#   ACCOUNTS_DIR — {installDir}/data/accounts
#   USERS_FILE   — $HOME/<brand.configDir>/users.json
#
# Contract (outputs via env vars):
#   ACCOUNT_ID   — resolved uuid (existing kept, or freshly minted)
#   ACCOUNT_DIR  — full path to the live account dir
#
# Exit:
#   0 on success; non-zero on abort (multi-candidate + no unique identity match).

# Helper: classify account.json at $1 against userId $2.
# Exit codes are trichotomous so callers can distinguish "definitely not a match"
# from "could not determine" — the latter is a doctrine-level loud failure, not
# a silent fall-through to orphan-classification.
#   0 = admins[] contains userId (match)
#   1 = account.json readable and parsed; userId absent from admins[] (no match)
#   2 = account.json missing, unreadable, or unparseable (undetermined — caller must abort)
_account_dir_matches() {
  local config="$1/account.json"
  local uid="$2"
  [ -f "$config" ] || return 2
  python3 - "$config" "$uid" <<'PY'
import json, sys
try:
    cfg = json.load(open(sys.argv[1]))
except Exception:
    sys.exit(2)
admins = cfg.get('admins') or []
uid = sys.argv[2]
for a in admins:
    if isinstance(a, dict) and a.get('userId') == uid:
        sys.exit(0)
sys.exit(1)
PY
}

# Two-pass resolver: enumerate+classify all candidates, then act.
# Never mutates filesystem before the full classification is complete.
resolve_and_sweep_account_dir() {
  mkdir -p "$ACCOUNTS_DIR"

  # Enumerate candidates: direct children with account.json, basenames not starting with '.'
  local candidates=()
  local dir base
  for dir in "$ACCOUNTS_DIR"/*/; do
    [ -d "$dir" ] || continue
    base="$(basename "$dir")"
    case "$base" in .*) continue ;; esac
    [ -f "$dir/account.json" ] || continue
    candidates+=("$base")
  done

  # Read target userId from users.json (if present and valid)
  local target_user_id=""
  if [ -f "$USERS_FILE" ]; then
    target_user_id=$(python3 - "$USERS_FILE" <<'PY' 2>/dev/null || true
import json, sys
try:
    data = json.load(open(sys.argv[1]))
    if isinstance(data, list) and data and isinstance(data[0], dict):
        uid = data[0].get('userId')
        if uid:
            print(uid)
except Exception:
    pass
PY
)
  fi

  local n=${#candidates[@]}

  # Case 0: no candidates — fresh install, mint new UUID.
  # sub-scope F — atomic-mint discipline: refuse to mint a new
  # accountId if any stub dir (subdir present, account.json missing) exists
  # under data/accounts/. The root cause is install 1 minting accountId X
  # then losing X/account.json (cron stamped X/logs/ first); the next
  # install sees zero candidates and mints Y, orphaning X/logs/. The
  # guard turns that silent regression into an operator-actionable abort.
  if [ "$n" -eq 0 ]; then
    local stub_dirs=()
    for dir in "$ACCOUNTS_DIR"/*/; do
      [ -d "$dir" ] || continue
      base="$(basename "$dir")"
      case "$base" in .*) continue ;; esac
      [ -f "$dir/account.json" ] && continue
      stub_dirs+=("$base")
    done
    if [ "${#stub_dirs[@]}" -gt 0 ]; then
      echo "==> [seed] FAIL phase=mint reason=\"stub-account-dirs present (no account.json): ${stub_dirs[*]}\" — refusing to mint a fresh accountId; remove or repair the stub before re-running install" >&2
      return 1
    fi
    ACCOUNT_ID="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c 'import uuid; print(uuid.uuid4())')"
    ACCOUNT_DIR="$ACCOUNTS_DIR/$ACCOUNT_ID"
    return 0
  fi

  # Case 1: single candidate — accept (preserves legacy + fresh-install-pre-users.json).
  # Log identity-match on exact match; WARN on mismatch (mirrors TS resolveAccount line 653 —
  # accept-with-flag rather than abort because the users.json regeneration on PIN
  # change would otherwise brick legitimate installs).
  if [ "$n" -eq 1 ]; then
    ACCOUNT_ID="${candidates[0]}"
    ACCOUNT_DIR="$ACCOUNTS_DIR/$ACCOUNT_ID"
    if [ -n "$target_user_id" ]; then
      local rc=0
      _account_dir_matches "$ACCOUNT_DIR" "$target_user_id" || rc=$?
      case "$rc" in
        0) echo "==> [seed] identity-match: kept=${ACCOUNT_ID:0:8} via userId=${target_user_id:0:8}" ;;
        1) echo "==> [seed] WARN: single account does not match users.json userId=${target_user_id:0:8} — accepting but flagging" >&2 ;;
        *) echo "==> [seed] identity-match FAILED: sole candidate $ACCOUNT_ID has unreadable or unparseable account.json — aborting" >&2
           return 1 ;;
      esac
    fi
    return 0
  fi

  # Case 2+: multi-candidate — identity-match-or-abort.
  # First classify everything, THEN decide and act.
  if [ -z "$target_user_id" ]; then
    echo "==> [seed] identity-match FAILED: users.json missing or unreadable, $n candidates — aborting" >&2
    for uuid in "${candidates[@]}"; do echo "    candidate: $uuid" >&2; done
    return 1
  fi

  local matches=()
  local orphans=()
  local rc
  for uuid in "${candidates[@]}"; do
    rc=0
    _account_dir_matches "$ACCOUNTS_DIR/$uuid" "$target_user_id" || rc=$?
    case "$rc" in
      0) matches+=("$uuid") ;;
      1) orphans+=("$uuid") ;;
      *) echo "==> [seed] identity-match FAILED: candidate $uuid has unreadable or unparseable account.json — aborting" >&2
         for u in "${candidates[@]}"; do echo "    candidate: $u" >&2; done
         return 1 ;;
    esac
  done

  if [ "${#matches[@]}" -eq 0 ]; then
    echo "==> [seed] identity-match FAILED: $n candidates, none match users.json userId=${target_user_id:0:8} — aborting" >&2
    for uuid in "${candidates[@]}"; do echo "    candidate: $uuid" >&2; done
    return 1
  fi

  if [ "${#matches[@]}" -gt 1 ]; then
    echo "==> [seed] identity-match FAILED: $n candidates, ${#matches[@]} match userId=${target_user_id:0:8} (ambiguous) — aborting" >&2
    for uuid in "${candidates[@]}"; do echo "    candidate: $uuid" >&2; done
    return 1
  fi

  ACCOUNT_ID="${matches[0]}"
  ACCOUNT_DIR="$ACCOUNTS_DIR/$ACCOUNT_ID"
  echo "==> [seed] identity-match: kept=${ACCOUNT_ID:0:8} via userId=${target_user_id:0:8}"

  # Sweep orphans. mv is the only destructive op; abort loud on failure before any further state change.
  local trash_dir="$ACCOUNTS_DIR/.trash"
  mkdir -p "$trash_dir"
  local ts; ts=$(date -u +%Y%m%dT%H%M%SZ)
  local moved=0 uuid dest
  for uuid in "${orphans[@]}"; do
    dest="$trash_dir/${uuid}-${ts}"
    if ! mv "$ACCOUNTS_DIR/$uuid" "$dest"; then
      echo "==> [seed] ERROR: failed to move $uuid → $dest — aborting" >&2
      return 1
    fi
    echo "==> [seed] swept orphan: ${uuid:0:8} → .trash/${uuid:0:8}-${ts}"
    moved=$((moved + 1))
  done
  if [ "$moved" -gt 0 ]; then
    echo "==> [seed] orphan sweep: moved $moved → $ACCOUNTS_DIR/.trash/"
  fi

  return 0
}
