#!/usr/bin/env bash
# ============================================================
# setup-account.sh — Phase 0 account setup
# Creates the account directory from templates, writes the
# Claude Code project settings file (hooks only — no permission
# allowlist; native agent frontmatter is the permission surface),
# installs specialist plugin templates, seeds account.json
# defaults, and resolves the owner userId. Neo4j seeding lives
# in `seed-neo4j.sh`; this script does no Neo4j work.
# ============================================================

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
TEMPLATES_DIR="$PROJECT_DIR/templates"
# Accounts live at {installDir}/data/accounts/ — outside the platform/ wipe zone.
INSTALL_DIR="$(dirname "$PROJECT_DIR")"
ACCOUNTS_DIR="$INSTALL_DIR/data/accounts"

# ------------------------------------------------------------------
# 1. Resolve / create account directory
# ------------------------------------------------------------------
# Resolve existing account by identity (users.json[0].userId) or mint a new one on fresh install.
# Orphan siblings under data/accounts/ — non-matching dirs left behind by the userId
# regeneration — are swept to data/accounts/.trash/<uuid>-<ISO8601-ts>/. Multi-candidate with
# no identity match aborts loud; .trash/ is operator-emptied, never auto-cleaned.
# shellcheck source=lib/resolve-account-dir.sh
. "$SCRIPT_DIR/lib/resolve-account-dir.sh"
# shellcheck source=lib/read-brand-json.sh
. "$SCRIPT_DIR/lib/read-brand-json.sh"
# Resolve brand-aware persistent users.json path before the resolver runs.
_RESOLVER_CONFIG_DIR_NAME=".maxy"
read_brand_json_key "$PROJECT_DIR/config/brand.json" "configDir"
[ -n "$BRAND_JSON_VALUE" ] && _RESOLVER_CONFIG_DIR_NAME="$BRAND_JSON_VALUE"
USERS_FILE="$HOME/$_RESOLVER_CONFIG_DIR_NAME/users.json" resolve_and_sweep_account_dir

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

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

# ------------------------------------------------------------------
# 2. Write project-level .claude/settings.json (permissions + hooks)
# ------------------------------------------------------------------
# Task 453 removed `permissions.allow` because its short-prefix entries
# (mcp__memory__*, mcp__contacts__*, …) never matched the plugin-manifest
# long-prefix tool names Claude Code actually emits (mcp__plugin_memory_memory__…),
# so every Task-subagent plugin call was denied at use-time.
#
# Task 583 reinstates a `permissions` block — but as wildcard
# `allow: ["*"]` plus `defaultMode: "bypassPermissions"`. The wildcard
# bypasses the prefix-matching defect Task 453 was avoiding (a list of one
# entry that matches everything cannot mismatch), and `bypassPermissions`
# short-circuits Stage 1 of the local matcher so claude.ai/code sessions
# never fall through to the Anthropic remote auto-classifier (which routes
# Agent dispatch to `ask` and surfaces a permission prompt no unattended
# operator clicks). Per-agent frontmatter `tools:` lists remain the
# permission surface for what each subagent is allowed to invoke.
#
# Hook commands use $PLATFORM_ROOT (set by claude-agent.ts at spawn time)
# to locate scripts relative to the platform installation, not the account dir.
ACCOUNT_SETTINGS="$ACCOUNT_DIR/.claude/settings.json"
HOOKS_PATH="\$PLATFORM_ROOT/plugins/admin/hooks"
cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
{
  "permissions": {
    "defaultMode": "bypassPermissions",
    "allow": ["*"],
    "deny": []
  },
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/pin-identity-gate.sh" }
        ]
      },
      {
        "matcher": "Write",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
        ]
      },
      {
        "matcher": "Edit",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
        ]
      },
      {
        "matcher": "NotebookEdit",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-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" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Agent",
        "hooks": [
          { "type": "command", "command": "bash $HOOKS_PATH/post-tool-use-agent.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" }
        ]
      }
    ]
  }
}
SETTINGS_EOF

# ------------------------------------------------------------------
# 3. Install agent identities and specialist plugin templates
# ------------------------------------------------------------------
# Always overwrite IDENTITY.md — Rubytech-controlled, pure behaviour.
cp "$TEMPLATES_DIR/agents/admin/IDENTITY.md" "$ACCOUNT_DIR/agents/admin/IDENTITY.md"

# Task 618 — public agents are authored entirely client-side by the admin via
# public-agent-manager; the installer never creates a public agent dir or seeds
# its SOUL/KNOWLEDGE/config. On upgrade, re-sync the Rubytech-controlled
# IDENTITY.md for every already-existing non-admin agent dir (security-load-
# bearing toolless directive — Task 615). Never create dirs; never write
# SOUL/KNOWLEDGE/config.
for _AGENT_DIR in "$ACCOUNT_DIR/agents"/*/; do
  [ -d "$_AGENT_DIR" ] || continue
  _AGENT_NAME="$(basename "$_AGENT_DIR")"
  [ "$_AGENT_NAME" = "admin" ] && continue
  [ -f "$_AGENT_DIR/IDENTITY.md" ] || continue
  cp "$TEMPLATES_DIR/agents/public/IDENTITY.md" "$_AGENT_DIR/IDENTITY.md"
  echo "  agents/$_AGENT_NAME/IDENTITY.md re-synced"
done

# Always overwrite specialist plugin — Rubytech-controlled, not user-editable.
# Specialists are a Claude Code plugin loaded via --plugin-dir. The plugin lives at
# accountDir/specialists/ with .claude-plugin/plugin.json and agents/*.md.
# The admin agent dispatches them as specialists:{name} via the Agent tool.
cp "$TEMPLATES_DIR/specialists/.claude-plugin/plugin.json" "$ACCOUNT_DIR/specialists/.claude-plugin/plugin.json"

# Remove orphaned core specialist files before copying fresh templates.
# Core specialists (no '--' in filename) are Rubytech-controlled and fully replaced
# on every seed. Premium plugin agents (filename contains '--') are preserved —
# they are managed by the admin agent when plugins are activated/deactivated.
for _existing in "$ACCOUNT_DIR/specialists/agents/"*.md; do
  [ -f "$_existing" ] || continue
  case "$(basename "$_existing")" in
    *--*) continue ;;  # Premium plugin agent — preserve
  esac
  rm -f "$_existing"
done

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

# Clean up stale .claude/agents/*.md from previous seed format.
# The old format placed specialists here as slash-command agents — that never worked
# for programmatic dispatch. Safe to remove; .claude/ dir stays for settings.json.
rm -f "$ACCOUNT_DIR/.claude/agents/"*.md 2>/dev/null || true

# SOUL.md — user-controlled personalisation. 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

# AGENTS.md — specialist registry. Bootstrap on first seed; idempotent
# additive update on re-seed. Doctrine (Task 486): every core specialist
# under $ACCOUNT_DIR/specialists/agents/ has exactly one line in AGENTS.md;
# operator-tuned lines survive re-seed. Logic is factored into a helper so
# the same code path runs from the test suite.
bash "$SCRIPT_DIR/lib/agents-md-bootstrap.sh" "$ACCOUNT_DIR"

# ------------------------------------------------------------------
# 4. Create / backfill account.json
# ------------------------------------------------------------------
# Create account.json only if it doesn't exist (business config is in the graph).
# Reads defaults from the template so there is a single source of truth.
if [ ! -f "$ACCOUNT_DIR/account.json" ]; then
  TEMPLATE="$TEMPLATES_DIR/account.json"
  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
    exit 1
  fi
fi

# ------------------------------------------------------------------
# 5. Seed enabledPlugins from brand.json plugins.defaultEnabled
# ------------------------------------------------------------------
# Fresh accounts: replaces the template's empty [] with defaultEnabled.
# Existing accounts: additive merge — adds missing defaultEnabled entries,
# never removes plugins the user enabled manually.
# Dependency validation: skips any defaultEnabled plugin whose PLUGIN.md
# `requires:` dependencies are not satisfied by core or defaultEnabled.
BRAND_JSON="$PROJECT_DIR/config/brand.json"
if [ -f "$BRAND_JSON" ] && [ -f "$ACCOUNT_DIR/account.json" ]; then
  python3 -c "
import json, os, sys

brand_path = '$BRAND_JSON'
account_path = '$ACCOUNT_DIR/account.json'
plugins_dir = '$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)

# Dependency validation: check each plugin's PLUGIN.md requires field.
# A dependency is satisfied if it appears in core or in defaultEnabled.
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:'):
                    # Handle inline list: requires: [a, b]
                    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')
"
fi

# ------------------------------------------------------------------
# 6. Stamp first admin userId into account.json (if users.json present)
# ------------------------------------------------------------------
# users.json is created by the set-pin POST during onboarding. This
# script reads it to stamp the owner into account.json admins[]; the
# matching :AdminUser graph node is created by seed-neo4j.sh.
CONFIG_DIR="$PROJECT_DIR/config"
_CONFIG_DIR_NAME=".maxy"
read_brand_json_key "$CONFIG_DIR/brand.json" "configDir"
[ -n "$BRAND_JSON_VALUE" ] && _CONFIG_DIR_NAME="$BRAND_JSON_VALUE"
USERS_FILE="$HOME/$_CONFIG_DIR_NAME/users.json"
mkdir -p "$HOME/$_CONFIG_DIR_NAME"

if [ -f "$USERS_FILE" ]; then
  USER_ID=$(python3 -c "import json; print(json.load(open('$USERS_FILE'))[0]['userId'])" 2>/dev/null || true)
  [ -n "$USER_ID" ] && echo "  Existing users.json found, userId=$USER_ID"

  # Ensure account.json admins[] contains the owner. Idempotent.
  if [ -n "$USER_ID" ] && [ -f "$ACCOUNT_DIR/account.json" ]; then
    python3 -c "
import json
with open('$ACCOUNT_DIR/account.json', 'r') as f:
    config = json.load(f)
config.setdefault('admins', [])
if not any(a.get('userId') == '$USER_ID' for a in config['admins']):
    config['admins'].append({'userId': '$USER_ID', 'role': 'owner'})
    with open('$ACCOUNT_DIR/account.json', 'w') as f:
        json.dump(config, f, indent=2)
        f.write('\n')
    print('  Stamped userId=$USER_ID as owner in account.json admins')
"
  fi
else
  echo "  No users.json found — skipping owner stamp (set-pin POST creates it)"
fi

echo "  Account $ACCOUNT_ID at $ACCOUNT_DIR"
