#!/usr/bin/env bash
# ============================================================
# seed-neo4j.sh — Phase 0 setup
# Creates the account from templates and applies Neo4j schema.
# Product knowledge is ingested post-installation via the admin
# agent, not seeded as cypher.
# ============================================================

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"
NEO4J_DIR="$PROJECT_DIR/neo4j"

NEO4J_URI="${NEO4J_URI:-bolt://localhost:7687}"
NEO4J_USER="${NEO4J_USER:-neo4j}"

# Read password from the file created during setup
NEO4J_PASSWORD_FILE="$PROJECT_DIR/config/.neo4j-password"
if [ -n "${NEO4J_PASSWORD:-}" ]; then
  : # Explicit env var takes precedence
elif [ -f "$NEO4J_PASSWORD_FILE" ]; then
  NEO4J_PASSWORD=$(cat "$NEO4J_PASSWORD_FILE")
else
  echo "Error: Neo4j password not found."
  echo "  Expected at: $NEO4J_PASSWORD_FILE"
  echo "  Or set NEO4J_PASSWORD environment variable."
  exit 1
fi

CYPHER_SHELL="cypher-shell"

# ------------------------------------------------------------------
# 1. Create account from templates
# ------------------------------------------------------------------

# Resolve existing account or generate a new UUID.
# Re-runs must use the same ID — find any existing account directory.
mkdir -p "$ACCOUNTS_DIR"
EXISTING=$(find "$ACCOUNTS_DIR" -maxdepth 2 -name "account.json" | head -1)
if [ -n "$EXISTING" ]; then
  ACCOUNT_DIR="$(dirname "$EXISTING")"
  ACCOUNT_ID="$(basename "$ACCOUNT_DIR")"
else
  ACCOUNT_ID="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c 'import uuid; print(uuid.uuid4())')"
  ACCOUNT_DIR="$ACCOUNTS_DIR/$ACCOUNT_ID"
fi

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"

# Claude Code permissions — project-level settings now work since the account dir
# has .git. Write permissions to the account's own .claude/settings.json.
ACCOUNT_SETTINGS="$ACCOUNT_DIR/.claude/settings.json"

# Wildcard permissions for all plugin MCP servers (core + optional).
# Including optional plugins here is safe — permissions without a running
# MCP server are inert. This ensures tools are allowed when the user
# enables an optional plugin, without requiring a re-seed.
MAXY_PERMISSIONS='["Read","Edit","Write","Bash","Glob","Grep","Agent","mcp__memory__*","mcp__contacts__*","mcp__telegram__*","mcp__admin__*","mcp__cloudflare__*","mcp__tasks__*","mcp__scheduling__*","mcp__email__*","mcp__workflows__*","mcp__web__*","mcp__plugin_playwright_playwright__*"]'

# Append wildcard permissions for any optional plugins not already covered
PLUGINS_DIR="$(dirname "$0")/../plugins"
if [ -d "$PLUGINS_DIR" ]; then
  for PLUGIN_DIR in "$PLUGINS_DIR"/*/; do
    PLUGIN_NAME="$(basename "$PLUGIN_DIR")"
    # Skip plugins already in the static list
    case "$MAXY_PERMISSIONS" in
      *"mcp__${PLUGIN_NAME}__"*) continue ;;
    esac
    # Only add if plugin has an MCP server
    if [ -d "$PLUGIN_DIR/mcp" ]; then
      MAXY_PERMISSIONS="${MAXY_PERMISSIONS%]},\"mcp__${PLUGIN_NAME}__*\"]"
    fi
  done
fi

# Always overwrite — Rubytech-controlled. Project-level settings take
# precedence over global, so each account is properly isolated.
#
# 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.
#
# Hooks wired:
#   PreToolUse — pre-tool-use.sh: admin write boundaries
#                playwright-file-guard.sh: intercepts browser_navigate with file:// URLs
HOOKS_PATH="\$PLATFORM_ROOT/plugins/admin/hooks"
cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
{
  "permissions": {
    "allow": $MAXY_PERMISSIONS
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash $HOOKS_PATH/pre-tool-use.sh admin"
          }
        ]
      },
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash $HOOKS_PATH/pre-tool-use.sh admin"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash $HOOKS_PATH/pre-tool-use.sh admin"
          }
        ]
      },
      {
        "matcher": "mcp__plugin_playwright_playwright__browser_navigate",
        "hooks": [
          {
            "type": "command",
            "command": "bash $HOOKS_PATH/playwright-file-guard.sh"
          }
        ]
      }
    ]
  }
}
SETTINGS_EOF

# Always overwrite IDENTITY.md — Rubytech-controlled, pure behaviour.
cp "$TEMPLATES_DIR/agents/admin/IDENTITY.md" "$ACCOUNT_DIR/agents/admin/IDENTITY.md"
if [ -d "$ACCOUNT_DIR/agents/public" ]; then
  cp "$TEMPLATES_DIR/agents/public/IDENTITY.md" "$ACCOUNT_DIR/agents/public/IDENTITY.md"
else
  echo "  Skipping agents/public — directory does not exist (user has not created a public agent)"
fi

# 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"
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.
[ -f "$ACCOUNT_DIR/agents/admin/SOUL.md" ] || cp "$TEMPLATES_DIR/agents/admin/SOUL.md" "$ACCOUNT_DIR/agents/admin/SOUL.md"

# Public agent files — only provision if the user has created a public agent (directory exists).
# config.json is what makes the agent visible in the sidebar; creating it here would
# resurrect an agent the user deliberately deleted.
if [ -d "$ACCOUNT_DIR/agents/public" ]; then
  [ -f "$ACCOUNT_DIR/agents/public/SOUL.md" ] || cp "$TEMPLATES_DIR/agents/public/SOUL.md" "$ACCOUNT_DIR/agents/public/SOUL.md"
  [ -f "$ACCOUNT_DIR/agents/public/config.json" ] || cp "$TEMPLATES_DIR/agents/public/config.json" "$ACCOUNT_DIR/agents/public/config.json"
fi

# AGENTS.md — specialist registry. Pre-populated from templates, maintained by admin agent at runtime.
# Always overwrite on re-seed (Rubytech-controlled initial state, like IDENTITY.md).
# Dispatch format: specialists:{name} via the Agent tool.
printf '# Installed Specialists\n\nDispatch via the Agent tool with `subagent_type: "specialists:{name}"`.\n\n' > "$ACCOUNT_DIR/agents/admin/AGENTS.md"
for specialist in "$ACCOUNT_DIR/specialists/agents/"*.md; do
  [ -f "$specialist" ] || continue
  # Extract name and description from YAML frontmatter
  _name=$(sed -n 's/^name: *//p' "$specialist" | head -1)
  _desc=$(sed -n 's/^description: *"*//p' "$specialist" | sed 's/"$//' | head -1)
  [ -z "$_name" ] && continue
  # Skip Phase 1 stubs — not yet functional
  echo "$_desc" | grep -qi "Phase 1" && continue
  printf -- '- **specialists:%s**: %s\n' "$_name" "$_desc" >> "$ACCOUNT_DIR/agents/admin/AGENTS.md"
done

# 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

# Backfill defaultAgent in account.json if missing (existing accounts pre-Task 108).
# Uses the first non-admin agent directory that has a config.json.
if [ -f "$ACCOUNT_DIR/account.json" ] && ! grep -q '"defaultAgent"' "$ACCOUNT_DIR/account.json"; then
  _DEFAULT_SLUG=""
  for _AGENT_DIR in "$ACCOUNT_DIR/agents"/*/; do
    _AGENT_NAME="$(basename "$_AGENT_DIR")"
    [ "$_AGENT_NAME" = "admin" ] && continue
    [ -f "$_AGENT_DIR/config.json" ] && { _DEFAULT_SLUG="$_AGENT_NAME"; break; }
  done
  if [ -n "$_DEFAULT_SLUG" ]; then
    # Insert defaultAgent before the closing brace
    sed -i'' "s/}$/,\"defaultAgent\":\"$_DEFAULT_SLUG\"}/" "$ACCOUNT_DIR/account.json"
    echo "  Backfilled defaultAgent=$_DEFAULT_SLUG in account.json"
  fi
fi

# ------------------------------------------------------------------
# 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('  [seed] 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'  [seed] 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'  [seed] seeded enabledPlugins: {validated}')
elif added:
    print(f'  [seed] merged into enabledPlugins: +{added} (existing: {existing})')
else:
    print(f'  [seed] enabledPlugins already contains all defaultEnabled entries')
"
fi

echo "  Account $ACCOUNT_ID at $ACCOUNT_DIR"

# ------------------------------------------------------------------
# 1b. Create first admin user (migration from .admin-pin)
# ------------------------------------------------------------------

CONFIG_DIR="$PROJECT_DIR/config"
USERS_FILE="$CONFIG_DIR/users.json"

# Resolve the brand-specific config directory for .admin-pin location.
# Mirrors the logic in platform/ui/app/lib/paths.ts.
_CONFIG_DIR_NAME=".maxy"
if [ -f "$CONFIG_DIR/brand.json" ]; then
  _BRAND_CFG_DIR=$(python3 -c "import json; print(json.load(open('$CONFIG_DIR/brand.json')).get('configDir',''))" 2>/dev/null || true)
  [ -n "$_BRAND_CFG_DIR" ] && _CONFIG_DIR_NAME="$_BRAND_CFG_DIR"
fi
ADMIN_PIN_FILE="$HOME/$_CONFIG_DIR_NAME/.admin-pin"

# Only create users.json if it doesn't exist AND .admin-pin exists (migration case).
# Fresh installs: no .admin-pin yet (set during onboarding). The session route falls
# back to .admin-pin when users.json is absent — no migration needed until next seed.
if [ ! -f "$USERS_FILE" ] && [ -f "$ADMIN_PIN_FILE" ]; then
  USER_ID="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || python3 -c 'import uuid; print(uuid.uuid4())')"
  PIN_HASH="$(cat "$ADMIN_PIN_FILE")"

  # Create users.json with the first user entry.
  # PINs are stored as SHA-256 hashes — same format as .admin-pin.
  cat > "$USERS_FILE" << USERS_EOF
[{"userId":"$USER_ID","name":"Owner","pin":"$PIN_HASH"}]
USERS_EOF
  echo "  [seed] created initial user: userId=$USER_ID"

  # Add the user to account.json's admins array.
  # The template includes "admins": [] — populate it with the owner entry.
  if [ -f "$ACCOUNT_DIR/account.json" ]; then
    python3 -c "
import json, sys
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')
"
    echo "  Added userId=$USER_ID as owner to account.json admins"
  fi
elif [ -f "$USERS_FILE" ]; then
  # Re-run: users.json exists. Read the first user's ID for Neo4j seeding.
  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"

  # Backfill admins in account.json if missing (existing accounts pre-Task 248).
  if [ -f "$ACCOUNT_DIR/account.json" ] && ! grep -q '"admins"' "$ACCOUNT_DIR/account.json"; then
    python3 -c "
import json
with open('$ACCOUNT_DIR/account.json', 'r') as f:
    config = json.load(f)
config['admins'] = [{'userId': '$USER_ID', 'role': 'owner'}]
with open('$ACCOUNT_DIR/account.json', 'w') as f:
    json.dump(config, f, indent=2)
    f.write('\n')
"
    echo "  Backfilled admins array in account.json"
  fi
else
  echo "  No .admin-pin found — skipping users.json creation (fresh install)"
  USER_ID=""
fi

# ------------------------------------------------------------------
# 2. Apply Neo4j schema (constraints + indexes only)
# ------------------------------------------------------------------
if ! command -v "$CYPHER_SHELL" &> /dev/null; then
    echo "Error: cypher-shell not found. Install Neo4j or add cypher-shell to PATH."
    exit 1
fi

echo "==> Connecting to Neo4j at $NEO4J_URI as $NEO4J_USER"

# Migration: drop single-key UserProfile constraint (replaced by composite
# (accountId, userId) in Task 249). Also drop the old preference_category
# index — replaced by (accountId, userId, category) composite.
echo "==> Migrating schema: dropping single-key UserProfile constraint..."
"$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" << 'MIGRATE_EOF'
DROP CONSTRAINT user_profile_account_unique IF EXISTS;
DROP INDEX preference_category IF EXISTS;
MIGRATE_EOF

echo "==> Applying schema (constraints, indexes, vector indexes)..."
"$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" \
    -f "$NEO4J_DIR/schema.cypher"

# ------------------------------------------------------------------
# 3. Create AdminUser node + ADMIN_OF relationship
# ------------------------------------------------------------------

if [ -n "${USER_ID:-}" ]; then
  # Escape single quotes for Cypher string interpolation (e.g. O'Brien → O''Brien).
  USER_NAME=$(python3 -c "import json; n=json.load(open('$USERS_FILE'))[0]['name']; print(n.replace(\"'\",\"''\"))" 2>/dev/null || echo "Owner")
  CREATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)

  echo "==> Creating AdminUser node for userId=$USER_ID"
  "$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" << CYPHER_EOF
MERGE (au:AdminUser {userId: '$USER_ID'})
ON CREATE SET au.name = '$USER_NAME', au.createdAt = '$CREATED_AT'
ON MATCH SET au.name = '$USER_NAME'
WITH au
MATCH (b:LocalBusiness {accountId: '$ACCOUNT_ID'})
MERGE (au)-[r:ADMIN_OF]->(b)
ON CREATE SET r.role = 'owner', r.grantedAt = '$CREATED_AT'
RETURN au.userId, b.accountId;
CYPHER_EOF
  echo "  AdminUser node created/updated"

  # ------------------------------------------------------------------
  # 3b. Backfill orphaned conversations (Task 380)
  # ------------------------------------------------------------------
  # When userId was regenerated on every install, conversations created with
  # previous userIds became invisible. Reassign them to the current userId.
  # Scoped to this accountId. Only touches conversations where userId is set
  # but doesn't match the current user (NULL userId is Task 378's domain).
  echo "==> Backfilling orphaned admin conversations for userId=$USER_ID"
  BACKFILL_RESULT=$("$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" << BACKFILL_EOF
MATCH (c:Conversation {accountId: '$ACCOUNT_ID', agentType: 'admin'})
WHERE c.userId IS NOT NULL AND c.userId <> '$USER_ID'
WITH c, c.userId AS oldUserId
SET c.userId = '$USER_ID'
RETURN count(c) AS reassigned, collect(DISTINCT oldUserId) AS oldUserIds;
BACKFILL_EOF
  )
  echo "  [backfill] $BACKFILL_RESULT"
fi

echo "  Done."
