# provision-account-dir.sh — per-account scaffolding, callable per account.
# Sourced by setup-account.sh (for the resolved house account) and by the
# admin-core `account_create` lifecycle tool (for a new client account). Defines
# `provision_account_dir <account_dir> <account_id> <role>`.
#
# The function is pure scaffolding: it creates the account dir layout, writes the
# Claude Code project settings (hooks only), installs agent identities and the
# specialist plugin, seeds account.json (stamping accountId + role), seeds
# enabledPlugins from brand defaults, and stamps the install owner into admins[].
# It does NO account resolution and NO Neo4j work.
#
# Required globals (set by the caller before invoking):
#   TEMPLATES_DIR  — <project>/templates
#   PROJECT_DIR    — the platform/ dir
#   SCRIPT_DIR     — the platform/scripts dir (for lib bootstraps)
#   USERS_FILE     — $HOME/<brand.configDir>/users.json (owner stamp; optional)

# Owned-dir schema merge (plugin-declared top-level account dirs).
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/account-schema-owned-dirs.sh"

provision_account_dir() {
  local ACCOUNT_DIR="$1"
  local ACCOUNT_ID="$2"
  local ROLE="$3"

  mkdir -p "$ACCOUNT_DIR/agents/admin" "$ACCOUNT_DIR/agents/passive" "$ACCOUNT_DIR/.claude" "$ACCOUNT_DIR/specialists/.claude-plugin" "$ACCOUNT_DIR/specialists/agents"

  # --- Account filesystem schema seed (idempotent) ---------------------------
  # Operator-data buckets (entity-anchored, flat) + tool-owned dirs. agents/ and
  # specialists/ are created above. The seeded SCHEMA.md is the single source of
  # the allowed top-level set, parsed at runtime by the fs-schema write guard.
  # `sites` joins the seed set because it is a home bucket in the /data browser
  # (HOME_FIXED); an unseeded home bucket is reported by the standing
  # home-parity audit as missing on every account that has not published a site.
  local _seed_dirs=(projects contacts documents url-get output generated extracted uploads sites)
  local _d _seeded=0
  for _d in "${_seed_dirs[@]}"; do
    [ -d "$ACCOUNT_DIR/$_d" ] || { mkdir -p "$ACCOUNT_DIR/$_d" && _seeded=$((_seeded+1)); }
  done
  if [ -f "$TEMPLATES_DIR/account-schema/SCHEMA.md" ]; then
    cp "$TEMPLATES_DIR/account-schema/SCHEMA.md" "$ACCOUNT_DIR/SCHEMA.md"
  else
    echo "  [acct-schema] WARNING: template missing at $TEMPLATES_DIR/account-schema/SCHEMA.md — SCHEMA.md not seeded" >&2
  fi
  echo "  [acct-schema] seeded dirs=$_seeded"

  # Merge brand-shipped plugin-declared owned top-level dirs into the freshly
  # seeded SCHEMA.md so the guard admits them (e.g. quoting/ on SiteDesk).
  merge_owned_dirs_into_schema "$ACCOUNT_DIR"

  # Create the vertical's root domain buckets on disk (Property -> properties/
  # for estate-agent, Job/Customer/... for construction) so they are present
  # directories, not just allowed names the operator never sees.
  seed_domain_root_dirs "$ACCOUNT_DIR"

  # Claude Code discovers project-level .claude/ relative to the nearest .git
  # root. Without a .git in the account dir it traverses up to ~/maxy/.git and
  # misses accountDir/.claude/. Initialise a repo so the account dir IS the root.
  [ -d "$ACCOUNT_DIR/.git" ] || git init -q "$ACCOUNT_DIR"

  # --- project-level .claude/settings.json (hooks; no allowlist) -------------
  local ACCOUNT_SETTINGS="$ACCOUNT_DIR/.claude/settings.json"
  local HOOKS_PATH="\$PLATFORM_ROOT/plugins/admin/hooks"
  cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
{
  "permissions": {
    "defaultMode": "bypassPermissions",
    "allow": [],
    "deny": []
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
          { "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard.sh" }
        ]
      },
      {
        "matcher": "Edit",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
          { "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard.sh" }
        ]
      },
      {
        "matcher": "NotebookEdit",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
          { "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard.sh" }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
          { "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard-bash-pre.sh" },
          { "type": "command", "command": "bash $HOOKS_PATH/quote-engine-gate.sh" }
        ]
      },
      {
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
        ]
      },
      {
        "matcher": "WebFetch",
        "hooks": [
          { "type": "command", "command": "node $HOOKS_PATH/webfetch-preflight.mjs" }
        ]
      },
      {
        "matcher": "AskUserQuestion",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/askuserquestion-investigate-gate.sh" },
          { "type": "command", "command": "bash $HOOKS_PATH/askuserquestion-channel-carrier-gate.sh" }
        ]
      },
      {
        "matcher": "mcp__plugin_browser_browser__browser-pdf-save",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/quote-render-gate.sh" }
        ]
      },
      {
        "matcher": "mcp__plugin_browser_browser__browser-pdf-save",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/preference-consult-gate.sh" }
        ]
      },
      {
        "matcher": "mcp__plugin_email_email__email-send|mcp__plugin_email_email__email-reply|mcp__plugin_email_email__email-draft-send|mcp__plugin_outlook_outlook__outlook-mail-send|mcp__plugin_outlook_outlook__outlook-mail-reply|mcp__plugin_outlook_outlook__outlook-draft-send|SendUserFile",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/preference-consult-gate.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/fs-schema-guard-bash-post.sh" }
        ]
      },
      {
        "matcher": "Agent",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/post-tool-use-agent.sh" }
        ]
      },
      {
        "matcher": "mcp__plugin_browser_browser__browser-pdf-save",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/quote-render-pdf-conformance.sh" }
        ]
      },
      {
        "matcher": "mcp__.*__.*-(export|import)-parse$",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
        ]
      },
      {
        "matcher": "mcp__.*",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/mcp-tool-missing.sh" }
        ]
      },
      {
        "matcher": "Write",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/admin-authoring-observer.sh" }
        ]
      },
      {
        "matcher": "Edit",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/admin-authoring-observer.sh" }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" },
          { "type": "command", "command": "bash $HOOKS_PATH/prompt-optimiser-directive.sh" },
          { "type": "command", "command": "bash $HOOKS_PATH/datetime-inject.sh" },
          { "type": "command", "command": "bash $HOOKS_PATH/preference-consult-directive.sh" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/prompt-optimiser-compliance.sh" }
        ]
      }
    ]
  }
}
SETTINGS_EOF

  # --- agent identities + specialist plugin templates ------------------------
  cp "$TEMPLATES_DIR/agents/admin/IDENTITY.md" "$ACCOUNT_DIR/agents/admin/IDENTITY.md"
  # Task 1483 — the WhatsApp passive requirement-intake identity. Rubytech-
  # controlled and uniform (like admin), so it is re-seeded from the template on
  # every provision, and excluded from the public re-sync loop below (which would
  # otherwise clobber it with the public visitor IDENTITY).
  cp "$TEMPLATES_DIR/agents/passive/IDENTITY.md" "$ACCOUNT_DIR/agents/passive/IDENTITY.md"

  # On re-provision, re-sync the Rubytech-controlled IDENTITY.md for every
  # already-existing non-admin agent dir (security-load-bearing). Never create
  # dirs; never write SOUL/KNOWLEDGE/config.
  local _AGENT_DIR _AGENT_NAME
  for _AGENT_DIR in "$ACCOUNT_DIR/agents"/*/; do
    [ -d "$_AGENT_DIR" ] || continue
    _AGENT_NAME="$(basename "$_AGENT_DIR")"
    [ "$_AGENT_NAME" = "admin" ] && continue
    # Task 1483 — passive has its own Rubytech IDENTITY (seeded above); it must
    # NOT be re-synced from the public visitor template.
    [ "$_AGENT_NAME" = "passive" ] && continue
    [ -f "$_AGENT_DIR/IDENTITY.md" ] || continue
    cp "$TEMPLATES_DIR/agents/public/IDENTITY.md" "$_AGENT_DIR/IDENTITY.md"
    # Defensive verify — a 0-byte cp traps the brand server in STARTING (the
    # reachability auditor reports `reachable=false reason=files-missing
    # IDENTITY.md:empty` every minute). Observed on 192.168.88.16 after a
    # re-install: source 1655 bytes, dest 0 bytes. Loud-fail here so the
    # operator sees the bad write at install time, not the splash forever.
    if [ ! -s "$_AGENT_DIR/IDENTITY.md" ]; then
      echo "  [setup] FATAL agents/$_AGENT_NAME/IDENTITY.md is empty after copy from $TEMPLATES_DIR/agents/public/IDENTITY.md ($(wc -c < "$TEMPLATES_DIR/agents/public/IDENTITY.md") bytes); aborting before the brand server starts" >&2
      return 1
    fi
    echo "  agents/$_AGENT_NAME/IDENTITY.md re-synced"
  done

  cp "$TEMPLATES_DIR/specialists/.claude-plugin/plugin.json" "$ACCOUNT_DIR/specialists/.claude-plugin/plugin.json"

  # Replace core specialist files (no '--'); preserve premium plugin agents.
  local _existing
  for _existing in "$ACCOUNT_DIR/specialists/agents/"*.md; do
    [ -f "$_existing" ] || continue
    case "$(basename "$_existing")" in
      *--*) continue ;;
    esac
    rm -f "$_existing"
  done

  local specialist
  for specialist in "$TEMPLATES_DIR/specialists/agents/"*.md; do
    [ -f "$specialist" ] && cp "$specialist" "$ACCOUNT_DIR/specialists/agents/$(basename "$specialist")"
  done

  # Task 1996 — the copy loop above re-delivers every core specialist, which
  # would silently undo a disable on the next publish. specialists/agents/ is
  # the dispatchable set (spawn-context.ts reads it), so an agent the operator
  # switched off is removed again here. The quarantined copy under
  # specialists/agents-disabled/ is never touched: it is the operator's own
  # file, and for a premium or edited agent it is the only copy.
  #
  # An unreadable store withholds NOTHING. A corrupt file is not evidence that
  # an agent was disabled, and withholding the whole set on a parse error would
  # turn a bad byte into a fleet-wide loss of specialists.
  if [ -f "$ACCOUNT_DIR/agents-disabled.json" ]; then
    local _disabled_names _withheld=0
    if _disabled_names="$(python3 -c '
import json, sys
with open(sys.argv[1]) as fh:
    names = json.load(fh)["disabled"]
if not isinstance(names, list):
    raise SystemExit(1)
for n in names:
    if isinstance(n, str) and n.endswith(".md") and "/" not in n and ".." not in n:
        print(n)
' "$ACCOUNT_DIR/agents-disabled.json" 2>/dev/null)"; then
      local _name
      while IFS= read -r _name; do
        [ -n "$_name" ] || continue
        if [ -e "$ACCOUNT_DIR/specialists/agents/$_name" ]; then
          # MOVE, never delete. In the drift state the standing parity check
          # exists to catch (store names it, file is live, quarantine empty) a
          # delete would destroy the only copy of a premium `--` agent or an
          # operator-edited override: the replace loop above skips `*--*` and
          # the copy loop only restores templates, so nothing re-delivers it.
          mkdir -p "$ACCOUNT_DIR/specialists/agents-disabled"
          mv -f "$ACCOUNT_DIR/specialists/agents/$_name" \
                "$ACCOUNT_DIR/specialists/agents-disabled/$_name"
          _withheld=$((_withheld + 1))
        fi
      done <<< "$_disabled_names"
      echo "  specialists agents-withheld=$_withheld (disabled by operator)"
    else
      echo "  specialists agents-disabled-store-unreadable — withholding nothing"
    fi
  fi

  rm -f "$ACCOUNT_DIR/.claude/agents/"*.md 2>/dev/null || true

  # SOUL.md — user-controlled. Only create if missing. Never overwrite.
  if [ -f "$ACCOUNT_DIR/agents/admin/SOUL.md" ]; then
    echo "  agents/admin/SOUL.md preserved ($(wc -c < "$ACCOUNT_DIR/agents/admin/SOUL.md" | tr -d ' ') bytes)"
  else
    cp "$TEMPLATES_DIR/agents/admin/SOUL.md" "$ACCOUNT_DIR/agents/admin/SOUL.md"
    echo "  agents/admin/SOUL.md created ($(wc -c < "$ACCOUNT_DIR/agents/admin/SOUL.md" | tr -d ' ') bytes)"
  fi

  bash "$SCRIPT_DIR/lib/agents-md-bootstrap.sh" "$ACCOUNT_DIR"
  bash "$SCRIPT_DIR/lib/admin-skills-bootstrap.sh" "$ACCOUNT_DIR" "$PROJECT_DIR/plugins"

  # --- account.json: create from template, stamp accountId + role ------------
  local TEMPLATE="$TEMPLATES_DIR/account.json"
  if [ ! -f "$ACCOUNT_DIR/account.json" ]; then
    if [ -f "$TEMPLATE" ]; then
      sed "s/\"accountId\": *\"[^\"]*\"/\"accountId\":\"$ACCOUNT_ID\"/" "$TEMPLATE" \
        | sed "s/\"tier\": *\"[^\"]*\"/\"tier\":\"solo\"/" \
        > "$ACCOUNT_DIR/account.json"
    else
      echo "ERROR: account.json template not found at $TEMPLATE" >&2
      return 1
    fi
  fi

  # Stamp role on the account.json (idempotent migration + designation). A
  # pre-existing legacy account.json with no role gets ROLE; an account already
  # carrying a role keeps it unless ROLE differs (operator-driven via the
  # lifecycle tools, never here).
  ACCOUNT_ID="$ACCOUNT_ID" ROLE="$ROLE" python3 - "$ACCOUNT_DIR/account.json" <<'PY'
import json, os, sys
path = sys.argv[1]
role = os.environ["ROLE"]
acct_id = os.environ["ACCOUNT_ID"]
with open(path) as f:
    cfg = json.load(f)
changed = False
if not cfg.get("accountId"):
    cfg["accountId"] = acct_id; changed = True
if role and cfg.get("role") != role:
    cfg["role"] = role; changed = True
if "managedService" not in cfg:
    cfg["managedService"] = False; changed = True
# Task 1623 — the client sub-account admins[] is vestigial (Task 999 retired the
# seats; Task 1596 retired the inbound read). Access is install-wide, so a client
# never carries admins[]. The house keeps it (its real access surface).
if role == "client" and "admins" in cfg:
    del cfg["admins"]; changed = True
if changed:
    with open(path, "w") as f:
        json.dump(cfg, f, indent=2); f.write("\n")
    print(f"  account.json role={cfg.get('role')} stamped")
PY

  # --- enabledPlugins from brand.json plugins.defaultEnabled -----------------
  local BRAND_JSON="$PROJECT_DIR/config/brand.json"
  if [ -f "$BRAND_JSON" ] && [ -f "$ACCOUNT_DIR/account.json" ]; then
    PROJECT_DIR="$PROJECT_DIR" BRAND_JSON="$BRAND_JSON" ACCOUNT_JSON="$ACCOUNT_DIR/account.json" python3 - <<'PY'
import json, os, sys

brand_path = os.environ['BRAND_JSON']
account_path = os.environ['ACCOUNT_JSON']
plugins_dir = os.path.join(os.environ['PROJECT_DIR'], 'plugins')

with open(brand_path) as f:
    brand = json.load(f)

plugins_cfg = brand.get('plugins', {})
default_enabled = plugins_cfg.get('defaultEnabled', [])
core = set(plugins_cfg.get('core', []))

if not default_enabled:
    print('  [setup] no defaultEnabled plugins in brand.json — skipping')
    sys.exit(0)

satisfied = core | set(default_enabled)
validated = []
for plugin in default_enabled:
    plugin_md = os.path.join(plugins_dir, plugin, 'PLUGIN.md')
    requires = []
    if os.path.isfile(plugin_md):
        in_frontmatter = False
        in_requires = False
        with open(plugin_md) as f:
            for line in f:
                stripped = line.rstrip()
                if stripped == '---':
                    if in_frontmatter:
                        break
                    in_frontmatter = True
                    continue
                if not in_frontmatter:
                    continue
                if stripped.startswith('requires:'):
                    inline = stripped.split(':', 1)[1].strip()
                    if inline.startswith('['):
                        requires = [x.strip().strip('"').strip("'") for x in inline.strip('[]').split(',') if x.strip()]
                        in_requires = False
                    else:
                        in_requires = True
                    continue
                if in_requires:
                    if stripped.startswith('  - '):
                        requires.append(stripped.lstrip(' -').strip())
                    else:
                        in_requires = False
    unmet = [r for r in requires if r not in satisfied]
    if unmet:
        print(f'  [setup] WARNING: skipping defaultEnabled plugin "{plugin}" — unmet requires: {unmet}')
    else:
        validated.append(plugin)

with open(account_path) as f:
    account = json.load(f)

existing = account.get('enabledPlugins', [])
existing_set = set(existing)
added = [p for p in validated if p not in existing_set]
account['enabledPlugins'] = existing + added

with open(account_path, 'w') as f:
    json.dump(account, f, indent=2)
    f.write('\n')

if not existing:
    print(f'  [setup] seeded enabledPlugins: {validated}')
elif added:
    print(f'  [setup] merged into enabledPlugins: +{added} (existing: {existing})')
else:
    print(f'  [setup] enabledPlugins already contains all defaultEnabled entries')
PY
  fi

  # Task 999 — no admins[] owner stamp at provision time. account.json admins[]
  # is no longer the access authority: every authenticated install admin sees
  # every account. The old stamp wrote users.json[0] (the first user, not the
  # operating admin) into every new account, so a client account a non-first
  # admin created listed the wrong owner and stayed invisible to them. The
  # house account's owner record is authored durably by set-pin (writeAdminEntry
  # in onboarding) and account.json survives reinstalls, so nothing load-bearing
  # is lost here.

  echo "  Account $ACCOUNT_ID at $ACCOUNT_DIR (role=$ROLE)"
}
