/**
 * Vault health tools - diagnostics and statistics
 */

import * as fs from 'fs';
import { z } from 'zod';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { VaultIndex } from '../../core/read/types.js';
import { resolveTarget, getBacklinksForNote, findSimilarEntity, getIndexState, getIndexProgress, getIndexError, normalizeTarget, type IndexState } from '../../core/read/graph.js';
import { detectPeriodicNotes } from './periodic.js';
import { getActivitySummary } from './temporal.js';
import { loadConfig, type FlywheelConfig } from '../../core/read/config.js';
import { SCHEMA_VERSION, getWriteState, saveFlywheelConfigToDb, type StateDb } from '@velvetmonkey/vault-core';
import { VALID_CONFIG_KEYS } from '../write/config.js';
import { getRecentIndexEvents, getRecentPipelineEvent, getLastSuccessfulEvent, getLastEventByTrigger, compactPipelineRun, compactStep, type PipelineStep, type CompactStep, type CompactPipelineRun } from '../../core/shared/indexActivity.js';
import type { PipelineActivity } from '../../core/read/watch/pipeline.js';
import { getFTS5State } from '../../core/read/fts5.js';
import { hasEmbeddingsIndex, isEmbeddingsBuilding, getEmbeddingsCount, getActiveModelId, diagnoseEmbeddings } from '../../core/read/embeddings.js';
import { isTaskCacheReady, isTaskCacheBuilding } from '../../core/read/taskCache.js';
import { getServerLog, type LogEntry } from '../../core/shared/serverLog.js';
import { getSweepResults, type SweepResults } from '../../core/read/sweep.js';
import { getProactiveLinkingSummary, getProactiveLinkingOneLiner } from '../../core/shared/proactiveLinkingStats.js';
import { getSuppressedCount, getEntityStats, getWeightedEntityStats, computePosteriorMean, PRIOR_ALPHA, PRIOR_BETA, SUPPRESSION_MIN_OBSERVATIONS, SUPPRESSION_POSTERIOR_THRESHOLD } from '../../core/write/wikilinkFeedback.js';
import { getEntityEmbeddingsCount } from '../../core/read/embeddings.js';
import type { WatcherStatus } from '../../core/read/watch/types.js';
import { TOOL_CATEGORY, parseEnabledCategories, resolveToolConfig, ALL_CATEGORIES } from '../../config.js';
import { getRecentInvocations } from '../../core/shared/toolTracking.js';
import { searchFTS5 } from '../../core/read/fts5.js';
import { recordBenchmark, getBenchmarkHistory, getBenchmarkTrends } from '../../core/shared/benchmarks.js';

/** Staleness threshold in seconds (5 minutes) */
const STALE_THRESHOLD_SECONDS = 300;

/**
 * Register vault health tools
 */
export function registerHealthTools(
  server: McpServer,
  getIndex: () => VaultIndex,
  getVaultPath: () => string,
  getConfig: () => FlywheelConfig = () => ({}),
  getStateDb: () => StateDb | null = () => null,
  getWatcherStatus: () => WatcherStatus | null = () => null,
  getVersion: () => string = () => 'unknown',
  getPipelineActivityState: () => Readonly<PipelineActivity> | null = () => 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;
  } = () => ({
    bootState: 'booting',
    integrityState: 'unknown',
    integrityCheckInProgress: false,
    integrityStartedAt: null,
    integritySource: null,
    lastIntegrityCheckedAt: null,
    lastIntegrityDurationMs: null,
    lastIntegrityDetail: null,
    lastBackupAt: null,
  }),
  doctorSetConfig?: (config: FlywheelConfig) => void,
): void {
  // health_check - MCP server health status + periodic note detection + config
  const IndexProgressSchema = z.object({
    parsed: z.coerce.number().describe('Number of files parsed so far'),
    total: z.coerce.number().describe('Total number of files to parse'),
  }).optional();

  const PeriodicNoteInfoSchema = z.object({
    type: z.string(),
    detected: z.boolean(),
    folder: z.string().nullable(),
    pattern: z.string().nullable(),
    today_path: z.string().nullable(),
    today_exists: z.boolean(),
  });

  const HealthCheckOutputSchema = {
    status: z.enum(['healthy', 'degraded', 'unhealthy']).describe('Overall health status'),
    vault_health_score: z.coerce.number().describe('Composite vault health score (0-100)'),
    schema_version: z.coerce.number().describe('StateDb schema version'),
    vault_accessible: z.boolean().describe('Whether the vault path is accessible'),
    vault_path: z.string().describe('The vault path being used'),
    index_state: z.enum(['building', 'ready', 'error']).describe('Current state of the vault index'),
    index_progress: IndexProgressSchema.describe('Progress of index build (when building)'),
    index_error: z.string().optional().describe('Error message if index failed to build'),
    index_built: z.boolean().describe('Whether the index has been built'),
    index_age_seconds: z.coerce.number().describe('Seconds since the index was built'),
    index_stale: z.boolean().describe('Whether the index is stale (>5 minutes old)'),
    note_count: z.coerce.number().describe('Number of notes in the index'),
    entity_count: z.coerce.number().describe('Number of linkable entities (titles + aliases)'),
    tag_count: z.coerce.number().describe('Number of unique tags'),
    link_count: z.coerce.number().describe('Total number of outgoing wikilinks'),
    periodic_notes: z.array(PeriodicNoteInfoSchema).optional().describe('Detected periodic note conventions'),
    config: z.record(z.unknown()).optional().describe('Current flywheel config (paths, templates, etc.)'),
    last_rebuild: z.object({
      trigger: z.string(),
      timestamp: z.number(),
      duration_ms: z.number(),
      ago_seconds: z.number(),
    }).optional().describe('Most recent index rebuild event'),
    last_pipeline: z.object({
      timestamp: z.number(),
      trigger: z.string(),
      duration_ms: z.number(),
      files_changed: z.number().nullable(),
      changed_paths_total: z.number().describe('Total number of changed files'),
      changed_paths_sample: z.array(z.string()).describe('Up to 3 sample changed paths'),
      step_count: z.number().describe('Number of pipeline steps'),
      steps: z.array(z.object({
        name: z.string(),
        duration_ms: z.number(),
        skipped: z.boolean().optional(),
        skip_reason: z.string().optional(),
        summary: z.record(z.union([z.number(), z.boolean(), z.string()])),
      })).optional().describe('Compact step summaries (full mode only)'),
    }).optional().describe('Most recent watcher pipeline run'),
    recent_pipelines: z.array(z.object({
      timestamp: z.number(),
      trigger: z.string(),
      duration_ms: z.number(),
      files_changed: z.number().nullable(),
      changed_paths_total: z.number(),
      changed_paths_sample: z.array(z.string()),
      step_count: z.number(),
      steps: z.array(z.object({
        name: z.string(),
        duration_ms: z.number(),
        skipped: z.boolean().optional(),
        skip_reason: z.string().optional(),
        summary: z.record(z.union([z.number(), z.boolean(), z.string()])),
      })),
    })).optional().describe('Up to 5 most recent pipeline runs with compact step summaries (full mode only)'),
    fts5_ready: z.boolean().describe('Whether the FTS5 keyword search index is ready'),
    fts5_building: z.boolean().describe('Whether the FTS5 keyword search index is currently building'),
    embeddings_building: z.boolean().describe('Whether semantic embeddings are currently building'),
    embeddings_ready: z.boolean().describe('Whether semantic embeddings have been built (enables hybrid keyword+semantic search)'),
    embeddings_count: z.coerce.number().describe('Number of notes with semantic embeddings'),
    embedding_model: z.string().optional().describe('Active embedding model ID (when embeddings are built)'),
    embedding_diagnosis: z.object({
      healthy: z.boolean(),
      checks: z.array(z.object({
        name: z.string(),
        status: z.enum(['ok', 'stale', 'warning']),
        detail: z.string(),
      })),
      counts: z.object({
        embedded: z.coerce.number(),
        vaultNotes: z.coerce.number(),
        orphaned: z.coerce.number(),
        missing: z.coerce.number(),
      }),
    }).optional().describe('Detailed embedding health diagnosis (when embeddings exist)'),
    tasks_ready: z.boolean().describe('Whether the task cache is ready to serve queries'),
    tasks_building: z.boolean().describe('Whether the task cache is currently rebuilding'),
    watcher_state: z.enum(['starting', 'ready', 'rebuilding', 'dirty', 'error']).optional()
      .describe('Current file watcher state'),
    watcher_pending: z.coerce.number().optional()
      .describe('Number of pending file events in the watcher queue'),
    last_index_activity_at: z.number().optional()
      .describe('Epoch ms of latest successful index event (any trigger)'),
    last_index_activity_ago_seconds: z.coerce.number().optional()
      .describe('Seconds since last successful index event'),
    last_full_rebuild_at: z.number().optional()
      .describe('Epoch ms of latest startup_build or manual_refresh event'),
    last_watcher_batch_at: z.number().optional()
      .describe('Epoch ms of latest watcher batch event'),
    pipeline_activity: z.object({
      busy: z.boolean(),
      current_step: z.string().nullable(),
      started_at: z.number().nullable(),
      progress: z.string().nullable(),
      last_completed_ago_seconds: z.number().nullable(),
    }).optional().describe('Live pipeline activity state'),
    dead_link_count: z.coerce.number().optional().describe('Total number of broken/dead wikilinks across the vault (full mode only)'),
    top_dead_link_targets: z.array(z.object({
      target: z.string().describe('The dead link target'),
      mention_count: z.coerce.number().describe('How many notes reference this dead target'),
    })).optional().describe('Top 5 most-referenced dead link targets (highest-ROI candidates to create, full mode only)'),
    sweep: z.object({
      last_sweep_at: z.number().describe('When the last background sweep completed (ms epoch)'),
      sweep_duration_ms: z.number().describe('How long the last sweep took'),
      dead_link_count: z.number().describe('Dead links found by sweep'),
      top_dead_targets: z.array(z.object({
        target: z.string(),
        wikilink_references: z.number(),
        content_mentions: z.number(),
      })).describe('Top dead link targets with FTS5 content mention counts'),
      top_unlinked_entities: z.array(z.object({
        entity: z.string(),
        path: z.string(),
        unlinked_mentions: z.number(),
      })).describe('Entities with the most unlinked plain-text mentions'),
    }).optional().describe('Background sweep results (graph hygiene metrics, updated every 5 min)'),
    proactive_linking: z.object({
      enabled: z.boolean().describe('Whether proactive linking is enabled in config'),
      queue_pending: z.number().describe('Number of queued proactive suggestions awaiting drain'),
      summary: z.string().nullable().describe('One-liner: "12 links applied across 8 notes (11 survived, 92% rate)"'),
      total_applied_24h: z.number().describe('Total proactive applications in last 24h'),
      survived_24h: z.number().describe('Proactive links still present'),
      removed_24h: z.number().describe('Proactive links removed by user'),
      files_24h: z.number().describe('Distinct files touched by proactive linking'),
    }).optional().describe('Proactive linking observability (full mode only)'),
    recommendations: z.array(z.string()).describe('Suggested actions if any issues detected'),
  };

  type PeriodicNoteInfo = {
    type: string;
    detected: boolean;
    folder: string | null;
    pattern: string | null;
    today_path: string | null;
    today_exists: boolean;
  };

  type HealthCheckOutput = {
    status: 'healthy' | 'degraded' | 'unhealthy';
    vault_health_score: number;
    schema_version: number;
    vault_accessible: boolean;
    vault_path: string;
    index_state: IndexState;
    index_progress?: { parsed: number; total: number };
    index_error?: string;
    index_built: boolean;
    index_age_seconds: number;
    index_stale: boolean;
    note_count: number;
    entity_count: number;
    tag_count: number;
    link_count: number;
    periodic_notes?: PeriodicNoteInfo[];
    config?: Record<string, unknown>;
    last_rebuild?: {
      trigger: string;
      timestamp: number;
      duration_ms: number;
      ago_seconds: number;
    };
    last_pipeline?: {
      timestamp: number;
      trigger: string;
      duration_ms: number;
      files_changed: number | null;
      changed_paths_total: number;
      changed_paths_sample: string[];
      step_count: number;
      steps?: CompactStep[];
    };
    recent_pipelines?: CompactPipelineRun[];
    fts5_ready: boolean;
    fts5_building: boolean;
    embeddings_building: boolean;
    embeddings_ready: boolean;
    embeddings_count: number;
    embedding_model?: string;
    embedding_diagnosis?: {
      healthy: boolean;
      checks: Array<{ name: string; status: 'ok' | 'stale' | 'warning'; detail: string }>;
      counts: { embedded: number; vaultNotes: number; orphaned: number; missing: number };
    };
    tasks_ready: boolean;
    tasks_building: boolean;
    watcher_state?: 'starting' | 'ready' | 'rebuilding' | 'dirty' | 'error';
    boot_state: string;
    integrity_state: string;
    integrity_check_in_progress: boolean;
    integrity_started_at: number | null;
    integrity_source: string | null;
    integrity_last_checked_at: number | null;
    integrity_duration_ms: number | null;
    integrity_detail: string | null;
    watcher_pending?: number;
    last_index_activity_at?: number;
    last_index_activity_ago_seconds?: number;
    last_full_rebuild_at?: number;
    last_watcher_batch_at?: number;
    pipeline_activity?: {
      busy: boolean;
      current_step: string | null;
      started_at: number | null;
      progress: string | null;
      last_completed_ago_seconds: number | null;
    };
    dead_link_count?: number;
    top_dead_link_targets?: Array<{ target: string; mention_count: number }>;
    sweep?: SweepResults;
    proactive_linking?: {
      enabled: boolean;
      queue_pending: number;
      summary: string | null;
      total_applied_24h: number;
      survived_24h: number;
      removed_24h: number;
      files_24h: number;
    };
    recommendations: string[];
  };

  async function runHealthCheck(detail: 'summary' | 'full' = 'summary'): Promise<{
    content: Array<{ type: 'text'; text: string }>;
    structuredContent: HealthCheckOutput;
  }> {
    const isFull = detail === 'full';
    const index = getIndex();
    const vaultPath = getVaultPath();
    const recommendations: string[] = [];

    // Get index state info
    const indexState = getIndexState();
    const indexProgress = getIndexProgress();
    const indexErrorObj = getIndexError();

    // Check vault accessibility
    let vaultAccessible = false;
    try {
      fs.accessSync(vaultPath, fs.constants.R_OK);
      vaultAccessible = true;
    } catch {
      vaultAccessible = false;
      recommendations.push('Vault path is not accessible. Check PROJECT_PATH environment variable.');
    }

    // Check database integrity
    let dbIntegrityFailed = false;
    const stateDb = getStateDb();
    const runtimeState = getVaultRuntimeState();
    if (runtimeState.integrityState === 'failed') {
      dbIntegrityFailed = true;
      recommendations.push(`Database integrity check failed: ${runtimeState.lastIntegrityDetail ?? 'unknown integrity failure'}`);
    } else if (runtimeState.integrityState === 'error') {
      recommendations.push(`Database integrity check error: ${runtimeState.lastIntegrityDetail ?? 'integrity runner error'}`);
    } else if (runtimeState.integrityCheckInProgress) {
      recommendations.push('Database integrity check is still running.');
    }

    // Check index status
    const indexBuilt = indexState === 'ready' && index !== undefined && index.notes !== undefined;

    // Canonical timestamps from index_events
    let lastIndexActivityAt: number | undefined;
    let lastFullRebuildAt: number | undefined;
    let lastWatcherBatchAt: number | undefined;
    let lastBuild: ReturnType<typeof getLastEventByTrigger> | undefined;
    let lastManual: ReturnType<typeof getLastEventByTrigger> | undefined;
    if (stateDb) {
      try {
        const lastAny = getLastSuccessfulEvent(stateDb);
        if (lastAny) lastIndexActivityAt = lastAny.timestamp;
        lastBuild = getLastEventByTrigger(stateDb, 'startup_build') ?? undefined;
        lastManual = getLastEventByTrigger(stateDb, 'manual_refresh') ?? undefined;
        lastFullRebuildAt = Math.max(lastBuild?.timestamp ?? 0, lastManual?.timestamp ?? 0) || undefined;
        const lastWatcher = getLastEventByTrigger(stateDb, 'watcher');
        if (lastWatcher) lastWatcherBatchAt = lastWatcher.timestamp;
      } catch { /* ignore */ }
    }

    // Use last full rebuild for freshness, not watcher activity
    const freshnessTimestamp = lastFullRebuildAt ?? (indexBuilt && index.builtAt ? index.builtAt.getTime() : undefined);
    const indexAge = freshnessTimestamp
      ? Math.floor((Date.now() - freshnessTimestamp) / 1000)
      : -1;
    const indexStale = indexBuilt && indexAge > STALE_THRESHOLD_SECONDS;

    // Add state-specific recommendations
    if (indexState === 'building') {
      const { parsed, total } = indexProgress;
      const progress = total > 0 ? ` (${parsed}/${total} files)` : '';
      recommendations.push(`Index is building${progress}. Some tools may not be available yet.`);
    } else if (indexState === 'error') {
      recommendations.push(`Index failed to build: ${indexErrorObj?.message || 'unknown error'}`);
    } else if (indexStale) {
      recommendations.push(`Index is ${Math.floor(indexAge / 60)} minutes old. Consider running refresh_index.`);
    }

    // Count metrics (only if index is ready)
    const noteCount = indexBuilt ? index.notes.size : 0;
    const entityCount = indexBuilt ? index.entities.size : 0;
    const tagCount = indexBuilt ? index.tags.size : 0;
    let linkCount = 0;
    if (indexBuilt) {
      for (const note of index.notes.values()) linkCount += note.outlinks.length;
    }

    if (indexBuilt && noteCount === 0 && vaultAccessible) {
      recommendations.push('No notes found in vault. Is PROJECT_PATH pointing to a markdown vault?');
    }

    // Determine overall status
    let status: 'healthy' | 'degraded' | 'unhealthy';
    if (!vaultAccessible || indexState === 'error' || dbIntegrityFailed) {
      status = 'unhealthy';
    } else if (indexState === 'building' || indexStale || recommendations.length > 0) {
      status = 'degraded';
    } else {
      status = 'healthy';
    }

    // Detect periodic note conventions (only when index is ready, full mode only)
    let periodicNotes: PeriodicNoteInfo[] | undefined;
    if (isFull && indexBuilt) {
      const types = ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'] as const;
      periodicNotes = types.map(type => {
        const result = detectPeriodicNotes(index, type);
        return {
          type: result.type,
          detected: result.detected,
          folder: result.folder,
          pattern: result.pattern,
          today_path: result.today_path,
          today_exists: result.today_exists,
        };
      }).filter(p => p.detected);
    }

    // Include config info (full mode only)
    let configInfo: Record<string, unknown> | undefined;
    if (isFull) {
      const config = getConfig();
      configInfo = Object.keys(config).length > 0
        ? config as unknown as Record<string, unknown>
        : undefined;
    }

    // Get last rebuild event from StateDb
    let lastRebuild: HealthCheckOutput['last_rebuild'];
    if (stateDb) {
      try {
        const rebuildEvent = (lastBuild && lastManual)
          ? (lastBuild.timestamp >= lastManual.timestamp ? lastBuild : lastManual)
          : (lastBuild ?? lastManual);
        if (rebuildEvent) {
          lastRebuild = {
            trigger: rebuildEvent.trigger,
            timestamp: rebuildEvent.timestamp,
            duration_ms: rebuildEvent.duration_ms,
            ago_seconds: Math.floor((Date.now() - rebuildEvent.timestamp) / 1000),
          };
        }
      } catch {
        // Ignore errors reading index events
      }
    }

    // Get last pipeline run (most recent event with steps data — survives restarts)
    let lastPipeline: HealthCheckOutput['last_pipeline'];
    let recentPipelines: HealthCheckOutput['recent_pipelines'];
    if (stateDb) {
      try {
        const evt = getRecentPipelineEvent(stateDb);
        if (evt && evt.steps && evt.steps.length > 0) {
          const compact = compactPipelineRun(evt);
          if (isFull) {
            // Full mode: include compact step summaries
            lastPipeline = compact;
          } else {
            // Summary mode: metadata only, no steps array
            const { steps: _steps, ...metadataOnly } = compact;
            lastPipeline = metadataOnly;
          }
        }
      } catch {
        // Ignore errors reading pipeline data
      }

      // Recent pipeline events (last 5 with compact steps) — full mode only
      if (isFull) {
        try {
          const events = getRecentIndexEvents(stateDb, 10)
            .filter(e => e.steps && e.steps.length > 0)
            .slice(0, 5);
          if (events.length > 0) {
            recentPipelines = events.map(e => compactPipelineRun(e));
          }
        } catch {
          // Ignore errors reading recent pipeline data
        }
      }
    }

    const ftsState = getFTS5State();

    // Dead link scan — full mode only (iterates all outlinks)
    let deadLinkCount = 0;
    let topDeadLinkTargets: Array<{ target: string; mention_count: number }> = [];
    if (isFull && indexBuilt) {
      const deadTargetCounts = new Map<string, number>();
      for (const note of index.notes.values()) {
        for (const link of note.outlinks) {
          if (!resolveTarget(index, link.target)) {
            deadLinkCount++;
            const key = link.target.toLowerCase();
            deadTargetCounts.set(key, (deadTargetCounts.get(key) || 0) + 1);
          }
        }
      }
      topDeadLinkTargets = Array.from(deadTargetCounts.entries())
        .map(([target, mention_count]) => ({ target, mention_count }))
        .sort((a, b) => b.mention_count - a.mention_count)
        .slice(0, 5);
    }

    // Compute vault health score (0-100)
    let vault_health_score = 0;
    if (indexBuilt && noteCount > 0) {
      // Link density: avg outlinks per note, target 3+
      const avgOutlinks = linkCount / noteCount;
      const linkDensity = Math.min(1, avgOutlinks / 3);

      // Orphan ratio: notes with 0 backlinks (excluding periodic notes)
      let orphanCount = 0;
      for (const note of index.notes.values()) {
        const bl = index.backlinks.get(normalizeTarget(note.path));
        if (!bl || bl.length === 0) orphanCount++;
      }
      const orphanRatio = 1 - (orphanCount / noteCount);

      // Dead link ratio
      const totalLinks = linkCount > 0 ? linkCount : 1;
      const deadLinkRatio = 1 - (deadLinkCount / totalLinks);

      // Frontmatter coverage
      let notesWithFm = 0;
      for (const note of index.notes.values()) {
        if (Object.keys(note.frontmatter).length > 0) notesWithFm++;
      }
      const fmCoverage = notesWithFm / noteCount;

      // Freshness: notes modified in last 90 days
      const freshCutoff = Date.now() - 90 * 24 * 60 * 60 * 1000;
      let freshCount = 0;
      for (const note of index.notes.values()) {
        if (note.modified && note.modified.getTime() > freshCutoff) freshCount++;
      }
      const freshness = freshCount / noteCount;

      // Entity coverage: target 1 entity per 2 notes
      const entityCoverage = Math.min(1, entityCount / (noteCount * 0.5));

      // Weighted composite
      vault_health_score = Math.round(
        (linkDensity * 25 +
         orphanRatio * 20 +
         deadLinkRatio * 15 +
         fmCoverage * 15 +
         freshness * 15 +
         entityCoverage * 10)
      );
    }

    // Pipeline activity (always included — lightweight process-local read)
    const activity = getPipelineActivityState();
    const pipelineActivity = {
      busy: activity?.busy ?? false,
      current_step: activity?.current_step ?? null,
      started_at: activity?.started_at ?? null,
      progress: activity && activity.busy && activity.total_steps > 0
        ? `${activity.completed_steps}/${activity.total_steps} steps`
        : null,
      last_completed_ago_seconds: activity?.last_completed_at
        ? Math.floor((Date.now() - activity.last_completed_at) / 1000)
        : null,
    };

    const output: HealthCheckOutput = {
      status,
      vault_health_score,
      schema_version: SCHEMA_VERSION,
      vault_accessible: vaultAccessible,
      vault_path: vaultPath,
      index_state: indexState,
      index_progress: indexState === 'building' ? indexProgress : undefined,
      index_error: indexState === 'error' && indexErrorObj ? indexErrorObj.message : undefined,
      index_built: indexBuilt,
      index_age_seconds: indexAge,
      index_stale: indexStale,
      note_count: noteCount,
      entity_count: entityCount,
      tag_count: tagCount,
      link_count: linkCount,
      periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : undefined,
      config: configInfo,
      last_rebuild: lastRebuild,
      last_pipeline: lastPipeline,
      recent_pipelines: recentPipelines,
      fts5_ready: ftsState.ready,
      fts5_building: ftsState.building,
      embeddings_building: isEmbeddingsBuilding(),
      embeddings_ready: hasEmbeddingsIndex(),
      embeddings_count: getEmbeddingsCount(),
      embedding_model: hasEmbeddingsIndex() ? getActiveModelId() : undefined,
      embedding_diagnosis: isFull && hasEmbeddingsIndex() ? diagnoseEmbeddings(vaultPath) : undefined,
      tasks_ready: isTaskCacheReady(),
      tasks_building: isTaskCacheBuilding(),
      watcher_state: getWatcherStatus()?.state,
      boot_state: runtimeState.bootState,
      integrity_state: runtimeState.integrityState,
      integrity_check_in_progress: runtimeState.integrityCheckInProgress,
      integrity_started_at: runtimeState.integrityStartedAt,
      integrity_source: runtimeState.integritySource,
      integrity_last_checked_at: runtimeState.lastIntegrityCheckedAt,
      integrity_duration_ms: runtimeState.lastIntegrityDurationMs,
      integrity_detail: runtimeState.lastIntegrityDetail,
      watcher_pending: getWatcherStatus()?.pendingEvents,
      last_index_activity_at: lastIndexActivityAt,
      last_index_activity_ago_seconds: lastIndexActivityAt
        ? Math.floor((Date.now() - lastIndexActivityAt) / 1000) : undefined,
      last_full_rebuild_at: lastFullRebuildAt,
      last_watcher_batch_at: lastWatcherBatchAt,
      pipeline_activity: pipelineActivity,
      dead_link_count: isFull ? deadLinkCount : undefined,
      top_dead_link_targets: isFull ? topDeadLinkTargets : undefined,
      sweep: isFull ? (getSweepResults() ?? undefined) : undefined,
      proactive_linking: isFull && stateDb ? (() => {
        const config = getConfig();
        const enabled = config.proactive_linking !== false;
        const minScore = config.proactive_min_score ?? 20;
        const maxPerDay = config.proactive_max_per_day ?? 10;
        const queuePending = stateDb.db.prepare(
          `SELECT COUNT(*) as cnt FROM proactive_queue WHERE status = 'pending'`,
        ).get() as { cnt: number };
        const summary = getProactiveLinkingSummary(stateDb, 1);
        const oneLiner = getProactiveLinkingOneLiner(stateDb, 1);

        // Per-row pending breakdown: why each queued item hasn't applied yet.
        const pendingRows = stateDb.db.prepare(
          `SELECT note_path, entity, score, confidence, queued_at, expires_at
           FROM proactive_queue
           WHERE status = 'pending'
           ORDER BY queued_at DESC
           LIMIT 25`,
        ).all() as Array<{
          note_path: string;
          entity: string;
          score: number;
          confidence: string;
          queued_at: number;
          expires_at: number;
        }>;

        const now = Date.now();
        const pendingBreakdown = pendingRows.map(r => {
          const reasons: string[] = [];
          if (r.score < minScore) reasons.push(`score ${r.score} < min ${minScore}`);
          if (r.confidence !== 'high') reasons.push(`confidence=${r.confidence} (need high)`);
          if (r.expires_at <= now) reasons.push('expired');
          return {
            note_path: r.note_path,
            entity: r.entity,
            score: r.score,
            confidence: r.confidence,
            age_hours: Math.round((now - r.queued_at) / 3_600_000 * 10) / 10,
            likely_reasons: reasons.length > 0 ? reasons : ['passes filters — likely active_edit, daily_cap, or apply_empty (see last_drain)'],
          };
        });

        const lastDrain = getWriteState<{
          at: number;
          total_applied: number;
          applied_files: number;
          expired: number;
          skipped_active: number;
          skipped_mtime: number;
          skipped_daily_cap: number;
          rejection_count: number;
          rejection_sample: Array<Record<string, unknown>>;
          rejection_breakdown?: Record<string, number>;
        }>(stateDb, 'last_proactive_drain');

        return {
          enabled,
          queue_pending: queuePending.cnt,
          min_score: minScore,
          max_per_day: maxPerDay,
          summary: oneLiner,
          total_applied_24h: summary.total_applied,
          survived_24h: summary.survived,
          removed_24h: summary.removed,
          files_24h: summary.files_touched,
          pending_breakdown: pendingBreakdown,
          last_drain: lastDrain ? {
            at: lastDrain.at,
            age_minutes: Math.round((now - lastDrain.at) / 60_000),
            total_applied: lastDrain.total_applied,
            applied_files: lastDrain.applied_files,
            expired: lastDrain.expired,
            skipped_active: lastDrain.skipped_active,
            skipped_mtime: lastDrain.skipped_mtime,
            skipped_daily_cap: lastDrain.skipped_daily_cap,
            rejection_count: lastDrain.rejection_count,
            rejection_breakdown: lastDrain.rejection_breakdown ?? {},
            rejection_sample: lastDrain.rejection_sample,
          } : null,
        };
      })() : undefined,
      recommendations,
    };

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(output, null, 2),
        },
      ],
      structuredContent: output,
    };
  }

  // pipeline_status retired (T43 B3+) — use doctor(action: pipeline) instead

  // get_vault_stats - Comprehensive vault statistics
  const TagStatSchema = z.object({
    tag: z.string().describe('The tag name'),
    count: z.coerce.number().describe('Number of notes with this tag'),
  });

  const FolderStatSchema = z.object({
    folder: z.string().describe('Folder path'),
    note_count: z.coerce.number().describe('Number of notes in this folder'),
  });

  const OrphanStatsSchema = z.object({
    total: z.coerce.number().describe('Total orphan notes (no backlinks)'),
    periodic: z.coerce.number().describe('Orphan periodic notes (daily/weekly/monthly - expected)'),
    content: z.coerce.number().describe('Orphan content notes (non-periodic - may need linking)'),
  });

  const GetVaultStatsOutputSchema = {
    total_notes: z.coerce.number().describe('Total number of notes in the vault'),
    total_links: z.coerce.number().describe('Total number of wikilinks'),
    total_tags: z.coerce.number().describe('Total number of unique tags'),
    orphan_notes: OrphanStatsSchema.describe('Orphan notes breakdown'),
    broken_links: z.coerce.number().describe('Links pointing to non-existent notes'),
    average_links_per_note: z.coerce.number().describe('Average outgoing links per note'),
    most_linked_notes: z
      .array(
        z.object({
          path: z.string(),
          backlinks: z.number(),
        })
      )
      .describe('Top 10 most linked-to notes'),
    top_tags: z.array(TagStatSchema).describe('Top 20 most used tags'),
    folders: z.array(FolderStatSchema).describe('Note counts by top-level folder'),
    recent_activity: z.object({
      period_days: z.number(),
      notes_modified: z.number(),
      notes_created: z.number(),
      most_active_day: z.string().nullable(),
      daily_counts: z.record(z.number()),
    }).describe('Activity summary for the last 7 days'),
  };

  type VaultStatsOutput = {
    total_notes: number;
    total_links: number;
    total_tags: number;
    orphan_notes: {
      total: number;
      periodic: number;
      content: number;
    };
    broken_links: number;
    average_links_per_note: number;
    most_linked_notes: Array<{ path: string; backlinks: number }>;
    top_tags: Array<{ tag: string; count: number }>;
    folders: Array<{ folder: string; note_count: number }>;
    recent_activity: {
      period_days: number;
      notes_modified: number;
      notes_created: number;
      most_active_day: string | null;
      daily_counts: Record<string, number>;
    };
  };

  /**
   * Check if a note is a periodic note (daily, weekly, monthly, quarterly, yearly).
   * Periodic notes naturally have fewer backlinks - they're time-based, not topic-based.
   */
  function isPeriodicNote(path: string): boolean {
    const filename = path.split('/').pop() || '';
    const nameWithoutExt = filename.replace(/\.md$/, '');

    // Date patterns for periodic notes
    const patterns = [
      /^\d{4}-\d{2}-\d{2}$/,           // YYYY-MM-DD (daily)
      /^\d{4}-W\d{2}$/,                // YYYY-Wnn (weekly)
      /^\d{4}-\d{2}$/,                 // YYYY-MM (monthly)
      /^\d{4}-Q[1-4]$/,                // YYYY-Qn (quarterly)
      /^\d{4}$/,                       // YYYY (yearly)
    ];

    // Also check common folder names
    const periodicFolders = ['daily', 'weekly', 'monthly', 'quarterly', 'yearly', 'journal', 'journals'];
    const folder = path.split('/')[0]?.toLowerCase() || '';

    return patterns.some(p => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
  }

  async function runVaultStats(): Promise<{
    content: Array<{ type: 'text'; text: string }>;
    structuredContent: VaultStatsOutput;
  }> {
    const index = getIndex();

    // Count totals
    const totalNotes = index.notes.size;
    let totalLinks = 0;
    let brokenLinks = 0;
    let orphanTotal = 0;
    let orphanPeriodic = 0;
    let orphanContent = 0;

    // Count links and broken links (only count as broken if similar entity exists)
    for (const note of index.notes.values()) {
      totalLinks += note.outlinks.length;

      for (const link of note.outlinks) {
        if (!resolveTarget(index, link.target)) {
          // Only count as broken if there's a similar entity (typo detection)
          const similar = findSimilarEntity(index, link.target);
          if (similar) {
            brokenLinks++;
          }
        }
      }
    }

    // Count orphans, separating periodic notes from content notes
    for (const note of index.notes.values()) {
      const backlinks = getBacklinksForNote(index, note.path);
      if (backlinks.length === 0) {
        orphanTotal++;
        if (isPeriodicNote(note.path)) {
          orphanPeriodic++;
        } else {
          orphanContent++;
        }
      }
    }

    // Calculate most linked notes
    const linkCounts: Array<{ path: string; backlinks: number }> = [];
    for (const note of index.notes.values()) {
      const backlinks = getBacklinksForNote(index, note.path);
      if (backlinks.length > 0) {
        linkCounts.push({ path: note.path, backlinks: backlinks.length });
      }
    }
    linkCounts.sort((a, b) => b.backlinks - a.backlinks);
    const mostLinkedNotes = linkCounts.slice(0, 10);

    // Calculate top tags
    const tagStats: Array<{ tag: string; count: number }> = [];
    for (const [tag, notes] of index.tags) {
      tagStats.push({ tag, count: notes.size });
    }
    tagStats.sort((a, b) => b.count - a.count);
    const topTags = tagStats.slice(0, 20);

    // Calculate folder distribution
    const folderCounts = new Map<string, number>();
    for (const note of index.notes.values()) {
      const parts = note.path.split('/');
      const folder = parts.length > 1 ? parts[0] : '(root)';

      folderCounts.set(folder, (folderCounts.get(folder) || 0) + 1);
    }

    const folders = Array.from(folderCounts.entries())
      .map(([folder, count]) => ({ folder, note_count: count }))
      .sort((a, b) => b.note_count - a.note_count);

    // Get recent activity summary (last 7 days)
    const recentActivity = getActivitySummary(index, 7);

    const output: VaultStatsOutput = {
      total_notes: totalNotes,
      total_links: totalLinks,
      total_tags: index.tags.size,
      orphan_notes: {
        total: orphanTotal,
        periodic: orphanPeriodic,
        content: orphanContent,
      },
      broken_links: brokenLinks,
      average_links_per_note: totalNotes > 0 ? Math.round((totalLinks / totalNotes) * 100) / 100 : 0,
      most_linked_notes: mostLinkedNotes,
      top_tags: topTags,
      folders,
      recent_activity: recentActivity,
    };

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(output, null, 2),
        },
      ],
      structuredContent: output,
    };
  }

  // server_log — Query the in-memory activity log
  const LogEntrySchema = z.object({
    ts: z.number().describe('Unix timestamp (ms)'),
    component: z.string().describe('Source component'),
    message: z.string().describe('Log message'),
    level: z.enum(['info', 'warn', 'error']).describe('Log level'),
  });

  const ServerLogOutputSchema = {
    entries: z.array(LogEntrySchema).describe('Log entries (oldest first)'),
    server_uptime_ms: z.coerce.number().describe('Server uptime in milliseconds'),
  };

  type ServerLogOutput = {
    entries: LogEntry[];
    server_uptime_ms: number;
  };

  // server_log retired (T43 B3+) — use doctor(action: log) instead

  // flywheel_doctor retired (T43 B3+) — use doctor(action: health) instead

  // flywheel_trust_report retired (T43) — functionality absorbed into flywheel_doctor
  // flywheel_benchmark retired (T43)

  // doctor — Merged diagnostic/config tool (T43 B3+)
  // Absorbs: flywheel_doctor (health) + pipeline_status + server_log + flywheel_config
  server.registerTool(
    'doctor',
    {
      title: 'Doctor',
      description: `Use for diagnostics and runtime management. Returns vault health, named checks, stats, pipeline status, server log, or config. Does not read note content — use read or search for that. action: health | diagnosis | stats | pipeline | config | log`,
      inputSchema: {
        action: z.enum(['health', 'diagnosis', 'stats', 'pipeline', 'config', 'log'])
          .describe('Operation: health=vault status, pipeline=indexing status, config=runtime config, log=activity log'),
        // [health]
        detail: z.union([z.enum(['summary', 'full']), z.boolean()]).optional()
          .describe('[health] Detail level: summary or full. [pipeline] true = include per-step timings'),
        // [config]
        mode: z.enum(['get', 'set']).optional().describe('[config] Operation: get or set'),
        key: z.string().optional().describe('[config] Config key to update (required for set)'),
        value: z.unknown().optional().describe('[config] New value for the key (required for set)'),
        // [log]
        since: z.coerce.number().optional().describe('[log] Only return entries after this Unix timestamp (ms)'),
        component: z.string().optional().describe('[log] Filter by component (server, index, fts5, semantic, tasks, watcher, statedb, config)'),
        limit: z.coerce.number().optional().describe('[log] Max entries to return (default 100)'),
      },
    },
    async ({ action, detail, mode, key, value, since, component, limit }) => {
      switch (action) {
        case 'health': {
          const detailLevel = (typeof detail === 'string' ? detail : 'summary') as 'summary' | 'full';
          return runHealthCheck(detailLevel);
        }

        case 'diagnosis': {
          // Named checks array — port of old flywheel_doctor(report: diagnosis) default behaviour
          const checks: Array<{
            name: string;
            status: 'ok' | 'warning' | 'error';
            detail: string;
            fix?: string;
          }> = [];

          const diagIndex = getIndex();
          const diagVaultPath = getVaultPath();
          const diagStateDb = getStateDb();
          const diagWatcherStatus = getWatcherStatus();

          // 1. Schema version
          checks.push({ name: 'schema_version', status: 'ok', detail: `Schema version ${SCHEMA_VERSION}` });

          // 2. Vault accessibility
          try {
            fs.accessSync(diagVaultPath, fs.constants.R_OK | fs.constants.W_OK);
            checks.push({ name: 'vault_access', status: 'ok', detail: `Vault readable and writable at ${diagVaultPath}` });
          } catch {
            checks.push({ name: 'vault_access', status: 'error', detail: `Vault not accessible at ${diagVaultPath}`, fix: 'Check PROJECT_PATH environment variable and directory permissions' });
          }

          // 3. Index activity freshness
          const diagIndexState = getIndexState();
          const diagIndexBuilt = diagIndexState === 'ready' && diagIndex?.notes !== undefined;
          if (diagIndexState === 'ready' && diagIndexBuilt) {
            let activityAge: number | null = null;
            if (diagStateDb) {
              try {
                const lastEvt = getLastSuccessfulEvent(diagStateDb);
                if (lastEvt) activityAge = Math.floor((Date.now() - lastEvt.timestamp) / 1000);
              } catch { /* ignore */ }
            }
            const age = activityAge ?? Math.floor((Date.now() - diagIndex.builtAt.getTime()) / 1000);
            if (age > STALE_THRESHOLD_SECONDS) {
              checks.push({ name: 'index_activity', status: 'warning', detail: `Last index activity ${Math.floor(age / 60)} minutes ago`, fix: 'Run refresh_index to rebuild' });
            } else {
              checks.push({ name: 'index_activity', status: 'ok', detail: `Last activity ${age}s ago, ${diagIndex.notes.size} notes, ${diagIndex.entities.size} entities` });
            }
          } else if (diagIndexState === 'building') {
            const progress = getIndexProgress();
            checks.push({ name: 'index_activity', status: 'warning', detail: `Index building (${progress.parsed}/${progress.total} files)` });
          } else {
            const err = getIndexError();
            checks.push({ name: 'index_activity', status: 'error', detail: `Index in ${diagIndexState} state${err ? ': ' + err.message : ''}`, fix: 'Run refresh_index' });
          }

          // 4. Embedding coverage
          const embReady = hasEmbeddingsIndex();
          const embCount = getEmbeddingsCount();
          const diagNoteCount = diagIndexBuilt ? diagIndex.notes.size : 0;
          if (embReady && diagNoteCount > 0) {
            const coverage = Math.round((embCount / diagNoteCount) * 100);
            if (coverage < 50) {
              checks.push({ name: 'embedding_coverage', status: 'warning', detail: `${embCount}/${diagNoteCount} notes embedded (${coverage}%)`, fix: 'Run init_semantic with force=true to rebuild' });
            } else {
              checks.push({ name: 'embedding_coverage', status: 'ok', detail: `${embCount}/${diagNoteCount} notes embedded (${coverage}%), model: ${getActiveModelId() || 'default'}` });
            }
            // Entity embeddings
            const entityEmbCount = getEntityEmbeddingsCount();
            const diagEntityCount = (() => {
              if (!diagStateDb) return 0;
              try {
                return (diagStateDb.db.prepare('SELECT COUNT(*) as cnt FROM entities').get() as { cnt: number })?.cnt ?? 0;
              } catch { return 0; }
            })();
            if (diagEntityCount > 0) {
              const entityCoverage = Math.round((entityEmbCount / diagEntityCount) * 100);
              checks.push({ name: 'entity_embedding_coverage', status: entityCoverage < 50 ? 'warning' : 'ok', detail: `${entityEmbCount}/${diagEntityCount} canonical entities embedded (${entityCoverage}%)` });
            }
          } else if (!embReady) {
            checks.push({ name: 'embedding_coverage', status: 'warning', detail: 'Semantic embeddings not built', fix: 'Run init_semantic to enable hybrid search' });
          } else if (isEmbeddingsBuilding()) {
            checks.push({ name: 'embedding_coverage', status: 'warning', detail: 'Embedding build in progress' });
          }

          // 5. FTS5 state
          const fts = getFTS5State();
          if (fts.ready) {
            checks.push({ name: 'fts5', status: 'ok', detail: `FTS5 ready, ${fts.noteCount ?? 0} notes indexed` });
          } else if (fts.building) {
            checks.push({ name: 'fts5', status: 'warning', detail: 'FTS5 index building' });
          } else {
            checks.push({ name: 'fts5', status: 'error', detail: 'FTS5 not available', fix: 'Will build automatically on next index rebuild' });
          }

          // 6. Watcher state
          if (diagWatcherStatus) {
            if (diagWatcherStatus.state === 'ready') {
              checks.push({ name: 'watcher', status: 'ok', detail: `Watcher running, ${diagWatcherStatus.pendingEvents ?? 0} pending events` });
            } else if (diagWatcherStatus.state === 'error') {
              checks.push({ name: 'watcher', status: 'error', detail: 'Watcher in error state', fix: 'Restart the MCP server' });
            } else {
              checks.push({ name: 'watcher', status: 'warning', detail: `Watcher state: ${diagWatcherStatus.state}${diagWatcherStatus.pendingEvents ? `, ${diagWatcherStatus.pendingEvents} pending` : ''}` });
            }
          }

          // 7. Task cache
          if (isTaskCacheReady()) {
            checks.push({ name: 'task_cache', status: 'ok', detail: 'Task cache ready' });
          } else if (isTaskCacheBuilding()) {
            checks.push({ name: 'task_cache', status: 'warning', detail: 'Task cache building' });
          } else {
            checks.push({ name: 'task_cache', status: 'warning', detail: 'Task cache not ready' });
          }

          // 8. Suppression health
          if (diagStateDb) {
            try {
              const suppressedCount = getSuppressedCount(diagStateDb);
              const stats = getEntityStats(diagStateDb);
              const diagEntityCount2 = diagIndexBuilt ? diagIndex.entities.size : 0;
              if (diagEntityCount2 > 0 && suppressedCount > diagEntityCount2 * 0.2) {
                checks.push({ name: 'suppression_health', status: 'warning', detail: `${suppressedCount} entities suppressed (${Math.round(suppressedCount / diagEntityCount2 * 100)}% of total)`, fix: 'Review suppressed entities — high suppression rate may indicate overly aggressive feedback' });
              } else {
                checks.push({ name: 'suppression_health', status: 'ok', detail: `${suppressedCount} entities suppressed, ${stats.length} entities with feedback` });
              }
            } catch {
              checks.push({ name: 'suppression_health', status: 'ok', detail: 'No suppression data yet' });
            }
          }

          // 9. Pipeline health
          if (diagStateDb) {
            try {
              const evt = getRecentPipelineEvent(diagStateDb);
              if (evt) {
                const ageMin = Math.round((Date.now() - evt.timestamp) / 60000);
                const failedSteps = evt.steps?.filter((s: PipelineStep) => s.skipped && s.skip_reason?.includes('error')) || [];
                if (failedSteps.length > 0) {
                  checks.push({ name: 'pipeline', status: 'warning', detail: `Last pipeline ${ageMin}min ago (${evt.duration_ms}ms), ${failedSteps.length} failed steps: ${failedSteps.map((s: PipelineStep) => s.name).join(', ')}` });
                } else {
                  checks.push({ name: 'pipeline', status: 'ok', detail: `Last pipeline ${ageMin}min ago, ${evt.duration_ms}ms, ${evt.steps?.length ?? 0} steps` });
                }
              }
            } catch {
              checks.push({ name: 'pipeline', status: 'ok', detail: 'No pipeline data' });
            }
          }

          // 10. Proactive queue stall detection
          if (diagStateDb) {
            try {
              const queuePending = diagStateDb.db.prepare(
                `SELECT COUNT(*) as cnt FROM proactive_queue WHERE status = 'pending'`,
              ).get() as { cnt: number };
              const proactiveSummary = getProactiveLinkingSummary(diagStateDb, 1);
              const lastDrain = getWriteState<{
                rejection_count: number;
                rejection_breakdown?: Record<string, number>;
              }>(diagStateDb, 'last_proactive_drain');

              if (queuePending.cnt > 0 && proactiveSummary.total_applied === 0) {
                const top = Object.entries(lastDrain?.rejection_breakdown ?? {})
                  .sort(([, a], [, b]) => b - a)
                  .slice(0, 2)
                  .map(([reason, count]) => `${reason} (${count})`)
                  .join(', ');
                checks.push({
                  name: 'proactive_queue',
                  status: 'warning',
                  detail: `${queuePending.cnt} pending, 0 applied in 24h. Top rejections: ${top || 'unknown'}`,
                  fix: 'Lower proactive_min_score or inspect via doctor(action: pipeline)',
                });
              } else {
                checks.push({
                  name: 'proactive_queue',
                  status: 'ok',
                  detail: `${queuePending.cnt} pending, ${proactiveSummary.total_applied} applied in 24h`,
                });
              }
            } catch { /* skip */ }
          }

          const errorCount = checks.filter(c => c.status === 'error').length;
          const warningCount = checks.filter(c => c.status === 'warning').length;
          const overallStatus = errorCount > 0 ? 'unhealthy' : warningCount > 0 ? 'needs_attention' : 'healthy';

          const diagOutput = {
            status: overallStatus,
            summary: `${checks.length} checks: ${checks.length - errorCount - warningCount} ok, ${warningCount} warnings, ${errorCount} errors`,
            checks,
            fixes: checks.filter(c => c.fix).map(c => ({ check: c.name, fix: c.fix })),
          };
          return { content: [{ type: 'text' as const, text: JSON.stringify(diagOutput, null, 2) }] };
        }

        case 'pipeline': {
          const includeDetail = typeof detail === 'boolean' ? detail : false;
          const activity = getPipelineActivityState();
          const now = Date.now();
          const runtimeState = getVaultRuntimeState();

          const output: Record<string, unknown> = {
            busy: activity?.busy ?? false,
            trigger: activity?.trigger ?? null,
            started_at: activity?.started_at ?? null,
            age_ms: activity?.busy && activity.started_at ? now - activity.started_at : null,
            current_step: activity?.current_step ?? null,
            progress: activity && activity.busy && activity.total_steps > 0
              ? `${activity.completed_steps}/${activity.total_steps} steps`
              : null,
            pending_events: activity?.pending_events ?? 0,
            boot_state: runtimeState.bootState,
            integrity_state: runtimeState.integrityState,
            integrity_check_in_progress: runtimeState.integrityCheckInProgress,
            last_completed: activity?.last_completed_at ? {
              at: activity.last_completed_at,
              ago_seconds: Math.floor((now - activity.last_completed_at) / 1000),
              trigger: activity.last_completed_trigger,
              duration_ms: activity.last_completed_duration_ms,
              files: activity.last_completed_files,
              steps: activity.last_completed_steps,
            } : null,
          };

          if (includeDetail) {
            const stateDb = getStateDb();
            if (stateDb) {
              try {
                const events = getRecentIndexEvents(stateDb, 10)
                  .filter(e => e.steps && e.steps.length > 0)
                  .slice(0, 5);
                output.recent_runs = events.map(e => compactPipelineRun(e));
              } catch { /* ignore */ }
            }
          }

          return { content: [{ type: 'text' as const, text: JSON.stringify(output, null, 2) }] };
        }

        case 'config': {
          if (!mode || mode === 'get') {
            return { content: [{ type: 'text' as const, text: JSON.stringify(getConfig(), null, 2) }] };
          }
          // mode === 'set'
          if (!key) {
            return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'key is required for set mode' }) }] };
          }
          const configSchema = VALID_CONFIG_KEYS[key];
          if (!configSchema) {
            return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Unknown config key: "${key}". Valid keys: ${Object.keys(VALID_CONFIG_KEYS).join(', ')}` }) }] };
          }
          const parsed = configSchema.safeParse(value);
          if (!parsed.success) {
            return { content: [{ type: 'text' as const, text: JSON.stringify({ error: `Invalid value for "${key}": ${parsed.error.message}` }) }] };
          }
          const stateDb = getStateDb();
          if (!stateDb) {
            return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'StateDb not available' }) }] };
          }
          const updated: FlywheelConfig = { ...getConfig(), [key]: parsed.data };
          saveFlywheelConfigToDb(stateDb, updated as unknown as Record<string, unknown>);
          const reloaded = loadConfig(stateDb);
          if (doctorSetConfig) doctorSetConfig(reloaded);
          if (key === 'tool_tier_override') {
            return {
              content: [{
                type: 'text' as const,
                text: JSON.stringify({
                  config: reloaded,
                  warning: 'tool_tier_override is deprecated and has no runtime effect. Use agent, power, or full presets instead.',
                }, null, 2),
              }],
            };
          }
          return { content: [{ type: 'text' as const, text: JSON.stringify(reloaded, null, 2) }] };
        }

        case 'stats': {
          return runVaultStats();
        }

        case 'log': {
          const result = getServerLog({ since, component, limit });
          return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
        }
      }
    }
  );

}
