#!/usr/bin/env bash
set -euo pipefail

CLAUDE_DIR="$HOME/.claude"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SRC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
COMPONENTS=(skills commands agents rules hooks scripts templates)
CLAUDE_MD_SIDECAR="CLAUDE.scholar.md"
CLAUDE_ZH_MD_SIDECAR="CLAUDE.zh-CN.scholar.md"
BACKUP_ROOT="$CLAUDE_DIR/.claude-scholar-backups"
MANIFEST_FILE="$CLAUDE_DIR/.claude-scholar-manifest.txt"
STATE_FILE="$CLAUDE_DIR/.claude-scholar-install-state"
PREVIOUS_MANAGED_PATHS_FILE="$(mktemp)"
BACKUP_STAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="$BACKUP_ROOT/$BACKUP_STAMP"
BACKUP_READY=0
BACKUP_COUNT=0
UPDATED_COUNT=0
SKIPPED_COUNT=0
SETTINGS_CREATED=0
MANAGED_PATHS=()
CLAUDE_TARGETS=()
SETTINGS_META_FILE="$(mktemp)"
LEGACY_INSTALL_DETECTED=0

info()  { echo -e "\033[1;34m[INFO]\033[0m $*"; }
warn()  { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[1;31m[ERROR]\033[0m $*"; exit 1; }

cleanup_temp_files() {
  rm -f "$SETTINGS_META_FILE" "$PREVIOUS_MANAGED_PATHS_FILE"
}

trap cleanup_temp_files EXIT

check_deps() {
  command -v git  >/dev/null || error "Git is required. Install it first."
  command -v node >/dev/null || error "Node.js is required (hooks depend on it). Install it first."
}

load_previous_manifest() {
  if [ -f "$MANIFEST_FILE" ]; then
    cp "$MANIFEST_FILE" "$PREVIOUS_MANAGED_PATHS_FILE"
  else
    : > "$PREVIOUS_MANAGED_PATHS_FILE"
  fi
}

detect_legacy_install() {
  local settings="$CLAUDE_DIR/settings.json"
  [ -f "$MANIFEST_FILE" ] && return 0
  [ -f "$settings" ] || return 0

  if CLAUDE_SETTINGS_TARGET="$settings" node <<'NODE'
const fs = require('fs');
const settings = JSON.parse(fs.readFileSync(process.env.CLAUDE_SETTINGS_TARGET, 'utf8'));
const hookNeedles = [
  '.claude/hooks/security-guard.js',
  '.claude/hooks/session-summary.js',
  '.claude/hooks/session-start.js',
  '.claude/hooks/stop-summary.js',
  '.claude/hooks/skill-forced-eval.js',
];
let detected = false;

for (const matchers of Object.values(settings.hooks || {})) {
  if (!Array.isArray(matchers)) continue;
  for (const matcher of matchers) {
    for (const hook of matcher.hooks || []) {
      if (typeof hook.command === 'string' && hookNeedles.some((needle) => hook.command.includes(needle))) {
        detected = true;
      }
    }
  }
}

process.exit(detected ? 0 : 1);
NODE
  then
    LEGACY_INSTALL_DETECTED=1
  fi
}

record_managed_path() {
  local target="$1"
  local rel="${target#$CLAUDE_DIR/}"
  [ "$rel" = "$target" ] && return 0
  [ -z "$rel" ] && return 0
  MANAGED_PATHS+=("$rel")
}

record_claude_target() {
  local target="$1"
  local rel="${target#$CLAUDE_DIR/}"
  [ "$rel" = "$target" ] && return 0
  [ -z "$rel" ] && return 0
  CLAUDE_TARGETS+=("$rel")
}

was_previously_managed() {
  local target="$1"
  local rel="${target#$CLAUDE_DIR/}"
  [ "$rel" = "$target" ] && return 1
  grep -Fxq "$rel" "$PREVIOUS_MANAGED_PATHS_FILE"
}

should_adopt_existing_path() {
  local target="$1"
  if was_previously_managed "$target"; then
    return 0
  fi
  [ "$LEGACY_INSTALL_DETECTED" -eq 1 ]
}

write_install_state() {
  mkdir -p "$CLAUDE_DIR"
  if [ "${#MANAGED_PATHS[@]}" -gt 0 ]; then
    printf "%s\n" "${MANAGED_PATHS[@]}" | LC_ALL=C sort -u > "$MANIFEST_FILE"
  else
    : > "$MANIFEST_FILE"
  fi

  local managed_paths_file
  local claude_targets_file
  managed_paths_file="$(mktemp)"
  claude_targets_file="$(mktemp)"

  if [ "${#MANAGED_PATHS[@]}" -gt 0 ]; then
    printf "%s\n" "${MANAGED_PATHS[@]}" | LC_ALL=C sort -u > "$managed_paths_file"
  else
    : > "$managed_paths_file"
  fi

  if [ "${#CLAUDE_TARGETS[@]}" -gt 0 ]; then
    printf "%s\n" "${CLAUDE_TARGETS[@]}" | LC_ALL=C sort -u > "$claude_targets_file"
  else
    : > "$claude_targets_file"
  fi

  CLAUDE_STATE_FILE="$STATE_FILE" \
  CLAUDE_SETTINGS_META_FILE="$SETTINGS_META_FILE" \
  CLAUDE_MANAGED_PATHS_FILE="$managed_paths_file" \
  CLAUDE_TARGETS_FILE="$claude_targets_file" \
  CLAUDE_INSTALLED_AT="$BACKUP_STAMP" \
  CLAUDE_SOURCE_DIR="$SRC_DIR" \
  CLAUDE_SETTINGS_CREATED="$SETTINGS_CREATED" \
  CLAUDE_BACKUP_DIR="$BACKUP_DIR" \
  CLAUDE_BACKUP_READY="$BACKUP_READY" \
  node <<'NODE'
const fs = require('fs');

function readLines(path) {
  if (!path || !fs.existsSync(path)) return [];
  return fs.readFileSync(path, 'utf8').split('\n').map((line) => line.trim()).filter(Boolean);
}

function readJson(path) {
  if (!path || !fs.existsSync(path)) return {};
  return JSON.parse(fs.readFileSync(path, 'utf8'));
}

const state = {
  installedAt: process.env.CLAUDE_INSTALLED_AT,
  sourceDir: process.env.CLAUDE_SOURCE_DIR,
  settingsCreated: process.env.CLAUDE_SETTINGS_CREATED === '1',
  backupDir: process.env.CLAUDE_BACKUP_READY === '1' ? process.env.CLAUDE_BACKUP_DIR : '',
  managedPaths: readLines(process.env.CLAUDE_MANAGED_PATHS_FILE),
  claudeTargets: readLines(process.env.CLAUDE_TARGETS_FILE),
  settings: readJson(process.env.CLAUDE_SETTINGS_META_FILE),
};

fs.writeFileSync(process.env.CLAUDE_STATE_FILE, JSON.stringify(state, null, 2) + '\n');
NODE

  rm -f "$managed_paths_file" "$claude_targets_file"
}

ensure_backup_dir() {
  if [ "$BACKUP_READY" -eq 0 ]; then
    mkdir -p "$BACKUP_DIR"
    BACKUP_READY=1
    info "Backup directory: $BACKUP_DIR"
  fi
}

backup_path() {
  local target="$1"
  [ -e "$target" ] || return 0

  ensure_backup_dir

  local rel="${target#$CLAUDE_DIR/}"
  if [ "$rel" = "$target" ]; then
    rel="$(basename "$target")"
  fi

  mkdir -p "$BACKUP_DIR/$(dirname "$rel")"
  if [ -d "$target" ]; then
    cp -R "$target" "$BACKUP_DIR/$rel"
  else
    cp -p "$target" "$BACKUP_DIR/$rel"
  fi
  BACKUP_COUNT=$((BACKUP_COUNT + 1))
}

# Create settings.json from template
create_settings() {
  local template="$1/settings.json.template"
  local target="$CLAUDE_DIR/settings.json"
  if [ -f "$template" ] && [ ! -f "$target" ]; then
    cp "$template" "$target"
    SETTINGS_CREATED=1
    CLAUDE_SETTINGS_TEMPLATE="$template" CLAUDE_SETTINGS_META_FILE="$SETTINGS_META_FILE" node <<'NODE'
const fs = require('fs');

const template = JSON.parse(fs.readFileSync(process.env.CLAUDE_SETTINGS_TEMPLATE, 'utf8'));

const addedHooks = [];
for (const [eventName, matchers] of Object.entries(template.hooks || {})) {
  for (const matcher of matchers || []) {
    for (const hook of matcher.hooks || []) {
      addedHooks.push({
        event: eventName,
        matcher: matcher.matcher || '*',
        type: hook.type || '',
        command: hook.command || '',
        timeout: hook.timeout ?? null,
      });
    }
  }
}

fs.writeFileSync(process.env.CLAUDE_SETTINGS_META_FILE, JSON.stringify({
  addedHooks,
  addedMcpServers: Object.keys(template.mcpServers || {}),
  addedMcpServerFields: {},
  addedEnabledPlugins: Object.keys(template.enabledPlugins || {}),
}, null, 2) + '\n');
NODE
    info "Created settings.json from template."
    info "  → Edit $target to add your GITHUB_PERSONAL_ACCESS_TOKEN (optional)."
  fi
}

# Merge hooks, mcpServers, enabledPlugins from template into existing settings.json
merge_settings() {
  local template="$1/settings.json.template"
  local target="$CLAUDE_DIR/settings.json"

  [ -f "$template" ] || return 0
  [ -f "$target" ]   || { create_settings "$1"; return 0; }

  # Backup
  backup_path "$target"
  cp "$target" "${target}.bak"
  info "Backed up settings.json → settings.json.bak"

  # Merge hooks, mcpServers, enabledPlugins while preserving user env/model/API key settings.
  CLAUDE_SETTINGS_TARGET="$target" CLAUDE_SETTINGS_TEMPLATE="$template" CLAUDE_SETTINGS_META_FILE="$SETTINGS_META_FILE" node <<'NODE'
const fs = require('fs');

const targetPath = process.env.CLAUDE_SETTINGS_TARGET;
const templatePath = process.env.CLAUDE_SETTINGS_TEMPLATE;
const metaPath = process.env.CLAUDE_SETTINGS_META_FILE;
const existing = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
const template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
const addedHooks = [];
const addedMcpServers = [];
const addedMcpServerFields = {};
const addedEnabledPlugins = [];

function clone(value) {
  return JSON.parse(JSON.stringify(value));
}

function mergeMissing(existingValue, templateValue, pathParts, addedPaths) {
  if (existingValue === undefined) return clone(templateValue);
  if (templateValue === null || Array.isArray(templateValue) || typeof templateValue !== 'object') {
    return existingValue;
  }

  const output = { ...existingValue };
  for (const [key, value] of Object.entries(templateValue)) {
    if (!(key in output)) {
      output[key] = clone(value);
      addedPaths.push([...pathParts, key].join('.'));
      continue;
    }
    if (
      output[key] &&
      value &&
      !Array.isArray(output[key]) &&
      !Array.isArray(value) &&
      typeof output[key] === 'object' &&
      typeof value === 'object'
    ) {
      output[key] = mergeMissing(output[key], value, [...pathParts, key], addedPaths);
    }
  }
  return output;
}

function mergeHooks(existingHooks, templateHooks) {
  const output = existingHooks ? clone(existingHooks) : {};
  for (const [eventName, templateMatchers] of Object.entries(templateHooks || {})) {
    const existingMatchers = Array.isArray(output[eventName]) ? output[eventName] : [];
    for (const templateMatcher of templateMatchers) {
      const matchValue = templateMatcher.matcher || '*';
      let existingMatcher = existingMatchers.find((item) => (item.matcher || '*') === matchValue);
      if (!existingMatcher) {
        existingMatchers.push(clone(templateMatcher));
        for (const hook of templateMatcher.hooks || []) {
          addedHooks.push({
            event: eventName,
            matcher: matchValue,
            type: hook.type || '',
            command: hook.command || '',
            timeout: hook.timeout ?? null,
          });
        }
        continue;
      }

      existingMatcher.hooks = Array.isArray(existingMatcher.hooks) ? existingMatcher.hooks : [];
      const seen = new Set(
        existingMatcher.hooks.map((hook) =>
          JSON.stringify({
            type: hook.type || '',
            command: hook.command || '',
            timeout: hook.timeout ?? null,
          }),
        ),
      );

      for (const hook of templateMatcher.hooks || []) {
        const signature = JSON.stringify({
          type: hook.type || '',
          command: hook.command || '',
          timeout: hook.timeout ?? null,
        });
        if (!seen.has(signature)) {
          existingMatcher.hooks.push(clone(hook));
          seen.add(signature);
          addedHooks.push({
            event: eventName,
            matcher: matchValue,
            type: hook.type || '',
            command: hook.command || '',
            timeout: hook.timeout ?? null,
          });
        }
      }
    }
    output[eventName] = existingMatchers;
  }
  return output;
}

existing.hooks = mergeHooks(existing.hooks, template.hooks);

if (template.mcpServers) {
  existing.mcpServers = existing.mcpServers || {};
  for (const [key, value] of Object.entries(template.mcpServers)) {
    if (!(key in existing.mcpServers)) {
      addedMcpServers.push(key);
      existing.mcpServers[key] = clone(value);
      continue;
    }
    const addedPaths = [];
    existing.mcpServers[key] = mergeMissing(existing.mcpServers[key], value, [], addedPaths);
    if (addedPaths.length > 0) {
      addedMcpServerFields[key] = addedPaths;
    }
  }
}

if (template.enabledPlugins) {
  existing.enabledPlugins = existing.enabledPlugins || {};
  for (const [key, value] of Object.entries(template.enabledPlugins)) {
    if (!(key in existing.enabledPlugins)) {
      existing.enabledPlugins[key] = value;
      addedEnabledPlugins.push(key);
    }
  }
}

fs.writeFileSync(targetPath, JSON.stringify(existing, null, 2) + '\n');
fs.writeFileSync(metaPath, JSON.stringify({
  addedHooks,
  addedMcpServers,
  addedMcpServerFields,
  addedEnabledPlugins,
}, null, 2) + '\n');
NODE

  local merge_status=$?
  if [ "$merge_status" -ne 0 ]; then
    warn "Auto-merge failed. Please manually copy settings from settings.json.template."
    return 0
  fi

  info "Merged hooks/mcpServers/enabledPlugins into settings.json without touching env/model/API key fields."
}

# Copy one file with backup-aware overwrite
copy_file_safely() {
  local src_file="$1"
  local target_file="$2"

  mkdir -p "$(dirname "$target_file")"

  if [ -f "$target_file" ] && cmp -s "$src_file" "$target_file"; then
    if should_adopt_existing_path "$target_file"; then
      record_managed_path "$target_file"
    fi
    SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
    return 0
  fi

  if [ -e "$target_file" ]; then
    backup_path "$target_file"
    [ -d "$target_file" ] && rm -rf "$target_file"
  fi

  cp -p "$src_file" "$target_file"
  record_managed_path "$target_file"
  UPDATED_COUNT=$((UPDATED_COUNT + 1))
}

# Copy component directories with per-file backup
copy_dir_safely() {
  local src_dir="$1"
  local target_dir="$2"

  if [ -e "$target_dir" ] && [ ! -d "$target_dir" ]; then
    backup_path "$target_dir"
    rm -f "$target_dir"
  fi
  mkdir -p "$target_dir"

  while IFS= read -r -d '' src_file; do
    local rel="${src_file#$src_dir/}"
    local target_file="$target_dir/$rel"
    copy_file_safely "$src_file" "$target_file"
  done < <(find "$src_dir" -type f -print0)
}

install_claude_md() {
  local src_file="$1"
  local target_file="$CLAUDE_DIR/CLAUDE.md"
  local sidecar_file="$CLAUDE_DIR/$CLAUDE_MD_SIDECAR"

  if [ -f "$target_file" ] && should_adopt_existing_path "$target_file"; then
    copy_file_safely "$src_file" "$target_file"
    record_claude_target "$target_file"
    return 0
  fi

  if [ -f "$target_file" ]; then
    warn "Preserving existing CLAUDE.md"
    copy_file_safely "$src_file" "$sidecar_file"
    record_claude_target "$sidecar_file"
    info "Installed repository CLAUDE.md as $CLAUDE_MD_SIDECAR"
    return 0
  fi

  copy_file_safely "$src_file" "$target_file"
  record_claude_target "$target_file"
}

install_claude_zh_md() {
  local src_file="$1"
  local target_file="$CLAUDE_DIR/CLAUDE.zh-CN.md"
  local sidecar_file="$CLAUDE_DIR/$CLAUDE_ZH_MD_SIDECAR"

  if [ -f "$target_file" ] && should_adopt_existing_path "$target_file"; then
    copy_file_safely "$src_file" "$target_file"
    record_claude_target "$target_file"
    return 0
  fi

  if [ -f "$target_file" ]; then
    warn "Preserving existing CLAUDE.zh-CN.md"
    copy_file_safely "$src_file" "$sidecar_file"
    record_claude_target "$sidecar_file"
    info "Installed repository CLAUDE.zh-CN.md as $CLAUDE_ZH_MD_SIDECAR"
    return 0
  fi

  copy_file_safely "$src_file" "$target_file"
  record_claude_target "$target_file"
}

copy_components() {
  local src="$1"

  if [ -f "$src/CLAUDE.md" ]; then
    install_claude_md "$src/CLAUDE.md"
  fi

  if [ -f "$src/CLAUDE.zh-CN.md" ]; then
    install_claude_zh_md "$src/CLAUDE.zh-CN.md"
  fi

  for comp in "${COMPONENTS[@]}"; do
    if [ -e "$src/$comp" ]; then
      if [ -d "$src/$comp" ]; then
        copy_dir_safely "$src/$comp" "$CLAUDE_DIR/$comp"
      else
        copy_file_safely "$src/$comp" "$CLAUDE_DIR/$comp"
      fi
    fi
  done
  info "Updated components: ${COMPONENTS[*]}"
}

main() {
  echo ""
  echo "╔══════════════════════════════════════╗"
  echo "║       Claude Scholar Installer       ║"
  echo "╚══════════════════════════════════════╝"
  echo ""

  check_deps
  load_previous_manifest
  detect_legacy_install

  info "Installing from: $SRC_DIR"
  copy_components "$SRC_DIR"
  merge_settings "$SRC_DIR"
  write_install_state
  info "Your existing env/model/API key/permissions settings are preserved."
  info "Install manifest: $MANIFEST_FILE"
  info "Updated files: $UPDATED_COUNT | Unchanged files skipped: $SKIPPED_COUNT | Backups created: $BACKUP_COUNT"
  if [ "$BACKUP_READY" -eq 1 ]; then
    info "Recover previous files from: $BACKUP_DIR"
  fi

  echo ""
  info "Done! Restart Claude Code CLI to activate."
  echo ""
}

main "$@"
