#!/usr/bin/env bash
# install.sh — one-command installer for @memtensor/memos-local-plugin.
#
# Usage:
#   bash install.sh                        # install latest from npm
#   bash install.sh --version 2.0.0        # install specific npm version
#   bash install.sh --version ./pkg.tgz    # use a local tarball
#
# Interactive: with a TTY we ask where to install (OpenClaw / Hermes /
# both). Press ENTER for auto-detect. Non-TTY falls straight to
# auto-detect. macOS + Linux only.
#
# Design notes:
#   - Each agent runs its OWN viewer on its OWN well-known port:
#       openclaw → :18799
#       hermes   → :18800
#     Ports are intentionally fixed and not configurable by the
#     installer — having two agents share one port (the previous
#     "hub/peer" model) caused too many sharp edges (read-only
#     panels, dropped writes, mid-session ownership flips). Picking
#     a port at install time would also raise the question of
#     "which agent does this port belong to?" — we'd rather not
#     have that conversation.
#   - Each agent keeps its own SQLite DB under `~/.<agent>/memos-plugin/`.
#     There is no cross-agent memory in one UI; if both are installed
#     the root path on either viewer shows a small picker that links
#     to the other agent's port.
#   - All install logic is self-contained: Node bootstrap, tarball
#     resolution, better-sqlite3 rebuild, config patching, gateway
#     restart, viewer-readiness wait. No separate sub-scripts.

set -euo pipefail

# ─── Colors ────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

info()    { printf "  ${BLUE}›${NC} %b\n" "$*"; }
success() { printf "  ${GREEN}✔${NC} %b\n" "$*"; }
warn()    { printf "  ${YELLOW}⚠${NC}  %b\n" "$*" >&2; }
error()   { printf "  ${RED}✘${NC} %b\n" "$*" >&2; }
die()     { error "$*"; exit 1; }

header() {
  local text="$*"
  local pad_total=$((46 - ${#text}))
  (( pad_total < 0 )) && pad_total=0
  local padding=""
  local i; for ((i=0; i<pad_total; i++)); do padding+=" "; done
  echo
  printf "  ${BOLD}${BLUE}┌──────────────────────────────────────────────────┐${NC}\n"
  printf "  ${BOLD}${BLUE}│${NC}  ${BOLD}%s${NC}%s  ${BOLD}${BLUE}│${NC}\n" "${text}" "${padding}"
  printf "  ${BOLD}${BLUE}└──────────────────────────────────────────────────┘${NC}\n"
  echo
}

STEP_CURRENT=0
step() {
  STEP_CURRENT=$((STEP_CURRENT + 1))
  printf "  ${BOLD}${CYAN}[%d]${NC} %s\n" "${STEP_CURRENT}" "$*"
}

banner() {
  local ver="${VERSION_ARG:-latest}"
  echo
  printf "  ${BOLD}${BLUE}┌──────────────────────────────────────────────────┐${NC}\n"
  printf "  ${BOLD}${BLUE}│${NC}                                                  ${BOLD}${BLUE}│${NC}\n"
  printf "  ${BOLD}${BLUE}│${NC}   🧠  ${BOLD}MemOS Local Plugin Installer${NC}               ${BOLD}${BLUE}│${NC}\n"
  printf "  ${BOLD}${BLUE}│${NC}                                                  ${BOLD}${BLUE}│${NC}\n"
  printf "  ${BOLD}${BLUE}└──────────────────────────────────────────────────┘${NC}\n"
  printf "  ${DIM}Package: ${NPM_PACKAGE}  ·  Version: ${ver}${NC}\n"
  echo
}

# ─── Constants ─────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || pwd)"
PLUGIN_ID="memos-local-plugin"
NPM_PACKAGE="@memtensor/memos-local-plugin"
# Per-agent viewer ports are fixed (see header design notes).
OPENCLAW_PORT="18799"
HERMES_PORT="18800"
REQUIRED_NODE_MAJOR=20
OPENCLAW_RUNTIME_ENTRY="./dist/adapters/openclaw/index.js"
# Older plugin IDs disabled on install so they don't fight for the
# memory slot. We never touch the old plugin's data.
LEGACY_PLUGIN_IDS=("memos-local-openclaw-plugin")

# ─── Args ─────────────────────────────────────────────────────────────────
VERSION_ARG=""
AGENT_SELECTION=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --version) VERSION_ARG="${2:-}"; shift 2 ;;
    --agent|--target)
      AGENT_SELECTION="${2:-}"
      case "${AGENT_SELECTION}" in
        auto|openclaw|hermes|all) ;;
        *) die "--agent must be one of: auto, openclaw, hermes, all" ;;
      esac
      shift 2
      ;;
    --port)
      die "--port is no longer supported. Each agent uses a fixed port: \
openclaw → :${OPENCLAW_PORT}, hermes → :${HERMES_PORT}." ;;
    -h|--help)
      cat <<EOF
Usage:
  bash install.sh                                # latest from npm
  bash install.sh --version X.Y.Z                # specific npm version
  bash install.sh --version ./pkg.tgz            # local tarball
  bash install.sh --agent hermes                 # install one target
  bash install.sh --agent openclaw|hermes|all

Each agent runs its viewer on a fixed port:
  openclaw → http://127.0.0.1:${OPENCLAW_PORT}
  hermes   → http://127.0.0.1:${HERMES_PORT}
EOF
      exit 0
      ;;
    *) die "Unknown argument: $1 (only --version is supported)" ;;
  esac
done

# ─── Platform ─────────────────────────────────────────────────────────────
OS_NAME="$(uname -s)"
case "${OS_NAME}" in
  Darwin|Linux) ;;
  *) die "Unsupported platform: ${OS_NAME}. macOS and Linux only." ;;
esac

# ─── Node bootstrap ───────────────────────────────────────────────────────
node_major() {
  command -v node >/dev/null 2>&1 || { echo "0"; return; }
  node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1
}

download_to_file() {
  local url="$1" out="$2"
  if command -v curl >/dev/null 2>&1; then curl -fsSL "${url}" -o "${out}"; return $?; fi
  if command -v wget >/dev/null 2>&1; then wget -q "${url}" -O "${out}"; return $?; fi
  return 1
}

run_with_privilege() {
  if [[ "$(id -u)" -eq 0 ]]; then "$@"; else sudo "$@"; fi
}

install_node_mac() {
  command -v brew >/dev/null 2>&1 || die "Homebrew required on macOS. Install https://brew.sh first."
  info "Installing Node.js 22 via Homebrew..."
  brew install node@22 >/dev/null
  brew link node@22 --overwrite --force >/dev/null 2>&1 || true
  local p; p="$(brew --prefix node@22 2>/dev/null || true)"
  [[ -n "${p}" && -x "${p}/bin/node" ]] && export PATH="${p}/bin:${PATH}"
}

install_node_linux() {
  local tmp installer url
  tmp="$(mktemp)"
  if command -v apt-get >/dev/null 2>&1; then
    installer="apt"; url="https://deb.nodesource.com/setup_22.x"
  elif command -v dnf >/dev/null 2>&1; then
    installer="dnf"; url="https://rpm.nodesource.com/setup_22.x"
  elif command -v yum >/dev/null 2>&1; then
    installer="yum"; url="https://rpm.nodesource.com/setup_22.x"
  else
    die "No supported package manager. Install Node.js ≥ ${REQUIRED_NODE_MAJOR} manually."
  fi
  info "Installing Node.js 22 via ${installer}..."
  download_to_file "${url}" "${tmp}" || die "Failed to download Node setup script."
  run_with_privilege bash "${tmp}"
  case "${installer}" in
    apt) run_with_privilege apt-get update -qq && run_with_privilege apt-get install -y -qq nodejs ;;
    dnf) run_with_privilege dnf install -y -q nodejs ;;
    yum) run_with_privilege yum install -y -q nodejs ;;
  esac
  rm -f "${tmp}"
}

ensure_node() {
  local current; current="$(node_major)"
  if ! [[ "${current}" =~ ^[0-9]+$ ]] || (( current < REQUIRED_NODE_MAJOR )); then
    warn "Node.js >= ${REQUIRED_NODE_MAJOR} required (have ${current}). Auto-installing..."
    case "${OS_NAME}" in
      Darwin) install_node_mac ;;
      Linux)  install_node_linux ;;
    esac
    current="$(node_major)"
    [[ "${current}" =~ ^[0-9]+$ ]] && (( current >= REQUIRED_NODE_MAJOR )) \
      || die "Node.js install failed. Install ≥ ${REQUIRED_NODE_MAJOR} and re-run."
  fi

  # Node 25+ has no better-sqlite3 prebuilts → must compile. Warn the
  # user (but don't block; the rebuild step below tries regardless).
  if (( current >= 25 )); then
    warn "Node $(node -v) — no better-sqlite3 prebuild available, will compile from source."
    printf "       ${DIM}Tip: switch to Node LTS for prebuilt binaries:  nvm install 22${NC}\n" >&2
  fi
  success "Node.js $(node -v)"
}

# ─── Detect hosts ─────────────────────────────────────────────────────────
HAS_OPENCLAW="false"
HAS_HERMES="false"
[[ -d "${HOME}/.openclaw" ]] && HAS_OPENCLAW="true"
[[ -d "${HOME}/.hermes"   ]] && HAS_HERMES="true"

find_openclaw_cli() {
  command -v openclaw 2>/dev/null && return 0
  [[ -x "${HOME}/.local/bin/openclaw" ]] && { echo "${HOME}/.local/bin/openclaw"; return 0; }
  return 1
}

# ─── Interactive picker ───────────────────────────────────────────────────
pick_agents_interactively() {
  [[ -n "${AGENT_SELECTION}" ]] && return 0
  echo
  printf "  ${BOLD}Detected agents:${NC}\n"
  if [[ "${HAS_OPENCLAW}" == "true" ]]; then
    printf "    ${GREEN}●${NC}  OpenClaw   ${DIM}~/.openclaw${NC}\n"
  else
    printf "    ${DIM}○  OpenClaw   (not installed)${NC}\n"
  fi
  if [[ "${HAS_HERMES}" == "true" ]]; then
    printf "    ${GREEN}●${NC}  Hermes     ${DIM}~/.hermes${NC}\n"
  else
    printf "    ${DIM}○  Hermes     (not installed)${NC}\n"
  fi
  echo
  local choice
  if [[ ! -t 0 ]]; then
    info "Non-interactive mode — auto-detecting agents"
    choice=""
  else
    printf "  ${BOLD}Install into which agent?${NC}\n\n"
    printf "    ${DIM}[Enter]${NC}  🔍  Auto-detect\n"
    printf "        ${DIM}[1]${NC}  🦞  OpenClaw only\n"
    printf "        ${DIM}[2]${NC}  👩  Hermes only\n"
    printf "        ${DIM}[3]${NC}  📦  Both\n"
    printf "        ${DIM}[q]${NC}  🚪  Quit\n"
    echo
    printf "  Choice: "
    read -r choice || choice=""
  fi
  case "${choice}" in
    "")  AGENT_SELECTION="auto" ;;
    1)   AGENT_SELECTION="openclaw" ;;
    2)   AGENT_SELECTION="hermes" ;;
    3)   AGENT_SELECTION="all" ;;
    q|Q) info "Aborted."; exit 0 ;;
    *)   die "Invalid choice: ${choice}" ;;
  esac
}

# ─── Resolve tarball ──────────────────────────────────────────────────────
BUILT_TARBALL=""
STAGE_DIR=""
SOURCE_KIND=""   # "path" for a local file, "npm" otherwise
SOURCE_SPEC=""

resolve_tarball() {
  STAGE_DIR="$(mktemp -d)"
  trap 'rm -rf "${STAGE_DIR}"' EXIT

  if [[ -n "${VERSION_ARG}" && -f "${VERSION_ARG}" ]]; then
    BUILT_TARBALL="$(cd "$(dirname "${VERSION_ARG}")" && pwd)/$(basename "${VERSION_ARG}")"
    SOURCE_KIND="path"
    SOURCE_SPEC="${BUILT_TARBALL}"
    success "Using local tarball: ${BUILT_TARBALL}"
    return 0
  fi

  local spec
  if [[ -z "${VERSION_ARG}" ]]; then
    spec="${NPM_PACKAGE}"
    info "Downloading latest ${NPM_PACKAGE} from npm …"
  else
    spec="${NPM_PACKAGE}@${VERSION_ARG}"
    info "Downloading ${spec} from npm …"
  fi
  SOURCE_KIND="npm"
  SOURCE_SPEC="${spec}"

  (cd "${STAGE_DIR}" && npm pack "${spec}" --loglevel=error >/dev/null 2>&1)
  BUILT_TARBALL="$(ls "${STAGE_DIR}"/*.tgz 2>/dev/null | head -1)"
  [[ -n "${BUILT_TARBALL}" && -f "${BUILT_TARBALL}" ]] \
    || die "npm pack failed for ${spec}. Check the npm registry or pass a local path via --version ./pkg.tgz"
  success "Package ready: $(basename "${BUILT_TARBALL}")"
}

# ─── Deploy tarball into a prefix + rebuild native deps ───────────────────
#
# Hermes's layout puts the plugin source AND the runtime home in the same
# directory (${HOME}/.hermes/memos-plugin/). That means data/memos.db,
# config.yaml, logs/, skills/, daemon/, .auth.json all live next to the
# source files the tarball ships. A naive `rm -rf ${prefix}` would wipe
# the user's memory DB on every re-install.
#
# We mitigate that by preserving a well-known allowlist of user-data
# artefacts across the rm/extract cycle. node_modules is preserved too
# so npm install stays fast on re-install.
deploy_tarball_to_prefix() {
  local prefix="$1"
  step "Deploying to ${prefix}"
  local saved_dir=""
  local preserve=(node_modules data logs skills daemon config.yaml .auth.json .memos-node-bin)
  if [[ -d "${prefix}" ]]; then
    saved_dir="$(mktemp -d)"
    local item
    for item in "${preserve[@]}"; do
      if [[ -e "${prefix}/${item}" ]]; then
        mkdir -p "$(dirname "${saved_dir}/${item}")"
        mv "${prefix}/${item}" "${saved_dir}/${item}"
      fi
    done
    rm -rf "${prefix}"
    mkdir -p "${prefix}"
    tar xzf "${BUILT_TARBALL}" -C "${prefix}" --strip-components=1
    for item in "${preserve[@]}"; do
      if [[ -e "${saved_dir}/${item}" ]]; then
        rm -rf "${prefix}/${item}"
        mv "${saved_dir}/${item}" "${prefix}/${item}"
      fi
    done
    rm -rf "${saved_dir}"
  else
    mkdir -p "${prefix}"
    tar xzf "${BUILT_TARBALL}" -C "${prefix}" --strip-components=1
  fi
  [[ -f "${prefix}/package.json" ]] || die "Extraction failed: ${prefix}/package.json missing"
  success "Package extracted"

  step "Installing npm dependencies"
  local node_bin node_dir node_version
  node_bin="$(command -v node || true)"
  [[ -n "${node_bin}" && -x "${node_bin}" ]] || die "Node.js not found after bootstrap."
  node_dir="$(dirname "${node_bin}")"
  node_version="$("${node_bin}" -v 2>/dev/null || echo "unknown")"
  printf "%s\n" "${node_bin}" > "${prefix}/.memos-node-bin"
  ( cd "${prefix}" && PATH="${node_dir}:${PATH}" MEMOS_SKIP_SETUP=1 npm install --omit=dev --no-fund --no-audit --loglevel=error >/dev/null 2>&1 )
  [[ -d "${prefix}/node_modules" ]] || die "npm install failed in ${prefix}"

  if [[ -d "${prefix}/node_modules/better-sqlite3" ]]; then
    step "Rebuilding better-sqlite3 for Node ${node_version}"
    ( cd "${prefix}" && PATH="${node_dir}:${PATH}" npm rebuild better-sqlite3 --loglevel=error >/dev/null 2>&1 ) \
      || ( cd "${prefix}" && PATH="${node_dir}:${PATH}" npm rebuild better-sqlite3 --build-from-source --loglevel=error >/dev/null 2>&1 ) \
      || warn "better-sqlite3 rebuild did not complete cleanly."
    if ( cd "${prefix}" && "${node_bin}" -e "require('better-sqlite3')" >/dev/null 2>&1 ); then
      success "better-sqlite3 native module OK"
    else
      warn "better-sqlite3 not loadable — plugin will fail at startup."
      printf "       ${DIM}Fix: cd ${prefix} && PATH=${node_dir}:\$PATH npm rebuild better-sqlite3${NC}\n" >&2
    fi
  fi
  success "Dependencies ready"
}

# ─── Generate runtime config.yaml ─────────────────────────────────────────
# The template ships with the right per-agent port baked in
# (`templates/config.openclaw.yaml` → 18799,
#  `templates/config.hermes.yaml` → 18800), so we don't have to
# rewrite `port:` here. Existing files are left untouched.
ensure_runtime_home() {
  local agent="$1" home_dir="$2" prefix="$3"
  mkdir -p "${home_dir}/data" "${home_dir}/skills" "${home_dir}/logs" "${home_dir}/daemon"
  chmod 700 "${home_dir}"

  local template="${prefix}/templates/config.${agent}.yaml"
  [[ ! -f "${template}" ]] && template="${SCRIPT_DIR}/templates/config.${agent}.yaml"
  if [[ ! -f "${template}" ]]; then
    warn "Template missing: config.${agent}.yaml"
    return 0
  fi

  local target="${home_dir}/config.yaml"
  if [[ -f "${target}" ]]; then
    success "config.yaml exists — kept as-is"
  else
    cp "${template}" "${target}"
    chmod 600 "${target}"
    success "Wrote ${target} from template"
  fi
}

# ─── Wait for viewer — spin until HTTP endpoint actually responds ─────────
wait_for_viewer() {
  local port="$1"
  local url="http://127.0.0.1:${port}"
  local timeout="${2:-60}"
  local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
  local idx=0
  local elapsed=0
  local spin_tick=0

  while (( elapsed < timeout )); do
    if command -v curl >/dev/null 2>&1 && curl -fsS --max-time 1 "${url}/" >/dev/null 2>&1; then
      printf "\r\033[K"
      success "Memory Viewer is ready: ${CYAN}${url}${NC}"
      return 0
    fi
    printf "\r  ${BLUE}%s${NC}  Starting Memory Viewer ${DIM}(%ds)${NC} …" "${frames[idx]}" "${elapsed}"
    idx=$(((idx + 1) % ${#frames[@]}))
    sleep 0.12
    spin_tick=$((spin_tick + 1))
    if (( spin_tick % 8 == 0 )); then
      elapsed=$((elapsed + 1))
    fi
  done
  printf "\r\033[K"
  warn "Memory Viewer not ready after ${timeout}s"
  warn "Check: ${CYAN}${url}${NC}  Logs: ~/.openclaw/logs/ or ~/.hermes/memos-plugin/logs/"
  return 1
}

# ─── OpenClaw install ─────────────────────────────────────────────────────
install_openclaw() {
  STEP_CURRENT=0
  header "OpenClaw Install"
  local prefix="${HOME}/.openclaw/extensions/${PLUGIN_ID}"
  local home="${HOME}/.openclaw/memos-plugin"
  local config_path="${HOME}/.openclaw/openclaw.json"
  mkdir -p "${HOME}/.openclaw"

  local oc_bin=""
  if oc_bin="$(find_openclaw_cli)"; then
    step "Stopping OpenClaw gateway"
    "${oc_bin}" gateway stop >/dev/null 2>&1 || true
    sleep 1
    success "Gateway stopped"
  fi

  deploy_tarball_to_prefix "${prefix}"
  local runtime_entry="${prefix}/${OPENCLAW_RUNTIME_ENTRY#./}"
  [[ -f "${runtime_entry}" ]] \
    || die "OpenClaw runtime entry missing: ${OPENCLAW_RUNTIME_ENTRY}. Reinstall a package built with dist/ runtime output."

  step "Configuring runtime environment"
  ensure_runtime_home "openclaw" "${home}" "${prefix}"

  # 4. OpenClaw loads plugins via two artefacts:
  #      (a) package.json::openclaw — cheap metadata (we ship it in tgz)
  #      (b) openclaw.plugin.json   — full manifest (id, kind, configSchema,
  #          extensions)
  #    (b) is generated here so the user never edits it by hand.
  local plugin_version
  plugin_version="$(node -p "require('${prefix}/package.json').version" 2>/dev/null || echo 'unknown')"
  cat > "${prefix}/openclaw.plugin.json" <<EOF
{
  "id": "${PLUGIN_ID}",
  "name": "MemOS Local Memory (V7)",
  "description": "Reflect2Evolve V7 memory — L1/L2/L3 + skill crystallization + tier 1/2/3 retrieval + decision repair.",
  "kind": "memory",
  "version": "${plugin_version}",
  "homepage": "https://github.com/MemTensor/MemOS",
  "requirements": { "node": ">=${REQUIRED_NODE_MAJOR}.0.0" },
  "extensions": ["${OPENCLAW_RUNTIME_ENTRY}"],
  "contracts": {
    "tools": [
      "memos_search",
      "memos_get",
      "memos_timeline",
      "memos_skill_list",
      "memos_environment",
      "memos_skill_get"
    ]
  },
  "configSchema": {
    "type": "object",
    "additionalProperties": true,
    "description": "Edit ${home}/config.yaml to tune LLM / embedding / viewer.",
    "properties": {
      "viewerPort": { "type": "number", "description": "Memory Viewer HTTP port (default ${OPENCLAW_PORT})" }
    }
  }
}
EOF

  step "Patching ${config_path}"
  PLUGIN_ID="${PLUGIN_ID}" \
  INSTALL_PATH="${prefix}" \
  SOURCE_KIND="${SOURCE_KIND}" \
  SOURCE_SPEC="${SOURCE_SPEC}" \
  PLUGIN_VERSION="${plugin_version}" \
  LEGACY_JSON="$(printf '%s,' "${LEGACY_PLUGIN_IDS[@]}")" \
  CONFIG_PATH="${config_path}" \
  node - <<'NODE'
const fs = require('fs');
const {
  CONFIG_PATH: configPath, PLUGIN_ID: pluginId, INSTALL_PATH: installPath,
  SOURCE_KIND: sourceKind, SOURCE_SPEC: sourceSpec,
  PLUGIN_VERSION: pluginVersion, LEGACY_JSON: legacyCsv,
} = process.env;
const legacyIds = (legacyCsv || '').split(',').filter(Boolean);
const MEMOS_TOOL_NAMES = [
  'memos_search',
  'memos_get',
  'memos_timeline',
  'memos_environment',
  'memos_skill_list',
  'memos_skill_get',
];

let config = {};
if (fs.existsSync(configPath)) {
  const raw = fs.readFileSync(configPath, 'utf8').trim();
  if (raw) {
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) config = parsed;
  }
}

if (!config.gateway || typeof config.gateway !== 'object' || Array.isArray(config.gateway)) {
  config.gateway = {};
}
if (!config.gateway.mode) config.gateway.mode = 'local';

if (!config.tools || typeof config.tools !== 'object' || Array.isArray(config.tools)) {
  config.tools = {};
}
if (!Array.isArray(config.tools.alsoAllow)) config.tools.alsoAllow = [];
for (const toolName of MEMOS_TOOL_NAMES) {
  if (!config.tools.alsoAllow.includes(toolName)) config.tools.alsoAllow.push(toolName);
}

if (!config.plugins || typeof config.plugins !== 'object' || Array.isArray(config.plugins)) {
  config.plugins = {};
}
config.plugins.enabled = true;

if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
if (!config.plugins.allow.includes(pluginId)) config.plugins.allow.push(pluginId);

// Remove legacy plugins cleanly (OpenClaw schema rejects unknown keys,
// so we can't just tag them as disabled). The plugin directory on disk
// at ~/.openclaw/extensions/<legacy-id>/ is left untouched; the user
// can delete it themselves if desired.
for (const legacyId of legacyIds) {
  if (config.plugins.entries?.[legacyId]) delete config.plugins.entries[legacyId];
  if (config.plugins.installs?.[legacyId]) delete config.plugins.installs[legacyId];
  if (Array.isArray(config.plugins.allow)) {
    config.plugins.allow = config.plugins.allow.filter((x) => x !== legacyId);
  }
  if (config.plugins.slots && typeof config.plugins.slots === 'object') {
    for (const [slot, v] of Object.entries(config.plugins.slots)) {
      if (v === legacyId) delete config.plugins.slots[slot];
    }
  }
}

if (!config.plugins.slots || typeof config.plugins.slots !== 'object') config.plugins.slots = {};
config.plugins.slots.memory = pluginId;

if (!config.plugins.entries || typeof config.plugins.entries !== 'object') config.plugins.entries = {};
if (!config.plugins.entries[pluginId] || typeof config.plugins.entries[pluginId] !== 'object') {
  config.plugins.entries[pluginId] = {};
}
config.plugins.entries[pluginId].enabled = true;
// OpenClaw blocks conversation-level typed hooks for non-bundled plugins
// unless the user config explicitly grants access. The memory plugin needs
// agent_end to capture completed turns.
if (
  !config.plugins.entries[pluginId].hooks ||
  typeof config.plugins.entries[pluginId].hooks !== 'object' ||
  Array.isArray(config.plugins.entries[pluginId].hooks)
) {
  config.plugins.entries[pluginId].hooks = {};
}
config.plugins.entries[pluginId].hooks.allowConversationAccess = true;

if (!config.plugins.installs || typeof config.plugins.installs !== 'object') config.plugins.installs = {};
const installsEntry = {
  source: sourceKind === 'path' ? 'path' : 'npm',
  installPath,
  version: pluginVersion,
  resolvedVersion: pluginVersion,
  installedAt: new Date().toISOString(),
};
if (sourceKind !== 'path') {
  installsEntry.spec = sourceSpec;
  installsEntry.resolvedName = '@memtensor/memos-local-plugin';
  installsEntry.resolvedSpec = sourceSpec;
}
config.plugins.installs[pluginId] = installsEntry;

fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
NODE
  success "openclaw.json patched"

  if [[ -z "${oc_bin}" ]]; then
    warn "openclaw CLI not on PATH — restart manually: openclaw gateway start"
    return 1
  fi
  step "Starting OpenClaw gateway"
  local start_out
  if ! start_out="$("${oc_bin}" gateway start 2>&1)"; then
    # launchd KeepAlive may have already restarted the service after
    # the stop above, making "gateway start" fail with a kickstart
    # conflict. Check if the gateway is actually running before
    # treating this as a real error.
    if curl -fsS --max-time 2 "http://127.0.0.1:18789" >/dev/null 2>&1 \
       || (command -v lsof >/dev/null 2>&1 && lsof -i ":18789" -t >/dev/null 2>&1); then
      success "OpenClaw gateway already running"
    else
      error "openclaw gateway start failed:"
      echo "${start_out}" | sed 's/^/       /' >&2
      warn "Inspect ~/.openclaw/logs/gateway.err.log for the full reason."
      return 1
    fi
  else
    success "OpenClaw gateway started"
  fi

  step "Waiting for Memory Viewer"
  if wait_for_viewer "${OPENCLAW_PORT}"; then
    echo
    success "OpenClaw install complete"
    printf "       ${DIM}Plugin:${NC}    %s\n" "${HOME}/.openclaw/extensions/${PLUGIN_ID}"
    printf "       ${DIM}Viewer:${NC}    ${CYAN}http://127.0.0.1:${OPENCLAW_PORT}/${NC}\n"
    return 0
  fi

  warn "Memory Viewer did not respond after service start; trying foreground gateway mode."
  nohup "${oc_bin}" gateway >/tmp/openclaw-memos-gateway.log 2>&1 &
  sleep 2
  if wait_for_viewer "${OPENCLAW_PORT}"; then
    echo
    success "OpenClaw install complete"
    printf "       ${DIM}Plugin:${NC}    %s\n" "${HOME}/.openclaw/extensions/${PLUGIN_ID}"
    printf "       ${DIM}Viewer:${NC}    ${CYAN}http://127.0.0.1:${OPENCLAW_PORT}/${NC}\n"
    return 0
  fi

  warn "Memory Viewer did not respond within 60s."
  printf "       ${DIM}Check: /tmp/openclaw-memos-gateway.log or /tmp/openclaw/openclaw-*.log${NC}\n" >&2
  return 1
}

# ─── Hermes install ───────────────────────────────────────────────────────
install_hermes() {
  STEP_CURRENT=0
  header "Hermes Install"
  local prefix="${HOME}/.hermes/memos-plugin"
  local home="${prefix}"
  local config_file="${HOME}/.hermes/config.yaml"
  local adapter_dir="${prefix}/adapters/hermes"
  mkdir -p "${HOME}/.hermes"

  step "Stopping existing bridge daemon"
  local bridge_pids=""
  bridge_pids="$(pgrep -f "bridge\\.(cts|cjs)" 2>/dev/null || true)"
  if [[ -n "${bridge_pids}" ]]; then
    kill ${bridge_pids} >/dev/null 2>&1 || true
    local i
    for i in {1..10}; do
      sleep 1
      pgrep -f "bridge\\.(cts|cjs)" >/dev/null 2>&1 || break
    done
    bridge_pids="$(pgrep -f "bridge\\.(cts|cjs)" 2>/dev/null || true)"
    if [[ -n "${bridge_pids}" ]]; then
      kill -9 ${bridge_pids} >/dev/null 2>&1 || true
      sleep 1
    fi
  fi
  success "Bridge daemon stopped"
  local was_running="false"
  if pgrep -f "/bin/hermes" >/dev/null 2>&1; then
    step "Stopping running Hermes process"
    pkill -f "/bin/hermes" >/dev/null 2>&1 || true
    sleep 2
    pgrep -f "/bin/hermes" >/dev/null 2>&1 && pkill -9 -f "/bin/hermes" >/dev/null 2>&1 || true
    was_running="true"
    success "Hermes stopped"
  fi

  # Free Hermes' viewer port if something (e.g. a stale bridge from
  # a prior install, or the OpenClaw gateway reload) left it occupied.
  if command -v lsof >/dev/null 2>&1; then
    local stale_pid
    stale_pid="$(lsof -i ":${HERMES_PORT}" -t 2>/dev/null || true)"
    if [[ -n "${stale_pid}" ]]; then
      kill ${stale_pid} >/dev/null 2>&1 || true
      sleep 1
    fi
  fi

  deploy_tarball_to_prefix "${prefix}"

  step "Configuring runtime environment"
  ensure_runtime_home "hermes" "${home}" "${prefix}"

  local bridge_entry="${prefix}/dist/bridge.cjs"
  [[ -f "${bridge_entry}" ]] || bridge_entry="${prefix}/bridge.cts"
  echo "${bridge_entry}" > "${adapter_dir}/bridge_path.txt"
  success "Bridge path recorded"

  step "Locating Hermes Python environment"
  local python_bin=""
  if command -v hermes >/dev/null 2>&1; then
    local shebang; shebang="$(head -1 "$(command -v hermes)" 2>/dev/null || true)"
    [[ "${shebang}" == "#!"*python* ]] && python_bin="$(echo "${shebang}" | sed 's/^#!\s*//')"
  fi
  if [[ -z "${python_bin}" || ! -x "${python_bin}" ]] \
     && [[ -x "${HOME}/.hermes/hermes-agent/venv/bin/python3" ]]; then
    python_bin="${HOME}/.hermes/hermes-agent/venv/bin/python3"
  fi
  [[ -z "${python_bin}" || ! -x "${python_bin}" ]] && python_bin="$(command -v python3 || true)"
  [[ -n "${python_bin}" && -x "${python_bin}" ]] || die "Cannot locate Python for Hermes."
  success "Python: ${python_bin}"

  local plugin_dir=""
  plugin_dir="$("${python_bin}" -c "
from pathlib import Path
try:
    import plugins.memory as pm
    print(Path(pm.__file__).parent)
except Exception:
    pass
" 2>/dev/null || true)"
  if [[ -z "${plugin_dir}" || ! -d "${plugin_dir}" ]]; then
    for d in "${HOME}/.hermes/hermes-agent/plugins/memory"; do
      [[ -d "${d}" && -f "${d}/__init__.py" ]] && { plugin_dir="${d}"; break; }
    done
  fi
  [[ -n "${plugin_dir}" && -d "${plugin_dir}" ]] || die "plugins/memory not found"
  success "plugins/memory: ${plugin_dir}"

  step "Linking memtensor provider"
  local target="${plugin_dir}/memtensor"
  if [[ -L "${target}" ]]; then rm "${target}"
  elif [[ -e "${target}" ]]; then rm -rf "${target}"
  fi
  ln -s "${adapter_dir}/memos_provider" "${target}"
  cp "${adapter_dir}/plugin.yaml" "${adapter_dir}/memos_provider/plugin.yaml" 2>/dev/null || true
  success "Symlinked → ${target}"

  step "Verifying provider & patching config"
  local verify
  verify="$("${python_bin}" -c "
from plugins.memory import load_memory_provider
p = load_memory_provider('memtensor')
print('OK' if p and p.name == 'memtensor' else 'FAIL')
" 2>/dev/null || true)"
  [[ "${verify}" == "OK" ]] && success "Provider verification passed" \
    || warn "Provider verification didn't return OK"

  step "Installing Hermes profile defaults hook"
  "${python_bin}" - <<'PYEOF' || warn "Hermes profile defaults hook install failed"
import site
from pathlib import Path

site_dirs = site.getsitepackages()
if not site_dirs:
    raise SystemExit("no site-packages directory found")
site_dir = Path(site_dirs[0])
site_dir.mkdir(parents=True, exist_ok=True)

module_path = site_dir / "memos_hermes_profile_defaults.py"
module_path.write_text(
    r'''
"""MemOS profile defaults for Hermes.

This module is imported from a .pth file in the Hermes Python environment.
It wraps hermes_cli.profiles.create_profile so profiles created after the
MemOS plugin is installed inherit the memtensor memory provider even when the
user runs bare `hermes profile create <name>` without --clone.
"""

from __future__ import annotations

import importlib
import importlib.abc
import importlib.machinery
import sys
from pathlib import Path
from typing import Any

try:
    import yaml
except Exception:  # pragma: no cover
    yaml = None  # type: ignore[assignment]


def _patch_config(profile_dir: Any) -> None:
    if yaml is None:
        return
    path = Path(profile_dir) / "config.yaml"
    if path.exists():
        with path.open() as f:
            cfg = yaml.safe_load(f) or {}
    else:
        cfg = {}
    if not isinstance(cfg, dict):
        cfg = {}

    mem = cfg.get("memory")
    if not isinstance(mem, dict):
        mem = {}
        cfg["memory"] = mem
    mem["provider"] = "memtensor"
    mem.setdefault("memory_enabled", True)
    mem.setdefault("user_profile_enabled", True)

    plugins = cfg.get("plugins")
    if not isinstance(plugins, dict):
        plugins = {}
        cfg["plugins"] = plugins
    enabled = plugins.get("enabled")
    if enabled is True:
        enabled = ["memtensor"]
    elif isinstance(enabled, list):
        enabled = [item for item in enabled if item != "memtensor"]
        enabled.append("memtensor")
    else:
        enabled = ["memtensor"]
    plugins["enabled"] = enabled

    disabled = plugins.get("disabled")
    if isinstance(disabled, list):
        plugins["disabled"] = [item for item in disabled if item != "memtensor"]

    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w") as f:
        yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True, sort_keys=False)


def _wrap_profiles_module(module: Any) -> None:
    if getattr(module, "_memos_profile_defaults_wrapped", False):
        return
    original = getattr(module, "create_profile", None)
    if not callable(original):
        return

    def create_profile(*args: Any, **kwargs: Any) -> Any:
        profile_dir = original(*args, **kwargs)
        try:
            _patch_config(profile_dir)
        except Exception:
            pass
        return profile_dir

    module.create_profile = create_profile
    module._memos_profile_defaults_wrapped = True


class _ProfilesImportHook(importlib.abc.MetaPathFinder):
    _target = "hermes_cli.profiles"

    def find_spec(self, fullname: str, path: Any = None, target: Any = None) -> Any:
        if fullname != self._target:
            return None
        for finder in sys.meta_path:
            if finder is self:
                continue
            spec = finder.find_spec(fullname, path, target) if hasattr(finder, "find_spec") else None
            if spec and spec.loader:
                spec.loader = _ProfilesLoader(spec.loader)
                return spec
        return None


class _ProfilesLoader(importlib.abc.Loader):
    def __init__(self, loader: Any) -> None:
        self.loader = loader

    def create_module(self, spec: Any) -> Any:
        if hasattr(self.loader, "create_module"):
            return self.loader.create_module(spec)
        return None

    def exec_module(self, module: Any) -> None:
        self.loader.exec_module(module)
        _wrap_profiles_module(module)


existing = sys.modules.get("hermes_cli.profiles")
if existing is not None:
    _wrap_profiles_module(existing)
elif not any(isinstance(finder, _ProfilesImportHook) for finder in sys.meta_path):
    sys.meta_path.insert(0, _ProfilesImportHook())
'''.lstrip(),
    encoding="utf-8",
)

pth_path = site_dir / "memos_hermes_profile_defaults.pth"
pth_path.write_text("import memos_hermes_profile_defaults\n", encoding="utf-8")
print(module_path)
print(pth_path)
PYEOF
  success "Hermes profile defaults hook installed"

  if [[ -f "${config_file}" ]]; then
    local patched_configs
    patched_configs="$("${python_bin}" - "${HOME}/.hermes" 2>/dev/null <<'PYEOF'
import sys
from pathlib import Path

import yaml

hermes_home = Path(sys.argv[1])
paths = [hermes_home / "config.yaml"]
profiles_dir = hermes_home / "profiles"
if profiles_dir.is_dir():
    paths.extend(sorted(profiles_dir.glob("*/config.yaml")))

patched: list[str] = []
for path in paths:
    if not path.is_file():
        continue
    with path.open() as f:
        cfg = yaml.safe_load(f) or {}
    if not isinstance(cfg, dict):
        cfg = {}

    mem = cfg.get("memory")
    if not isinstance(mem, dict):
        mem = {}
        cfg["memory"] = mem
    mem["provider"] = "memtensor"
    mem.setdefault("memory_enabled", True)
    mem.setdefault("user_profile_enabled", True)

    plugins = cfg.get("plugins")
    if not isinstance(plugins, dict):
        plugins = {}
        cfg["plugins"] = plugins
    enabled = plugins.get("enabled")
    if enabled is True:
        enabled = ["memtensor"]
    elif isinstance(enabled, list):
        enabled = [item for item in enabled if item != "memtensor"]
        enabled.append("memtensor")
    else:
        enabled = ["memtensor"]
    plugins["enabled"] = enabled

    disabled = plugins.get("disabled")
    if isinstance(disabled, list):
        plugins["disabled"] = [item for item in disabled if item != "memtensor"]

    with path.open("w") as f:
        yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
    patched.append(str(path))

print("\n".join(patched))
PYEOF
)" || warn "Hermes config auto-patch failed"
    if [[ -n "${patched_configs}" ]]; then
      success "Hermes configs patched:"
      while IFS= read -r patched_config; do
        [[ -n "${patched_config}" ]] && printf "       ${DIM}%s${NC}\n" "${patched_config}"
      done <<< "${patched_configs}"
    fi
  else
    cat > "${config_file}" <<'CFGEOF'
memory:
  memory_enabled: true
  user_profile_enabled: true
  provider: memtensor
plugins:
  enabled:
  - memtensor
CFGEOF
    success "Created ${config_file}"
  fi

  # Smoke test — boot the bridge briefly and confirm the viewer
  # actually answers on Hermes' fixed port.
  if command -v lsof >/dev/null 2>&1 && lsof -i ":${HERMES_PORT}" -t >/dev/null 2>&1; then
    warn "Port :${HERMES_PORT} already in use — skipping smoke test."
  else
    step "Starting Memory Viewer daemon"
    local node_bin
    node_bin="$(cat "${prefix}/.memos-node-bin" 2>/dev/null || command -v node || true)"
    local tsx_bin="${prefix}/node_modules/tsx/dist/cli.mjs"
    local bridge_cts="${prefix}/bridge.cts"
    local bridge_cjs="${prefix}/dist/bridge.cjs"
    local bridge_entry="${bridge_cjs}"
    [[ -f "${bridge_entry}" ]] || bridge_entry="${bridge_cts}"
    if [[ -n "${node_bin}" && -x "${node_bin}" && -f "${bridge_entry}" && ( "${bridge_entry}" == *.cjs || -f "${tsx_bin}" ) ]]; then
      local daemon_log="${prefix}/logs/daemon-start.log"
      mkdir -p "${prefix}/logs"
      # Launch bridge in --daemon mode (pure HTTP, no stdio).
      # The process stays alive to serve the Memory Viewer.
      if [[ "${bridge_entry}" == *.cjs ]]; then
        ( cd "${prefix}" && nohup "${node_bin}" "${bridge_entry}" --agent=hermes --daemon >"${daemon_log}" 2>&1 & )
      else
        ( cd "${prefix}" && nohup "${node_bin}" "${tsx_bin}" "${bridge_entry}" --agent=hermes --daemon >"${daemon_log}" 2>&1 & )
      fi

      if wait_for_viewer "${HERMES_PORT}" 120; then
        success "Memory Viewer daemon running"
      else
        error "Memory Viewer did not respond within 120s."
        warn "Re-install dependencies and re-run: cd ${prefix} && npm install"
        return 1
      fi
    else
      warn "node or bridge runtime not found — skipping daemon start."
    fi
  fi

  echo
  success "Hermes install complete"
  printf "       ${DIM}Plugin:${NC}    %s\n" "${prefix}"
  printf "       ${DIM}Viewer:${NC}    ${CYAN}http://127.0.0.1:${HERMES_PORT}/${NC}\n"
  if [[ "${was_running}" == "true" ]]; then
    printf "       ${DIM}Next:${NC}      ${BOLD}hermes chat${NC}  ${DIM}(was stopped — relaunch to apply)${NC}\n"
  else
    printf "       ${DIM}Next:${NC}      ${BOLD}hermes chat${NC}\n"
  fi
  return 0
}

# ─── Main ─────────────────────────────────────────────────────────────────
banner
pick_agents_interactively

if [[ "${AGENT_SELECTION}" == "auto" ]]; then
  if [[ "${HAS_OPENCLAW}" != "true" && "${HAS_HERMES}" != "true" ]]; then
    die "Neither ~/.openclaw nor ~/.hermes exists. Install OpenClaw or Hermes first."
  fi
  if [[ "${HAS_OPENCLAW}" == "true" && "${HAS_HERMES}" == "true" ]]; then
    AGENT_SELECTION="all"
  elif [[ "${HAS_OPENCLAW}" == "true" ]]; then
    AGENT_SELECTION="openclaw"
  else
    AGENT_SELECTION="hermes"
  fi
  success "Auto-detected: ${AGENT_SELECTION}"
fi

case "${AGENT_SELECTION}" in
  openclaw) [[ "${HAS_OPENCLAW}" == "true" ]] || warn "~/.openclaw missing — will create." ;;
  hermes)   [[ "${HAS_HERMES}"   == "true" ]] || die  "~/.hermes missing — install Hermes first." ;;
  all) ;;
  *) die "Invalid selection: ${AGENT_SELECTION}" ;;
esac

ensure_node
resolve_tarball

STATUS=0
case "${AGENT_SELECTION}" in
  openclaw) install_openclaw || STATUS=1 ;;
  hermes)   install_hermes   || STATUS=1 ;;
  all)
    if [[ "${HAS_OPENCLAW}" == "true" ]]; then install_openclaw || STATUS=1; else warn "Skipping OpenClaw (~/.openclaw not found)"; fi
    if [[ "${HAS_HERMES}"   == "true" ]]; then install_hermes   || STATUS=1; else warn "Skipping Hermes (~/.hermes not found)"; fi
    ;;
esac

echo
if (( STATUS == 0 )); then
  echo
  printf "  ${BOLD}${GREEN}┌──────────────────────────────────────────────────┐${NC}\n"
  printf "  ${BOLD}${GREEN}│${NC}                                                  ${BOLD}${GREEN}│${NC}\n"
  printf "  ${BOLD}${GREEN}│${NC}   ✨  ${BOLD}${GREEN}MemOS Local installed successfully${NC}         ${BOLD}${GREEN}│${NC}\n"
  printf "  ${BOLD}${GREEN}│${NC}                                                  ${BOLD}${GREEN}│${NC}\n"
  printf "  ${BOLD}${GREEN}└──────────────────────────────────────────────────┘${NC}\n"
  echo
  case "${AGENT_SELECTION}" in
    openclaw)
      printf "  ${BOLD}Quick links:${NC}\n"
      printf "    ${GREEN}●${NC}  Memory Viewer   ${CYAN}http://127.0.0.1:${OPENCLAW_PORT}${NC}  ${DIM}(openclaw)${NC}\n"
      printf "    ${GREEN}●${NC}  OpenClaw Web UI  ${CYAN}http://localhost:18789${NC}\n"
      ;;
    hermes)
      printf "  ${BOLD}Quick links:${NC}\n"
      printf "    ${GREEN}●${NC}  Memory Viewer   ${CYAN}http://127.0.0.1:${HERMES_PORT}${NC}  ${DIM}(hermes)${NC}\n"
      ;;
    all)
      printf "  ${BOLD}Quick links:${NC}\n"
      printf "    ${GREEN}●${NC}  Memory Viewer   ${CYAN}http://127.0.0.1:${OPENCLAW_PORT}${NC}  ${DIM}(openclaw)${NC}\n"
      printf "    ${GREEN}●${NC}  Memory Viewer   ${CYAN}http://127.0.0.1:${HERMES_PORT}${NC}  ${DIM}(hermes)${NC}\n"
      printf "    ${GREEN}●${NC}  OpenClaw Web UI  ${CYAN}http://localhost:18789${NC}\n"
      ;;
  esac
  echo
  printf "  ${DIM}Docs: https://github.com/MemTensor/MemOS${NC}\n"
  echo
  exit 0
else
  echo
  printf "  ${BOLD}${RED}┌──────────────────────────────────────────────────┐${NC}\n"
  printf "  ${BOLD}${RED}│${NC}                                                  ${BOLD}${RED}│${NC}\n"
  printf "  ${BOLD}${RED}│${NC}   ${RED}Install finished with errors - see above${NC}       ${BOLD}${RED}│${NC}\n"
  printf "  ${BOLD}${RED}│${NC}                                                  ${BOLD}${RED}│${NC}\n"
  printf "  ${BOLD}${RED}└──────────────────────────────────────────────────┘${NC}\n"
  echo
  exit 1
fi
