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

CLAUDE_DIR="$HOME/.claude"
MANIFEST_FILE="$CLAUDE_DIR/.claude-scholar-manifest.txt"
STATE_FILE="$CLAUDE_DIR/.claude-scholar-install-state"
BACKUP_ROOT="$CLAUDE_DIR/.claude-scholar-backups"
UNINSTALL_STAMP="$(date +%Y%m%d-%H%M%S)"
UNINSTALL_BACKUP_DIR="$BACKUP_ROOT/uninstall-$UNINSTALL_STAMP"
COMPONENT_DIRS=(skills commands agents rules hooks scripts templates)
LEGACY_MANAGED_PATHS=(
  "skills/planning-with-files/SKILL.md"
  "skills/planning-with-files/examples.md"
  "skills/planning-with-files/reference.md"
)
REMOVED_COUNT=0
SKIPPED_COUNT=0
DRY_RUN=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 $*" >&2; exit 1; }

usage() {
  cat <<'EOF'
Usage: bash scripts/uninstall.sh [--dry-run]

Removes Claude Scholar managed files from ~/.claude without touching unrelated user files.
- Uses ~/.claude/.claude-scholar-manifest.txt when available.
- Falls back to the current repo checkout when run from a repo working tree.
- Cleans Claude Scholar hook / MCP / plugin entries from ~/.claude/settings.json.
EOF
}

parse_args() {
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --dry-run)
        DRY_RUN=1
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        error "Unknown argument: $1"
        ;;
    esac
    shift
  done
}

require_install_metadata() {
  [ -f "$MANIFEST_FILE" ] || error "Missing $MANIFEST_FILE. Refusing to guess ownership."
  [ -f "$STATE_FILE" ] || error "Missing $STATE_FILE. Refusing to guess settings ownership."
}

backup_target() {
  local target="$1"
  [ -e "$target" ] || return 0
  local rel="${target#$CLAUDE_DIR/}"
  [ "$rel" = "$target" ] && rel="$(basename "$target")"
  mkdir -p "$UNINSTALL_BACKUP_DIR/$(dirname "$rel")"
  if [ "$DRY_RUN" -eq 0 ]; then
    if [ -d "$target" ]; then
      cp -R "$target" "$UNINSTALL_BACKUP_DIR/$rel"
    else
      cp -p "$target" "$UNINSTALL_BACKUP_DIR/$rel"
    fi
  fi
}

append_path() {
  local path="$1"
  [ -n "$path" ] || return 0
  printf "%s\n" "$path"
}

collect_manifest_paths() {
  cat "$MANIFEST_FILE"
  printf "%s\n" "${LEGACY_MANAGED_PATHS[@]}"
}

remove_managed_files() {
  local rel
  while IFS= read -r rel; do
    [ -n "$rel" ] || continue
    case "$rel" in
      .*|*..*|/*) continue ;;
    esac
    local target="$CLAUDE_DIR/$rel"
    if [ ! -e "$target" ]; then
      SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
      continue
    fi
    backup_target "$target"
    if [ "$DRY_RUN" -eq 0 ]; then
      rm -rf "$target"
    fi
    REMOVED_COUNT=$((REMOVED_COUNT + 1))
  done < <(collect_manifest_paths | LC_ALL=C sort -u)
}

cleanup_empty_dirs() {
  local comp
  for comp in "${COMPONENT_DIRS[@]}"; do
    if [ -d "$CLAUDE_DIR/$comp" ] && [ "$DRY_RUN" -eq 0 ]; then
      find "$CLAUDE_DIR/$comp" -depth -type d -empty -delete
    fi
  done
}

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

  backup_target "$settings"
  if [ "$DRY_RUN" -eq 1 ]; then
    info "Would clean Claude Scholar entries from $settings"
    return 0
  fi

  SETTINGS_PATH="$settings" STATE_PATH="$STATE_FILE" node <<'NODE'
const fs = require('fs');

const settingsPath = process.env.SETTINGS_PATH;
const statePath = process.env.STATE_PATH;
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
const settingsCreated = Boolean(state.settingsCreated);
const addedHooks = Array.isArray(state.settings?.addedHooks) ? state.settings.addedHooks : [];
const addedMcpServers = new Set(Array.isArray(state.settings?.addedMcpServers) ? state.settings.addedMcpServers : []);
const addedMcpServerFields = state.settings?.addedMcpServerFields && typeof state.settings.addedMcpServerFields === 'object'
  ? state.settings.addedMcpServerFields
  : {};
const addedEnabledPlugins = new Set(Array.isArray(state.settings?.addedEnabledPlugins) ? state.settings.addedEnabledPlugins : []);

function hookSignature(hook) {
  return JSON.stringify({
    type: hook?.type || '',
    command: hook?.command || '',
    timeout: hook?.timeout ?? null,
  });
}

const ownedHookMatchers = new Map();
for (const hook of addedHooks) {
  const key = `${hook.event}::${hook.matcher || '*'}`;
  const sigs = ownedHookMatchers.get(key) || new Set();
  sigs.add(hookSignature(hook));
  ownedHookMatchers.set(key, sigs);
}

function pruneEmptyContainers(root, parts) {
  for (let i = parts.length - 1; i > 0; i -= 1) {
    const parent = parts.slice(0, i - 1).reduce((acc, key) => (acc && typeof acc === 'object') ? acc[key] : undefined, root);
    const key = parts[i - 1];
    if (!parent || typeof parent !== 'object') return;
    const value = parent[key];
    if (value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) {
      delete parent[key];
      continue;
    }
    return;
  }
}

function removeNestedPath(root, dottedPath) {
  if (!root || typeof root !== 'object' || !dottedPath) return;
  const parts = dottedPath.split('.').filter(Boolean);
  if (parts.length === 0) return;
  let current = root;
  for (let i = 0; i < parts.length - 1; i += 1) {
    current = current?.[parts[i]];
    if (!current || typeof current !== 'object') return;
  }
  delete current[parts[parts.length - 1]];
  pruneEmptyContainers(root, parts);
}

if (settings.hooks && typeof settings.hooks === 'object') {
  for (const [eventName, matchers] of Object.entries(settings.hooks)) {
    if (!Array.isArray(matchers)) continue;
    const nextMatchers = matchers
      .map((matcher) => {
        const key = `${eventName}::${matcher.matcher || '*'}`;
        const owned = ownedHookMatchers.get(key) || new Set();
        const hooks = Array.isArray(matcher.hooks)
          ? matcher.hooks.filter((hook) => !owned.has(hookSignature(hook)))
          : [];
        return hooks.length > 0 ? { ...matcher, hooks } : null;
      })
      .filter(Boolean);
    if (nextMatchers.length > 0) {
      settings.hooks[eventName] = nextMatchers;
    } else {
      delete settings.hooks[eventName];
    }
  }
  if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
}

if (settings.mcpServers && typeof settings.mcpServers === 'object') {
  for (const key of addedMcpServers) {
    delete settings.mcpServers[key];
  }
  for (const [key, paths] of Object.entries(addedMcpServerFields)) {
    if (!(key in settings.mcpServers) || !Array.isArray(paths)) continue;
    for (const dottedPath of paths) {
      removeNestedPath(settings.mcpServers[key], dottedPath);
    }
    if (
      settings.mcpServers[key] &&
      typeof settings.mcpServers[key] === 'object' &&
      !Array.isArray(settings.mcpServers[key]) &&
      Object.keys(settings.mcpServers[key]).length === 0
    ) {
      delete settings.mcpServers[key];
    }
  }
  if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
}

if (settings.enabledPlugins && typeof settings.enabledPlugins === 'object') {
  for (const key of addedEnabledPlugins) {
    delete settings.enabledPlugins[key];
  }
  if (Object.keys(settings.enabledPlugins).length === 0) delete settings.enabledPlugins;
}

const onlyDefaultTemplateRemainder =
  settingsCreated &&
  Object.keys(settings).every((key) => ['env', 'verbose'].includes(key)) &&
  settings.verbose === true &&
  settings.env &&
  Object.keys(settings.env).length === 1 &&
  settings.env.GITHUB_PERSONAL_ACCESS_TOKEN === '<your-github-token-optional>';

if (onlyDefaultTemplateRemainder) {
  fs.unlinkSync(settingsPath);
} else {
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
}
NODE
}

remove_metadata_files() {
  local path
  for path in "$MANIFEST_FILE" "$STATE_FILE"; do
    [ -e "$path" ] || continue
    backup_target "$path"
    if [ "$DRY_RUN" -eq 0 ]; then
      rm -f "$path"
    fi
  done
}

main() {
  parse_args "$@"
  require_install_metadata

  echo ""
  echo "╔══════════════════════════════════════╗"
  echo "║      Claude Scholar Uninstaller      ║"
  echo "╚══════════════════════════════════════╝"
  echo ""

  remove_managed_files
  cleanup_empty_dirs
  cleanup_settings
  remove_metadata_files

  if [ "$DRY_RUN" -eq 1 ]; then
    info "Dry run complete. Files that would be removed: $REMOVED_COUNT | Missing/skipped: $SKIPPED_COUNT"
    exit 0
  fi

  info "Removed files: $REMOVED_COUNT | Missing/skipped: $SKIPPED_COUNT"
  info "Uninstall backup: $UNINSTALL_BACKUP_DIR"
  info "Done."
}

main "$@"
