/**
 * Tool registration and gating.
 *
 * applyToolGating() — monkey-patches server.tool() to filter by category,
 *   track invocations, and inject multi-vault support.
 *
 * registerAllTools() — calls all tool registration functions with
 *   scope-aware getters for vault state.
 */

import * as path from 'path';
import { dirname, join } from 'path';
import { statSync, readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { z } from 'zod';
import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
import { CallToolRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';

const __trFilename = fileURLToPath(import.meta.url);
const __trDirname = dirname(__trFilename);
const trPkg = JSON.parse(readFileSync(join(__trDirname, '../package.json'), 'utf-8'));

import type { VaultIndex } from './core/read/types.js';
import type { FlywheelConfig } from './core/read/config.js';
import type { WatcherStatus } from './core/read/watch/index.js';
import type { PipelineActivity } from './core/read/watch/types.js';
import type { StateDb } from '@velvetmonkey/vault-core';
import { getSessionId } from '@velvetmonkey/vault-core';

import { ALL_CATEGORIES, TOOL_CATEGORY, type ToolCategory, type ToolTier, type ToolTierOverride } from './config.js';
import { VaultRegistry, type VaultContext } from './vault-registry.js';
import { runInVaultScope, getActiveScopeOrNull } from './vault-scope.js';
import { getCurrentCaller } from './caller-scope.js';
import {
  extractQueryContext,
  extractSearchMethod,
  getActivationSignals,
} from './tool-registry/activation.js';
import { emitObservation, summariseInput, summariseResult, observedHits, observedNearMisses, extractObservedDetails } from './core/shared/observer.js';
import { shouldSuppressMemoryTool } from './tool-registry/clientSuppressions.js';
import { runCrossVaultSearch } from './tool-registry/crossVault.js';
import { shouldEnableTieredTool, getDirectCallPromotion } from './tool-registry/tiering.js';
import type { ToolTierMode, ToolTierController, VaultActivationCallbacks } from './tool-registry/types.js';
export { getPatternSignals, unionSignalsByCategory } from './tool-registry/activation.js';
export type { ToolTierMode, ToolTierController, VaultActivationCallbacks } from './tool-registry/types.js';

// Core imports - Tool Tracking
import { recordToolInvocation } from './core/shared/toolTracking.js';

// Read tool registrations
// graph.ts retired (T43 B3+) — get_backlinks, get_forward_links, get_strong_connections folded into graph merged tool
// graphExport.ts standalone tool retired — graph export lives under graph(action: export)
// wikilinks.ts retired (T43 B3+) — suggest_wikilinks, validate_links, discover_stub_candidates, discover_cooccurrence_gaps folded into link merged tool
import { registerHealthTools } from './tools/read/health.js';
import { registerQueryTools } from './tools/read/query.js';
import { registerFindNotesTools } from './tools/read/find_notes.js';
import { registerSystemTools as registerReadSystemTools } from './tools/read/system.js';
import { registerPrimitiveTools } from './tools/read/primitives.js';
// migrations.ts retired (T43 B3+) — rename_field, migrate_field_values folded into schema merged tool
// graphAnalysis.ts retired (T43 B3+) — graph_analysis, get_connection_strength, get_link_path, get_common_neighbors folded into graph merged tool
// vaultSchema.ts retired (T43 B3+) — vault_schema, schema_conventions, schema_validate folded into schema merged tool
// semanticAnalysis.ts retired (T43) — semantic_analysis removed from surface
// noteIntelligence.ts retired (T43 B3+) — note_intelligence folded into insights merged tool
import { registerSchemaTools } from './tools/read/schemaTools.js';
import { registerGraphTools2 } from './tools/read/graphTools.js';
import { registerInsightsTools } from './tools/read/insightsTools.js';

// Write tool registrations
// mutations.ts retired (T43 B3+) — vault_add_to_section, vault_remove_from_section, vault_replace_in_section folded into edit_section merged tool
import { registerTaskTools } from './tools/write/tasks.js';
import { registerFrontmatterTools } from './tools/write/frontmatter.js';
// notes.ts retired (T43 B3+) — vault_create_note, vault_delete_note folded into note merged tool
// move-notes.ts retired (T43 B3+) — vault_move_note, vault_rename_note folded into note merged tool
// merge.ts retired (T43 B3+) — merge_entities, absorb_as_alias folded into entity merged tool
// system.ts (write) retired (T43 B3+) — vault_undo_last_mutation folded into correct(action: undo)
import { registerPolicyTools } from './tools/write/policy.js';
// tags.ts retired (T43 B3+) — rename_tag folded into schema merged tool
// wikilinkFeedback.ts retired (T43 B3+) — wikilink_feedback folded into link merged tool
// toolSelectionFeedback.ts retired (T43) — tool_selection_feedback removed
import { registerCorrectTool } from './tools/write/correct.js';
import { registerEntityTool } from './tools/write/entity.js';
import { registerLinkTool } from './tools/write/link.js';
import { registerNoteTool } from './tools/write/note.js';
import { registerEditSectionTool } from './tools/write/editSection.js';
import { detectMisroute, recordHeuristicMisroute } from './core/shared/misrouteDetection.js';
// corrections.ts retired (T43 B3+) — vault_record_correction, vault_list_corrections, vault_resolve_correction folded into correct merged tool
import { registerMemoryTools } from './tools/write/memory.js';
// recall removed — entity/memory search merged into search (uber search)
// brief.ts retired (T43 B3+) — brief folded into memory(action: brief); runBrief still used by memory.ts
// config.ts (write) retired (T43 B3+) — flywheel_config folded into doctor(action: config)
import { registerInitTools } from './tools/write/enrich.js';

// Additional read tool registrations
// metrics.ts retired (T43 B3+) — vault_growth folded into insights merged tool
// activity.ts retired — vault_activity modes folded into vault_session_history
// similarity.ts retired (T43) — find_similar absorbed into search as action: similar
import { registerSemanticTools } from './tools/read/semantic.js';
// registerReadMergeTools retired (T43) — suggest_entity_merges/dismiss_merge_suggestion absorbed into entity tool
// temporalAnalysis.ts retired (T43 B3+) — track_concept_evolution, predict_stale_notes, get_context_around_date folded into insights merged tool
// sessionHistory.ts retired (T43) — vault_session_history removed from surface
// entityHistory.ts retired (T43) — vault_entity_history removed from surface
// learningReport.ts retired (T43) — flywheel_learning_report removed from surface
// calibrationExport.ts retired (T43) — flywheel_calibration_export removed from surface
import { registerDiscoveryTools } from './tools/read/discovery.js';

// Resources
import { registerVaultResources } from './resources/vault.js';

// ============================================================================
// Types
// ============================================================================

/** Callbacks and getters injected from index.ts (owns the singletons) */
export interface ToolRegistryContext {
  getVaultPath: () => string;
  getVaultIndex: () => VaultIndex;
  getStateDb: () => StateDb | null;
  getFlywheelConfig: () => FlywheelConfig;
  getWatcherStatus: () => WatcherStatus | null;
  getPipelineActivity: () => Readonly<PipelineActivity> | null;
  getVaultRuntimeState: () => {
    bootState: string;
    integrityState: string;
    integrityCheckInProgress: boolean;
    integrityStartedAt: number | null;
    integritySource: string | null;
    lastIntegrityCheckedAt: number | null;
    lastIntegrityDurationMs: number | null;
    lastIntegrityDetail: string | null;
    lastBackupAt: number | null;
  };
  updateVaultIndex: (index: VaultIndex) => void;
  updateFlywheelConfig: (config: FlywheelConfig) => void;
}

/** Vault activation callbacks for multi-vault gating */
// ToolTierOverride re-exported from config.ts (side-effect-free for testing)
export type { ToolTierOverride } from './config.js';

// ToolTierController moved to tool-registry/types.ts (arch-review S1: breaks
// the tool-registry ⇄ tools/read/discovery import cycle); re-exported below.

const MUTATING_TOOL_NAMES = new Set([
  'vault_add_to_section',
  'vault_remove_from_section',
  'vault_replace_in_section',
  'vault_add_task',
  'vault_toggle_task',
  'vault_update_frontmatter',
  'vault_create_note',
  'vault_delete_note',
  'vault_move_note',
  'vault_rename_note',
  'merge_entities',
  'absorb_as_alias',
  'vault_undo_last_mutation',
  'policy',
  'rename_tag',
  'wikilink_feedback',
  'tool_selection_feedback',
  'vault_record_correction',
  'vault_resolve_correction',
  'memory',
  'flywheel_config',
  'vault_init',
  'rename_field',
  'migrate_field_values',
  'refresh_index',
  'init_semantic',
]);

// ============================================================================
// Tool Gating
// ============================================================================

/**
 * Apply tool gating to a McpServer instance.
 * Monkey-patches server.tool() and server.registerTool() to filter by category,
 * wrap handlers with invocation tracking, and optionally inject a `vault` parameter
 * for multi-vault support.
 *
 * When registry is multi-vault, every tool gets an optional `vault` parameter.
 * Before each handler runs, `activateVault(registry.getContext(vault))` is called
 * to swap module-level singletons to the correct vault.
 */
export function applyToolGating(
  targetServer: McpServer,
  categories: Set<ToolCategory>,
  getDb: () => StateDb | null,
  registry?: VaultRegistry | null,
  getVaultPath?: () => string,
  vaultCallbacks?: VaultActivationCallbacks,
  tierMode: ToolTierMode = 'off',
  onTierStateChange?: (controller: ToolTierController) => void,
  isFullToolset: boolean = false,
  onToolCall?: () => void,
): ToolTierController {
  let _registered = 0;
  let _skipped = 0;
  let tierOverride: ToolTierOverride = 'auto';
  const toolHandles = new Map<string, RegisteredTool>();
  const activatedCategoryTiers = new Map<ToolCategory, ToolTier>();
  let controllerRef: ToolTierController | null = null;

  function gate(name: string): boolean {
    const category = TOOL_CATEGORY[name];
    if (!category) {
      throw new Error(
        `Tool "${name}" has no entry in TOOL_CATEGORY (config.ts). ` +
        `Every tool must be assigned a category for gating to work.`
      );
    }
    if (!categories.has(category)) {
      _skipped++;
      return false;
    }
    _registered++;
    return true;
  }

  function enableCategory(category: ToolCategory, tier: ToolTier): string[] {
    if (!categories.has(category)) return [];
    const previousTier = activatedCategoryTiers.get(category) ?? 0;
    if (tier > previousTier) {
      activatedCategoryTiers.set(category, tier);
    }
    return refreshToolVisibility();
  }

  function shouldEnableTool(toolName: string): boolean {
    return shouldEnableTieredTool(toolName, {
      categories,
      tierMode,
      tierOverride,
      activatedCategoryTiers,
    });
  }

  /** Returns names of tools that were newly enabled (empty if no change). */
  function refreshToolVisibility(): string[] {
    const newlyEnabled: string[] = [];
    for (const [name, handle] of toolHandles) {
      const enabled = shouldEnableTool(name);
      if (enabled !== handle.enabled) {
        // Set directly instead of enable()/disable() to avoid per-tool
        // sendToolListChanged() notifications — we send one batch notification below.
        // `enabled` is a public mutable property on RegisteredTool (SDK >=1.26.0).
        handle.enabled = enabled;
        if (enabled) newlyEnabled.push(name);
      }
    }
    if (newlyEnabled.length > 0) {
      targetServer.sendToolListChanged();
    }
    // Always fire callback — override state may have changed even if no tools flipped
    if (controllerRef) {
      onTierStateChange?.(controllerRef);
    }
    return newlyEnabled;
  }

  /** Returns names of tools newly activated (empty if none). */
  async function maybeActivateFromContext(toolName: string, params: unknown, searchMethod?: string): Promise<string[]> {
    if (tierMode !== 'tiered' || tierOverride === 'full') return [];
    const newlyEnabled: string[] = [];
    for (const { category, tier } of await getActivationSignals(toolName, params, searchMethod, isFullToolset)) {
      newlyEnabled.push(...enableCategory(category, tier));
    }
    return newlyEnabled;
  }

  function ensureToolEnabledForDirectCall(toolName: string): void {
    if (tierMode !== 'tiered') return;
    const handle = toolHandles.get(toolName);
    if (!handle || handle.enabled) return;
    const promotion = getDirectCallPromotion(toolName, categories, tierMode);
    if (!promotion) return;
    enableCategory(promotion.category, promotion.tier);
  }

  function wrapWithTracking(toolName: string, handler: (...args: any[]) => any): (...args: any[]) => any {
    return async (...args: any[]) => {
      onToolCall?.();
      const start = Date.now();
      let success = true;
      let notePaths: string[] | undefined;
      let result: any;
      const params = args[0];
      if (params && typeof params === 'object') {
        const paths: string[] = [];
        if (typeof params.path === 'string') paths.push(params.path);
        if (Array.isArray(params.paths)) paths.push(...params.paths.filter((p: unknown) => typeof p === 'string'));
        if (typeof params.note_path === 'string') paths.push(params.note_path);
        if (typeof params.source === 'string') paths.push(params.source);
        if (typeof params.target === 'string') paths.push(params.target);
        if (paths.length > 0) notePaths = paths;
      }
      try {
        result = await handler(...args);
        // Extract search method from result for semantic routing gating
        const searchMethod = extractSearchMethod(result);
        const newlyActivated = await maybeActivateFromContext(toolName, params, searchMethod);
        // Append activation notice to response so HTTP clients learn about new tools
        // (sendToolListChanged notifications are lost over stateless HTTP transport)
        if (newlyActivated.length > 0 && result?.content && Array.isArray(result.content)) {
          result.content.push({
            type: 'text' as const,
            text: `\n[Progressive disclosure: ${newlyActivated.length} new tools activated: ${newlyActivated.join(', ')}. Call tools/list to refresh.]`,
          });
        }
        return result;
      } catch (err) {
        success = false;
        throw err;
      } finally {
        const db = getDb();
        if (db) {
          try {
            let sessionId: string | undefined;
            try { sessionId = getSessionId(); } catch { /* no session */ }

            // Estimate response tokens from MCP response
            let responseTokens: number | undefined;
            if (result?.content) {
              let totalChars = 0;
              for (const block of result.content) {
                if (block?.type === 'text' && typeof block.text === 'string') {
                  totalChars += block.text.length;
                }
              }
              if (totalChars > 0) responseTokens = Math.ceil(totalChars / 4);
            }

            // Estimate baseline tokens (raw file read cost)
            let baselineTokens: number | undefined;
            if (notePaths && notePaths.length > 0 && getVaultPath) {
              const vp = getVaultPath();
              let totalBytes = 0;
              for (const p of notePaths) {
                try {
                  totalBytes += statSync(path.join(vp, p)).size;
                } catch { /* file may not exist */ }
              }
              if (totalBytes > 0) baselineTokens = Math.ceil(totalBytes / 4);
            }

            const queryContext = extractQueryContext(params);
            const invocationId = recordToolInvocation(db, {
              tool_name: toolName,
              session_id: sessionId,
              note_paths: notePaths,
              duration_ms: Date.now() - start,
              success,
              response_tokens: responseTokens,
              baseline_tokens: baselineTokens,
              query_context: queryContext,
            });

            // Heuristic misroute detection (T15b)
            if (queryContext) {
              try {
                const misroute = detectMisroute(toolName, queryContext);
                if (misroute) {
                  recordHeuristicMisroute(db, invocationId, misroute);
                }
              } catch { /* never let heuristic errors affect tool execution */ }
            }
          } catch {
            // Never let tracking errors affect tool execution
          }
        }

        // Stage 1b — capture-at-source retrieval observation. Fire-and-forget
        // side-channel to FLYWHEEL_OBSERVER_URL (no-op when unset). Wrapped in
        // its own guard so a summariser/emit fault can never reach the tool
        // path; mirrors the "Never let tracking errors affect tool execution"
        // contract above. Runs whether or not a stateDb is present.
        try {
          if (process.env.FLYWHEEL_OBSERVER_URL) {
            let obsSession: string | undefined;
            try { obsSession = getSessionId(); } catch { /* no session */ }
            let resultChars: number | undefined;
            if (result?.content && Array.isArray(result.content)) {
              let total = 0;
              for (const block of result.content) {
                if (block?.type === 'text' && typeof block.text === 'string') total += block.text.length;
              }
              if (total > 0) resultChars = total;
            }
            emitObservation({
              tool: toolName,
              input_summary: summariseInput(params),
              result_summary: success ? summariseResult(toolName, result?.content) : undefined,
              result_chars: resultChars,
              is_error: !success,
              duration_ms: Date.now() - start,
              session_id: obsSession,
              // Per-request caller tag (X-Flywheel-Caller). Read synchronously
              // here, still inside the request's async context, so a consumer
              // can map this observation to the originating caller's turn.
              caller_id: getCurrentCaller(),
              // Scored hits a search-family handler stashed pre-strip (by result identity).
              // For non-search tools, fall through to detail extraction from result content.
              results: success && result
                ? (observedHits.get(result) ?? extractObservedDetails(toolName, result?.content))
                : undefined,
              // "Considered but discarded" candidates stashed alongside (same identity key).
              near_miss: success && result ? observedNearMisses.get(result) : undefined,
            });
          }
        } catch {
          // Never let observation errors affect tool execution
        }
      }
    };
  }

  const isMultiVault = registry?.isMultiVault ?? false;

  function getTargetVaultContext(params: Record<string, unknown> | undefined): VaultContext | null {
    if (!registry) return null;
    if (isMultiVault) {
      const vaultName = typeof params?.vault === 'string' ? params.vault : undefined;
      return registry.getContext(vaultName);
    }
    return registry.getContext();
  }

  function wrapWithIntegrityGate(toolName: string, handler: (...args: any[]) => any): (...args: any[]) => any {
    if (!MUTATING_TOOL_NAMES.has(toolName)) return handler;
    return async (...args: any[]) => {
      const params = (args[0] && typeof args[0] === 'object') ? args[0] as Record<string, unknown> : undefined;
      const vaultCtx = getTargetVaultContext(params);
      const integrityState = vaultCtx?.integrityState ?? getActiveScopeOrNull()?.integrityState;
      if (integrityState === 'failed') {
        throw new Error('StateDb integrity failed; write operations are disabled until recovery/restart.');
      }
      return handler(...args);
    };
  }

  /**
   * Wrap a handler to activate the correct vault before execution (multi-vault).
   * Extracts the `vault` param, calls activateVault(), then forwards to the original handler.
   * For `search` with no explicit vault, iterates all vaults and merges results.
   */
  function wrapWithVaultActivation(toolName: string, handler: (...args: any[]) => any): (...args: any[]) => any {
    if (!isMultiVault || !registry || !vaultCallbacks) return handler;
    return async (...args: any[]) => {
      const params = args[0];
      const vaultName = params?.vault;
      // Remove vault from params before forwarding (tools don't expect it)
      if (params && 'vault' in params) {
        delete params.vault;
      }
      // Cross-vault search/find: when no vault specified, query all vaults and merge
      if ((toolName === 'search' || toolName === 'find_notes') && !vaultName) {
        return runCrossVaultSearch(registry!, vaultCallbacks!, handler, args);
      }
      const ctx = registry.getContext(vaultName);
      // Set fallback scope + module-level state (for watcher/startup code paths)
      vaultCallbacks!.activateVault(ctx);
      // Run handler inside ALS context for per-request isolation
      return runInVaultScope(vaultCallbacks!.buildVaultScope(ctx), () => handler(...args));
    };
  }

  /**
   * Inject `vault` parameter into a tool's schema (multi-vault only).
   * server.tool() is called as: (name, description, schema, handler) or (name, schema, handler)
   */
  function injectVaultParam(args: any[]): void {
    if (!isMultiVault || !registry) return;
    // Find the schema object (the arg before the handler function)
    const handlerIdx = args.findIndex((a: any) => typeof a === 'function');
    if (handlerIdx <= 0) return;
    const schemaIdx = handlerIdx - 1;
    const schema = args[schemaIdx];
    if (schema && typeof schema === 'object' && !Array.isArray(schema)) {
      schema.vault = z.string().optional().describe(
        `Vault name for multi-vault mode. Available: ${registry.getVaultNames().join(', ')}. Default: ${registry.primaryName}`
      );
    }
  }

  // ==========================================================================
  // Wrapper-chain ORDER invariant (applies to both tool() and registerTool()):
  //
  //   registration:  gating → vault-param injection → vault activation →
  //                  integrity gate → tracking
  //
  // Wraps are applied inside-out, so at call time the onion runs
  // tracking (outermost) → integrity gate → vault activation → handler.
  // Order matters:
  //   1. gate(name) first — a category-skipped tool must not be wrapped or
  //      registered at all (and gate() throws on unknown tools).
  //   2. injectVaultParam before wrapping — the optional `vault` param must be
  //      in the schema or the SDK strips it before any wrapper can see it.
  //   3. integrity gate OUTSIDE vault activation — the gate resolves the
  //      target vault from params.vault, which wrapWithVaultActivation
  //      deletes before forwarding; reversed, the gate would always check the
  //      default vault. Inner activation also means a failed-integrity vault
  //      is rejected before it is ever activated.
  //   4. tracking outermost — duration/success recording and the retrieval
  //      observer must capture every outcome, including integrity-gate
  //      rejections and cross-vault fan-out, and post-process the final
  //      result (progressive-disclosure activation notices).
  // ==========================================================================
  const origTool = targetServer.tool.bind(targetServer) as (...args: unknown[]) => unknown;
  (targetServer as any).tool = (name: string, ...args: any[]) => {
    if (!gate(name)) return;
    // Inject vault param into schema (multi-vault)
    injectVaultParam(args);
    // Wrap handler with tracking + vault activation
    if (args.length > 0 && typeof args[args.length - 1] === 'function') {
      let handler = args[args.length - 1];
      handler = wrapWithVaultActivation(name, handler);
      handler = wrapWithIntegrityGate(name, handler);
      args[args.length - 1] = wrapWithTracking(name, handler);
    }
    const registered = origTool(name, ...args) as RegisteredTool;
    toolHandles.set(name, registered);
    return registered;
  };

  const origRegisterTool = (targetServer as any).registerTool?.bind(targetServer);
  if (origRegisterTool) {
    (targetServer as any).registerTool = (name: string, ...args: any[]) => {
      if (!gate(name)) return;
      injectVaultParam(args);
      if (args.length > 0 && typeof args[args.length - 1] === 'function') {
        let handler = args[args.length - 1];
        handler = wrapWithVaultActivation(name, handler);
        handler = wrapWithIntegrityGate(name, handler);
        args[args.length - 1] = wrapWithTracking(name, handler);
      }
      const registered = origRegisterTool(name, ...args) as RegisteredTool;
      toolHandles.set(name, registered);
      return registered;
    };
  }

  /**
   * Install a custom CallTool handler that adds auto-promotion for tiered tools.
   *
   * When a client calls a disabled tier-2 tool directly, this handler auto-promotes
   * the tool's category before the SDK rejects the call. This can't be done via
   * handler wrappers because the SDK checks `tool.enabled` before calling handlers.
   *
   * SDK internal access (documented for version audits):
   *   - serverAny.server.setRequestHandler — replaces the tool call handler
   *   - serverAny.validateToolInput — input schema validation
   *   - serverAny.executeToolHandler — handler invocation
   *   - serverAny.validateToolOutput — output schema validation
   *   - serverAny.createToolError — error response construction
   *   - serverAny.handleAutomaticTaskPolling — task support (optional mode)
   *
   * These are stable across SDK 1.25–1.26 but not part of the public API.
   * TODO: Upstream a pre-call hook or middleware API to eliminate this coupling.
   */
  function installTieredCallHandler(): void {
    if (tierMode !== 'tiered') return;
    const serverAny = targetServer as any;
    serverAny.server.setRequestHandler(CallToolRequestSchema, async (request: any, extra: any) => {
      try {
        const tool = toolHandles.get(request.params.name);
        if (!tool) {
          throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
        }
        if (!tool.enabled) {
          ensureToolEnabledForDirectCall(request.params.name);
        }
        if (!tool.enabled) {
          throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
        }
        const isTaskRequest = !!request.params.task;
        const taskSupport = tool.execution?.taskSupport;
        const isTaskHandler = 'createTask' in tool.handler;
        if ((taskSupport === 'required' || taskSupport === 'optional') && !isTaskHandler) {
          throw new McpError(ErrorCode.InternalError, `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask`);
        }
        if (taskSupport === 'required' && !isTaskRequest) {
          throw new McpError(ErrorCode.MethodNotFound, `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`);
        }
        if (taskSupport === 'optional' && !isTaskRequest && isTaskHandler) {
          return await serverAny.handleAutomaticTaskPolling(tool, request, extra);
        }
        const args = await serverAny.validateToolInput(tool, request.params.arguments, request.params.name);
        const result = await serverAny.executeToolHandler(tool, args, extra);
        if (isTaskRequest) {
          return result;
        }
        await serverAny.validateToolOutput(tool, result, request.params.name);
        return result;
      } catch (error) {
        if (error instanceof McpError && error.code === ErrorCode.UrlElicitationRequired) {
          throw error;
        }
        return serverAny.createToolError(error instanceof Error ? error.message : String(error));
      }
    });
  }

  const controller: ToolTierController = {
    mode: tierMode,
    get registered() {
      return _registered;
    },
    get skipped() {
      return _skipped;
    },
    get activeCategories() {
      return new Set(activatedCategoryTiers.keys());
    },
    getOverride() {
      return tierOverride;
    },
    finalizeRegistration() {
      refreshToolVisibility();
      installTieredCallHandler();
    },
    activateCategory(category: ToolCategory, tier: ToolTier = 2) {
      enableCategory(category, tier);
    },
    enableTierCategory(category: ToolCategory) {
      enableCategory(category, 2);
    },
    enableAllTiers() {
      tierOverride = 'full';
      refreshToolVisibility();
    },
    setOverride(override: ToolTierOverride) {
      tierOverride = override;
      refreshToolVisibility();
    },
    getActivatedCategoryTiers() {
      return new Map(activatedCategoryTiers);
    },
    getRegisteredTools() {
      return toolHandles;
    },
  };

  controllerRef = controller;

  return controller;
}

// ============================================================================
// Tool Registration
// ============================================================================

/**
 * Register all tools on a McpServer instance.
 * Uses scope-aware getters that read from ALS VaultScope first,
 * falling back to module-level singletons via the injected context.
 */
export interface RegisterAllToolsOptions {
  /**
   * When true (default), the Claude Code client fingerprint (CLAUDECODE=1)
   * causes the `memory` tool to be skipped because Claude Code intercepts
   * memory verbs onto its own native memory plane. Set to false in test
   * catalog collection so the manifest always reflects the complete surface.
   */
  applyClientSuppressions?: boolean;
}

export function registerAllTools(
  targetServer: McpServer,
  ctx: ToolRegistryContext,
  controller?: ToolTierController | null,
  options: RegisterAllToolsOptions = {},
): void {
  const { applyClientSuppressions = true } = options;
  const { getVaultPath: gvp, getVaultIndex: gvi, getStateDb: gsd, getFlywheelConfig: gcf } = ctx;

  // Read tools
  registerHealthTools(targetServer, gvi, gvp, gcf, gsd, ctx.getWatcherStatus, () => trPkg.version, ctx.getPipelineActivity, ctx.getVaultRuntimeState, (newConfig) => { ctx.updateFlywheelConfig(newConfig); });
  registerReadSystemTools(
    targetServer,
    gvi,
    (newIndex) => { ctx.updateVaultIndex(newIndex); },
    gvp,
    (newConfig) => { ctx.updateFlywheelConfig(newConfig); },
    gsd
  );
  registerQueryTools(targetServer, gvi, gvp, gsd);
  registerFindNotesTools(targetServer, gvi, gsd);
  registerPrimitiveTools(targetServer, gvi, gvp, gcf, gsd);
  registerSchemaTools(targetServer, gvi, gvp);
  registerGraphTools2(targetServer, gvi, gvp, gsd, gcf);
  registerInsightsTools(targetServer, gvi, gvp, gsd, gcf);

  // Write tools
  registerTaskTools(targetServer, gvp);
  registerFrontmatterTools(targetServer, gvp);
  registerPolicyTools(targetServer, gvp, () => {
    const index = gvi();
    if (!index) return undefined;
    return ({ query, folder, where, limit = 10 }: { query?: string; folder?: string; where?: Record<string, unknown>; limit?: number }) => {
      let notes = Array.from(index.notes.values());
      if (folder) {
        const normalizedFolder = folder.endsWith('/') ? folder : folder + '/';
        notes = notes.filter(n => n.path.startsWith(normalizedFolder) || n.path.split('/')[0] === folder.replace('/', ''));
      }
      if (where) {
        notes = notes.filter(n => {
          for (const [key, value] of Object.entries(where)) {
            const noteValue = n.frontmatter[key];
            if (Array.isArray(value)) {
              if (!value.some(v => String(noteValue).toLowerCase() === String(v).toLowerCase())) return false;
            } else if (value !== undefined && String(noteValue ?? '').toLowerCase() !== String(value).toLowerCase()) {
              return false;
            }
          }
          return true;
        });
      }
      return notes.slice(0, limit).map(n => ({
        path: n.path,
        title: n.title,
        frontmatter: n.frontmatter,
        snippet: undefined,
      }));
    };
  });
  // registerTagTools retired (T43 B3+) — rename_tag folded into schema merged tool
  // registerWikilinkFeedbackTools retired (T43 B3+) — wikilink_feedback folded into link merged tool
  // registerToolSelectionFeedbackTools retired (T43) — tool_selection_feedback removed from surface
  // registerCorrectionTools retired (T43 B3+) — vault_record_correction etc. folded into correct merged tool
  // registerInitTools retired (T43) — vault_init removed from surface
  // registerConfigTools retired (T43 B3+) — flywheel_config folded into doctor(action: config)

  // Additional read tools
  // registerMetricsTools retired (T43 B3+) — vault_growth folded into insights merged tool
  // vault_activity retired — modes folded into vault_session_history
  // registerSimilarityTools retired (T43) — find_similar absorbed into search
  registerSemanticTools(targetServer, gvp, gsd);
  // registerReadMergeTools retired (T43) — suggest_merges/dismiss_merge now in entity tool
  // registerTemporalAnalysisTools retired (T43 B3+) — track_concept_evolution etc. folded into insights merged tool
  // registerSessionHistoryTools retired (T43) — vault_session_history removed from surface
  // registerEntityHistoryTools retired (T43) — vault_entity_history removed from surface
  // registerLearningReportTools retired (T43) — flywheel_learning_report removed from surface
  // registerCalibrationExportTools retired (T43) — flywheel_calibration_export removed from surface

  // Memory tools
  //
  // Claude Code intercepts verbs like "remember", "search memory", "list memories",
  // "forget" onto its native client-side memory plane (Glob/Read against
  // ~/.claude/memory/), never reaching the MCP `memory` tool regardless of how
  // the tool is described or ranked. Measured 3/6 memory-action tests routed to
  // Glob instead of memory(*). To avoid the collision, suppress registration of
  // the `memory` tool when we detect the Claude Code client (via CLAUDECODE=1
  // env var it sets on spawned MCP subprocesses). `memory(action: brief)` stays
  // available — it's the Claude Code escape hatch for memory retrieval. flywheel-engine
  // and non-Claude clients are unaffected. Override with FW_ENABLE_MEMORY_FOR_CLAUDE=1.
  const suppressMemoryForClaude = shouldSuppressMemoryTool({ applyClientSuppressions });
  if (!suppressMemoryForClaude) {
    registerMemoryTools(targetServer, gsd);
  }
  // recall removed — entity/memory search merged into search (uber search)
  // brief tool retired (T43 B3+) — brief action now part of memory tool (memory.ts imports runBrief)

  // T43 merged tools
  registerNoteTool(targetServer, gvp, gvi);
  registerLinkTool(targetServer, gvi, gvp, gsd);
  registerCorrectTool(targetServer, gsd, gvp);
  registerEntityTool(targetServer, gvp, gsd, gvi);
  registerEditSectionTool(targetServer, gvp, gcf);

  // Discovery tool (progressive disclosure meta-tool — only in auto/tiered mode)
  if (controller && controller.mode === 'tiered') {
    registerDiscoveryTools(targetServer, controller);
  }

  // Resources (always registered, not gated by tool presets)
  registerVaultResources(targetServer, () => gvi() ?? null);
}
