#!/usr/bin/env bash
# ============================================================
# name-glsmith-owner.sh
#
# Purpose
# -------
# Name the G.L.Smith & Sons (accountId 2078cb54) business owner on the
# SiteDesk Code install (Neo4j bolt://localhost:7689). Task 1623.
#
# The owner name lives on the owner AdminUser's owned :Person
# (givenName/familyName) — the Task 1570 canonical name source. This is
# the ONLY correct place to write it. The older remediate-glsmith-identity.sh
# (Task 1569) wrote AdminUser.name, which Task 1570 retired; that field is
# no longer read for display, which is why the owner still renders nameless.
# This script supersedes that step with the owned-Person write.
#
# What it does (idempotent; logs its post-condition):
#   name-owner   set the owner's owned Person givenName='Dale',
#                familyName='Smith', embedding=null (so the standing reindex
#                re-embeds). If the owner owns no Person, create+OWNS one.
#   report-orphan  DETECT (read-only, never mutates): the orphaned
#                'Dale Smith' Person holding telephone 447958391734 with zero
#                edges to the LocalBusiness. Merging it into the owner identity
#                is OUT OF SCOPE (identity-dedup task); Dale is GL Smith's
#                account manager, so this contact is left in place. Reported so
#                the operator sees both Dale nodes and the deferral is explicit.
#
# Does NOT touch: managingAdminUserId (blocked on the managing-admin design
# question), account.json admins[] (removed fleet-wide by the installer, not
# here), the test account.
#
# Write-gate invariant: the account never drops below one AdminUser{accountId}
# and always retains its role:'owner' node. Asserted before and after the write.
#
# Usage
# -----
#   bash name-glsmith-owner.sh                 # dry-run (default)
#   bash name-glsmith-owner.sh --dry-run
#   bash name-glsmith-owner.sh --verify        # post-apply asserts
#   bash name-glsmith-owner.sh --apply --dump=/path/to/neo4j.dump
#   bash name-glsmith-owner.sh --apply --force-no-dump   # no dump (loud)
#
# --dry-run  Print every intended change; mutate nothing (default).
# --apply    Perform the write. Refuses without a valid --dump (a fresh full
#            neo4j-admin dump) unless --force-no-dump is given.
# --verify   Run the post-apply asserts; exit non-zero if any fail.
#
# Rollback: restore the operator's --dump via `neo4j-admin database load`.
#
# Reads:
#   ~/sitedesk-code/platform/config/.neo4j-password   Neo4j password
# ============================================================

set -euo pipefail

readonly ACCT='2078cb54-08e9-49e9-bf8e-e9f3ad76ca41'
readonly OWNER='3f81e7c2-8e38-42f0-bce2-7355cd7c1ecf'   # role:owner AdminUser
readonly GIVEN='Dale'
readonly FAMILY='Smith'
readonly DALE_PHONE='447958391734'

readonly PWFILE="$HOME/sitedesk-code/platform/config/.neo4j-password"
readonly NEO4J_USER="${NEO4J_USER:-neo4j}"
readonly URI="${NEO4J_URI:-bolt://localhost:7689}"

MODE="dry-run"
DUMP_PATH=""
FORCE_NO_DUMP=0

while [ $# -gt 0 ]; do
  case "$1" in
    --dry-run) MODE="dry-run"; shift ;;
    --apply)   MODE="apply"; shift ;;
    --verify)  MODE="verify"; shift ;;
    --dump=*)  DUMP_PATH="${1#*=}"; shift ;;
    --dump)    DUMP_PATH="${2:-}"; shift 2 ;;
    --force-no-dump) FORCE_NO_DUMP=1; shift ;;
    -h|--help) sed -n '2,/^# =\{20,\}/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
    *) echo "Unknown arg: $1" >&2; exit 2 ;;
  esac
done

log() { echo "[name-glsmith-owner] $*" >&2; }

[ -f "$PWFILE" ] || { echo "Error: $PWFILE missing (run on the SiteDesk Code box)" >&2; exit 1; }
command -v cypher-shell >/dev/null 2>&1 || { echo "Error: cypher-shell not in PATH" >&2; exit 1; }
PW="$(cat "$PWFILE")"

run_cypher() { cypher-shell -u "$NEO4J_USER" -p "$PW" -a "$URI" --format plain; }
scalar() { { tail -n +2 | tr -d '"' | grep -v '^[[:space:]]*$' | head -1; } || true; }
as_int() { awk -F',' '{for(i=1;i<=NF;i++){gsub(/[^0-9-]/,"",$i); if($i ~ /^-?[0-9]+$/) v=$i}} END{print v+0}'; }

# --- Write-gate: >=1 AdminUser{accountId} and >=1 role:'owner' + LocalBusiness -
write_gate() {
  local out n owners biz
  out="$(run_cypher <<CYPHER
MATCH (a:AdminUser {accountId:'$ACCT'})
WITH count(a) AS n, sum(CASE WHEN a.role='owner' THEN 1 ELSE 0 END) AS owners
OPTIONAL MATCH (b:LocalBusiness {accountId:'$ACCT'})
RETURN n, owners, CASE WHEN count(b) > 0 THEN 1 ELSE 0 END AS business;
CYPHER
)"
  n="$(echo "$out"   | tail -n +2 | awk -F',' '{print $1+0}')"
  owners="$(echo "$out" | tail -n +2 | awk -F',' '{print $2+0}')"
  biz="$(echo "$out" | tail -n +2 | awk -F',' '{print $3+0}')"
  echo "admins=$n owners=$owners business=$biz"
  [ "$n" -ge 1 ] && [ "$owners" -ge 1 ]
}

assert_write_gate() {
  local g; g="$(write_gate)" || { log "ABORT write-gate breached: $g"; exit 3; }
  case "$g" in *business=1*) : ;; *) log "ABORT write-gate: no LocalBusiness ($g)"; exit 3 ;; esac
  log "write-gate ok $g"
}

preconditions() {
  run_cypher <<< "RETURN 1;" >/dev/null 2>&1 || { echo "Error: cannot connect to $URI" >&2; exit 1; }
  local ownerCount
  ownerCount="$(run_cypher <<CYPHER | as_int
MATCH (a:AdminUser {userId:'$OWNER', accountId:'$ACCT', role:'owner'}) RETURN count(a);
CYPHER
)"
  [ "$ownerCount" = "1" ] || { log "ABORT precondition: expected exactly one role:owner AdminUser $OWNER on $ACCT, found $ownerCount"; exit 3; }
  assert_write_gate
  log "preconditions ok owner=$OWNER"
}

# --- report-orphan: read-only detection, never mutates -----------------------
report_orphan() {
  local orphan
  orphan="$(run_cypher <<CYPHER | as_int
MATCH (p:Person {accountId:'$ACCT'})
WHERE p.givenName='$GIVEN' AND p.familyName='$FAMILY' AND p.telephone='$DALE_PHONE'
  AND NOT (p)<-[:OWNS]-(:AdminUser {accountId:'$ACCT', role:'owner'})
RETURN count(p);
CYPHER
)"
  if [ "$orphan" -ge 1 ]; then
    log "report-orphan found=$orphan note=orphaned '$GIVEN $FAMILY' Person holds WA $DALE_PHONE, not owned by the owner. Left as the account-manager contact; merge is out of scope (identity-dedup task)."
  else
    log "report-orphan found=0 (no separate account-manager Dale contact detected)"
  fi
}

# --- name-owner: SET the owner's owned Person, or create it if absent ---------
name_owner() {
  local ownedNamed ownsAny
  ownedNamed="$(run_cypher <<CYPHER | as_int
MATCH (:AdminUser {userId:'$OWNER', accountId:'$ACCT'})-[:OWNS]->(p:Person)
WHERE p.givenName='$GIVEN' AND p.familyName='$FAMILY' RETURN count(p);
CYPHER
)"
  if [ "$ownedNamed" -ge 1 ]; then log "step=name-owner no-op (owner already owns a '$GIVEN $FAMILY' Person)"; return; fi
  ownsAny="$(run_cypher <<CYPHER | as_int
MATCH (:AdminUser {userId:'$OWNER', accountId:'$ACCT'})-[:OWNS]->(p:Person) RETURN count(p);
CYPHER
)"
  if [ "$MODE" = "dry-run" ]; then
    if [ "$ownsAny" -ge 1 ]; then
      log "step=name-owner WOULD SET the owner's owned Person givenName='$GIVEN', familyName='$FAMILY', embedding=null (owner currently owns $ownsAny Person(s))"
    else
      log "step=name-owner WOULD CREATE a Person givenName='$GIVEN', familyName='$FAMILY' and OWNS it (owner currently owns 0)"
    fi
    return
  fi
  # apply: rename the existing owned Person, else create+OWNS one. Same shape as
  # setOwnerName (account-lifecycle.ts). embedding=null so the reindex re-embeds.
  run_cypher <<CYPHER >/dev/null
MATCH (au:AdminUser {userId:'$OWNER', accountId:'$ACCT'})
OPTIONAL MATCH (au)-[:OWNS]->(existing:Person)
WITH au, existing LIMIT 1
CALL {
  WITH au, existing
  WITH au, existing WHERE existing IS NOT NULL
  SET existing.givenName='$GIVEN', existing.familyName='$FAMILY',
      existing.embedding=null, existing.updatedAt=timestamp()
  RETURN 1 AS done
  UNION
  WITH au, existing
  WITH au WHERE existing IS NULL
  CREATE (p:Person {accountId:'$ACCT', givenName:'$GIVEN', familyName:'$FAMILY',
                    role:'admin-personal', scope:'admin', embedding:null, createdAt:timestamp()})
  MERGE (au)-[:OWNS]->(p)
  RETURN 1 AS done
}
RETURN done;
CYPHER
  assert_write_gate
  log "step=name-owner owner owned Person named '$GIVEN $FAMILY'"
}

rollback_guard() {
  if [ -n "$DUMP_PATH" ]; then
    [ -s "$DUMP_PATH" ] || { echo "Error: --dump path '$DUMP_PATH' is missing or empty" >&2; exit 1; }
    log "rollback dump provided path=$DUMP_PATH bytes=$(wc -c < "$DUMP_PATH")"
  elif [ "$FORCE_NO_DUMP" -eq 1 ]; then
    log "WARNING proceeding WITHOUT a neo4j dump (--force-no-dump)"
  else
    echo "Error: --apply needs --dump=/path/to/neo4j.dump (fresh full dump) or --force-no-dump" >&2
    echo "  Take a dump on the box, e.g.:" >&2
    echo "    systemctl --user stop neo4j-sitedesk-code.service && \\" >&2
    echo "    neo4j-admin database dump neo4j --to-path=\$HOME/glsmith-1623-dump && \\" >&2
    echo "    systemctl --user start neo4j-sitedesk-code.service" >&2
    exit 1
  fi
}

verify() {
  local fail=0
  check() { if [ "$2" = "$3" ]; then log "verify OK  $1 ($2)"; else log "verify FAIL $1 (got '$2' want '$3')"; fail=1; fi; }
  local ownerName ownerOwns gate
  ownerName="$(run_cypher <<CYPHER | scalar
MATCH (:AdminUser {userId:'$OWNER', accountId:'$ACCT'})-[:OWNS]->(p:Person)
RETURN trim(coalesce(p.givenName,'') + ' ' + coalesce(p.familyName,''));
CYPHER
)"
  ownerOwns="$(run_cypher <<CYPHER | as_int
MATCH (:AdminUser {userId:'$OWNER', accountId:'$ACCT'})-[:OWNS]->(p:Person) RETURN count(p);
CYPHER
)"
  gate="$(write_gate || true)"
  check "owner owned Person named '$GIVEN $FAMILY'" "$ownerName" "$GIVEN $FAMILY"
  check "owner owns at least one Person" "$([ "$ownerOwns" -ge 1 ] && echo yes || echo no)" "yes"
  case "$gate" in *"owners=1 business=1"*) log "verify OK  write-gate ($gate)";; *) log "verify FAIL write-gate ($gate)"; fail=1;; esac
  [ "$fail" -eq 0 ] && { log "verify PASS"; return 0; } || { log "verify FAILED"; return 1; }
}

log "start mode=$MODE account=$ACCT uri=$URI"
if [ "$MODE" = "verify" ]; then verify; exit $?; fi
preconditions
report_orphan
if [ "$MODE" = "apply" ]; then rollback_guard; fi
name_owner
if [ "$MODE" = "apply" ]; then
  log "apply complete — running verify"
  verify || { log "POST-APPLY VERIFY FAILED — inspect before trusting"; exit 4; }
else
  log "dry-run complete — no writes performed. Re-run with --apply --dump=<path>."
fi
