/**
 * System and utility tools - infrastructure primitives for v2.0
 *
 * Tools: refresh_index, list_entities, suggest_entity_aliases, unlinked_mentions_report
 */

import { z } from 'zod';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { VaultIndex } from '../../core/read/types.js';
import { buildVaultIndex, setIndexState, setIndexError, saveVaultIndexToCache } from '../../core/read/graph.js';
import { loadConfig, inferConfig, saveConfig, DEFAULT_ENTITY_EXCLUDE_FOLDERS, getExcludeTags, type FlywheelConfig } from '../../core/read/config.js';
import { buildFTS5Index } from '../../core/read/fts5.js';
import { scanVaultEntities, getEntityIndexFromDb, getAllEntitiesFromDb, type StateDb } from '@velvetmonkey/vault-core';
import { suggestEntityAliases } from '../../core/read/aliasSuggestions.js';
import { createStepTracker, recordIndexEvent } from '../../core/shared/indexActivity.js';
import { MAX_LIMIT } from '../../core/read/constants.js';
import { requireIndex } from '../../core/read/indexGuard.js';
import { countFTS5Mentions } from '../../core/read/fts5.js';
import { recomputeEdgeWeights } from '../../core/write/edgeWeights.js';
import {
  hasEmbeddingsIndex,
  buildEmbeddingsIndex,
  buildEntityEmbeddingsIndex,
  hasEntityEmbeddingsIndex,
  loadEntityEmbeddingsToMemory,
  classifyUncategorizedEntities,
  saveInferredCategories,
  getInferredCategory,
} from '../../core/read/embeddings.js';
import { initializeEntityIndex, setCooccurrenceIndex } from '../../core/write/wikilinks.js';
import { exportHubScores } from '../../core/shared/hubExport.js';
import { computeGraphMetrics, recordGraphSnapshot } from '../../core/shared/graphSnapshots.js';
import { updateSuppressionList, updateStoredNoteLinks } from '../../core/write/wikilinkFeedback.js';
import { buildTaskCache } from '../../core/read/taskCache.js';
import { buildRecencyIndex, saveRecencyToStateDb } from '../../core/shared/recency.js';
import { mineCooccurrences, saveCooccurrenceToStateDb } from '../../core/shared/cooccurrence.js';

/**
 * Register system/utility tools with the MCP server
 */
export function registerSystemTools(
  server: McpServer,
  getIndex: () => VaultIndex,
  setIndex: (index: VaultIndex) => void,
  getVaultPath: () => string,
  setConfig?: (config: FlywheelConfig) => void,
  getStateDb?: () => StateDb | null
) {
  // refresh_index - Rebuild vault index + FTS5 search index without server restart
  const RefreshIndexOutputSchema = {
    success: z.boolean().describe('Whether the refresh succeeded'),
    degraded: z.boolean().optional().describe('True when a non-fatal step failed (e.g. FTS5 rebuild) — index rebuilt but search may be impaired'),
    fts5_rebuild_error: z.string().optional().describe('Error message when the FTS5 rebuild step failed'),
    notes_count: z.number().describe('Number of notes indexed'),
    entities_count: z.number().describe('Number of entities (titles + aliases)'),
    fts5_notes: z.number().describe('Number of notes in FTS5 search index'),
    edges_recomputed: z.number().optional().describe('Number of edges with recomputed weights'),
    note_links_synced: z.number().optional().describe('Number of notes whose note_links were synced'),
    hub_scores: z.number().optional().describe('Number of hub scores exported'),
    graph_snapshot: z.boolean().optional().describe('Whether graph topology snapshot was recorded'),
    suppression_list: z.boolean().optional().describe('Whether wikilink suppression list was updated'),
    task_cache: z.boolean().optional().describe('Whether task cache was refreshed'),
    embeddings_refreshed: z.number().optional().describe('Number of note embeddings updated'),
    entity_embeddings_refreshed: z.number().optional().describe('Number of entity embeddings updated'),
    recency_rebuilt: z.boolean().optional().describe('Whether recency index was rebuilt'),
    cooccurrence_associations: z.number().optional().describe('Number of co-occurrence associations rebuilt'),
    index_cached: z.boolean().optional().describe('Whether vault index cache was saved'),
    duration_ms: z.number().describe('Time taken to rebuild index'),
  };

  type RefreshIndexOutput = {
    success: boolean;
    degraded?: boolean;
    fts5_rebuild_error?: string;
    notes_count: number;
    entities_count: number;
    fts5_notes: number;
    edges_recomputed?: number;
    note_links_synced?: number;
    hub_scores?: number;
    graph_snapshot?: boolean;
    suppression_list?: boolean;
    task_cache?: boolean;
    embeddings_refreshed?: number;
    entity_embeddings_refreshed?: number;
    recency_rebuilt?: boolean;
    cooccurrence_associations?: number;
    index_cached?: boolean;
    duration_ms: number;
  };

  server.registerTool(
    'refresh_index',
    {
      title: 'Refresh Index',
      description:
        'Use when the vault index seems stale or after bulk external edits. Produces a full rebuild of the vault index and FTS5 search database. Returns rebuild duration and note count. Does not restart the server — only rebuilds the in-memory index.',
      inputSchema: {},
      outputSchema: RefreshIndexOutputSchema,
    },
    async (): Promise<{
      content: Array<{ type: 'text'; text: string }>;
      structuredContent: RefreshIndexOutput;
    }> => {
      const vaultPath = getVaultPath();
      const startTime = Date.now();
      const tracker = createStepTracker();

      // Mark index as building during refresh
      setIndexState('building');
      setIndexError(null);

      try {
        // Step 1: Rebuild vault index
        tracker.start('vault_index', {});
        const newIndex = await buildVaultIndex(vaultPath);
        setIndex(newIndex);
        setIndexState('ready');
        tracker.end({ notes: newIndex.notes.size, entities: newIndex.entities.size });

        // Step 2: Update entities in StateDb
        const stateDb = getStateDb?.();
        if (stateDb) {
          tracker.start('entity_sync', {});
          try {
            const config = loadConfig(stateDb);
            const excludeFolders = config.exclude_entity_folders?.length
              ? config.exclude_entity_folders
              : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
            const entityIndex = await scanVaultEntities(vaultPath, {
              excludeFolders,
              customCategories: config.custom_categories,
            });
            stateDb.replaceAllEntities(entityIndex);
            tracker.end({ entities: entityIndex._metadata.total_entities });
            console.error(`[Flywheel] Updated ${entityIndex._metadata.total_entities} entities in StateDb`);
          } catch (e) {
            tracker.end({ error: String(e) });
            console.error('[Flywheel] Failed to update entities:', e);
          }
        }

        // Step 3: Infer config from vault, merge with existing, save
        let flywheelConfig: FlywheelConfig | undefined;
        if (setConfig) {
          tracker.start('config_merge', {});
          const existing = loadConfig(stateDb);
          const inferred = inferConfig(newIndex, vaultPath);
          if (stateDb) {
            saveConfig(stateDb, inferred, existing);
          }
          flywheelConfig = loadConfig(stateDb);
          setConfig(flywheelConfig);
          tracker.end({});
        }

        // Step 4: Rebuild FTS5 search index
        let fts5Notes = 0;
        let fts5RebuildError: string | undefined;
        tracker.start('fts5_rebuild', {});
        try {
          const ftsState = await buildFTS5Index(vaultPath);
          fts5Notes = ftsState.noteCount;
          tracker.end({ notes: fts5Notes });
          console.error(`[Flywheel] FTS5 index rebuilt: ${fts5Notes} notes`);
        } catch (err) {
          fts5RebuildError = err instanceof Error ? err.message : String(err);
          tracker.end({ error: fts5RebuildError });
          console.error('[Flywheel] FTS5 rebuild failed:', err);
        }
        // A populated vault with an empty FTS index after "rebuild" is a failed
        // rebuild even if buildFTS5Index didn't throw — never report it as a
        // clean success (the silent version of this once cascaded into a full
        // embedding wipe via orphan cleanup).
        if (!fts5RebuildError && fts5Notes === 0 && newIndex.notes.size > 0) {
          fts5RebuildError = `FTS5 index empty after rebuild despite ${newIndex.notes.size} indexed notes`;
          console.error(`[Flywheel] ${fts5RebuildError}`);
        }


        // Step 4b: Sync note_links from rebuilt VaultIndex
        let noteLinksSynced = 0;
        if (stateDb) {
          tracker.start('note_links_sync', {});
          try {
            const indexPaths = new Set<string>();
            for (const [notePath, note] of newIndex.notes) {
              indexPaths.add(notePath);
              const targets = new Set(note.outlinks.map(link => link.target.toLowerCase()));
              updateStoredNoteLinks(stateDb, notePath, targets);
              noteLinksSynced++;
            }
            // Remove stale source rows for notes no longer in the index
            const dbSourcePaths = stateDb.db.prepare(
              'SELECT DISTINCT note_path FROM note_links'
            ).all() as Array<{ note_path: string }>;
            for (const row of dbSourcePaths) {
              if (!indexPaths.has(row.note_path)) {
                stateDb.db.prepare('DELETE FROM note_links WHERE note_path = ?').run(row.note_path);
              }
            }
            tracker.end({ notes: noteLinksSynced });
            console.error(`[Flywheel] note_links synced for ${noteLinksSynced} notes`);
          } catch (err) {
            tracker.end({ error: String(err) });
            console.error('[Flywheel] note_links sync failed:', err);
          }
        }
        // Step 5: Recompute edge weights
        let edgesRecomputed = 0;
        if (stateDb) {
          tracker.start('edge_weights', {});
          try {
            const edgeResult = recomputeEdgeWeights(stateDb);
            edgesRecomputed = edgeResult.edges_updated;
            tracker.end({ edges: edgeResult.edges_updated, duration_ms: edgeResult.duration_ms });
            console.error(`[Flywheel] Edge weights: ${edgeResult.edges_updated} edges in ${edgeResult.duration_ms}ms`);
          } catch (err) {
            tracker.end({ error: String(err) });
            console.error('[Flywheel] Edge weight recompute failed:', err);
          }
        }

        // Step 6: Initialize wikilink entity index from StateDb
        tracker.start('entity_index_init', {});
        try {
          await initializeEntityIndex(vaultPath);
          tracker.end({});
          console.error('[Flywheel] Entity index initialized');
        } catch (err) {
          tracker.end({ error: String(err) });
          console.error('[Flywheel] Entity index init failed:', err);
        }

        // Step 7: Export hub scores
        let hubScoresExported = 0;
        tracker.start('hub_scores', {});
        try {
          hubScoresExported = await exportHubScores(newIndex, stateDb);
          tracker.end({ exported: hubScoresExported });
          if (hubScoresExported > 0) {
            console.error(`[Flywheel] Hub scores: ${hubScoresExported} entities`);
          }
        } catch (err) {
          tracker.end({ error: String(err) });
          console.error('[Flywheel] Hub score export failed:', err);
        }

        // Step 8: Record graph topology snapshot
        let graphSnapshotRecorded = false;
        if (stateDb) {
          tracker.start('graph_snapshot', {});
          try {
            const graphMetrics = computeGraphMetrics(newIndex);
            recordGraphSnapshot(stateDb, graphMetrics);
            graphSnapshotRecorded = true;
            tracker.end({ recorded: true });
          } catch (err) {
            tracker.end({ error: String(err) });
            console.error('[Flywheel] Graph snapshot failed:', err);
          }
        }

        // Step 9: Update wikilink suppression list
        let suppressionUpdated = false;
        if (stateDb) {
          tracker.start('suppression_list', {});
          try {
            updateSuppressionList(stateDb);
            suppressionUpdated = true;
            tracker.end({ updated: true });
          } catch (err) {
            tracker.end({ error: String(err) });
            console.error('[Flywheel] Suppression list update failed:', err);
          }
        }

        // Step 10: Rebuild recency index
        let recencyRebuilt = false;
        if (stateDb) {
          tracker.start('recency', {});
          try {
            const entities = getAllEntitiesFromDb(stateDb).map((e: { name: string; path: string; aliases: string[] }) => ({
              name: e.name, path: e.path, aliases: e.aliases,
            }));
            const recencyIndex = await buildRecencyIndex(vaultPath, entities);
            saveRecencyToStateDb(recencyIndex, stateDb);
            recencyRebuilt = true;
            tracker.end({ entities: recencyIndex.lastMentioned.size });
            console.error(`[Flywheel] Recency: rebuilt ${recencyIndex.lastMentioned.size} entities`);
          } catch (err) {
            tracker.end({ error: String(err) });
            console.error('[Flywheel] Recency rebuild failed:', err);
          }
        }

        // Step 11: Rebuild co-occurrence index
        let cooccurrenceAssociations: number | undefined;
        if (stateDb) {
          tracker.start('cooccurrence', {});
          try {
            const entityNames = getAllEntitiesFromDb(stateDb).map((e: { name: string }) => e.name);
            const cooccurrenceIdx = await mineCooccurrences(vaultPath, entityNames);
            setCooccurrenceIndex(cooccurrenceIdx);
            saveCooccurrenceToStateDb(stateDb, cooccurrenceIdx);
            cooccurrenceAssociations = cooccurrenceIdx._metadata.total_associations;
            tracker.end({ associations: cooccurrenceAssociations });
            console.error(`[Flywheel] Co-occurrence: rebuilt ${cooccurrenceAssociations} associations`);
          } catch (err) {
            tracker.end({ error: String(err) });
            console.error('[Flywheel] Co-occurrence rebuild failed:', err);
          }
        }

        // Step 12: Force-rebuild task cache
        let taskCacheRefreshed = false;
        tracker.start('task_cache', {});
        try {
          if (!flywheelConfig) {
            flywheelConfig = loadConfig(stateDb);
          }
          await buildTaskCache(vaultPath, newIndex, getExcludeTags(flywheelConfig));
          taskCacheRefreshed = true;
          tracker.end({ rebuilt: true });
          console.error('[Flywheel] Task cache rebuilt');
        } catch (err) {
          tracker.end({ error: String(err) });
          console.error('[Flywheel] Task cache rebuild failed:', err);
        }

        // Step 13: Sync note embeddings with current vault state (incremental — skips unchanged notes)
        let embeddingsRefreshed = 0;
        if (hasEmbeddingsIndex()) {
          tracker.start('embeddings_sync', {});
          try {
            const progress = await buildEmbeddingsIndex(vaultPath);
            embeddingsRefreshed = progress.total - progress.skipped;
            tracker.end({ refreshed: embeddingsRefreshed });
            if (embeddingsRefreshed > 0) {
              console.error(`[Flywheel] Embeddings: ${embeddingsRefreshed} notes updated`);
            }
          } catch (err) {
            tracker.end({ error: String(err) });
            console.error('[Flywheel] Embedding sync failed:', err);
          }
        }

        // Step 14: Sync entity embeddings
        let entityEmbeddingsRefreshed = 0;
        if (stateDb && hasEntityEmbeddingsIndex()) {
          tracker.start('entity_embeddings', {});
          try {
            const entities = getAllEntitiesFromDb(stateDb);
            if (entities.length > 0) {
              const entityMap = new Map(entities.map(e => [e.name, {
                name: e.name,
                path: e.path,
                category: e.category,
                aliases: e.aliases,
              }]));
              entityEmbeddingsRefreshed = await buildEntityEmbeddingsIndex(vaultPath, entityMap);
              loadEntityEmbeddingsToMemory();
              saveInferredCategories(classifyUncategorizedEntities(
                entities.map(entity => ({
                  entity: {
                    name: entity.name,
                    path: entity.path,
                    aliases: entity.aliases,
                  },
                  category: entity.category,
                }))
              ));
              tracker.end({ refreshed: entityEmbeddingsRefreshed });
              if (entityEmbeddingsRefreshed > 0) {
                console.error(`[Flywheel] Entity embeddings: ${entityEmbeddingsRefreshed} updated`);
              }
            } else {
              tracker.end({ refreshed: 0 });
            }
          } catch (err) {
            tracker.end({ error: String(err) });
            console.error('[Flywheel] Entity embedding sync failed:', err);
          }
        }

        // Step 15: Save vault index cache
        let indexCached = false;
        if (stateDb) {
          tracker.start('index_cache', {});
          try {
            saveVaultIndexToCache(stateDb, newIndex);
            indexCached = true;
            tracker.end({ cached: true });
          } catch (err) {
            tracker.end({ error: String(err) });
            console.error('[Flywheel] Index cache save failed:', err);
          }
        }

        const duration = Date.now() - startTime;

        // Record index event with per-step telemetry
        if (stateDb) {
          recordIndexEvent(stateDb, {
            trigger: 'manual_refresh',
            duration_ms: duration,
            note_count: newIndex.notes.size,
            steps: tracker.steps,
          });
        }

        const output: RefreshIndexOutput = {
          success: true,
          degraded: fts5RebuildError ? true : undefined,
          fts5_rebuild_error: fts5RebuildError,
          notes_count: newIndex.notes.size,
          entities_count: newIndex.entities.size,
          fts5_notes: fts5Notes,
          edges_recomputed: edgesRecomputed,
          note_links_synced: noteLinksSynced || undefined,
          hub_scores: hubScoresExported || undefined,
          graph_snapshot: graphSnapshotRecorded || undefined,
          suppression_list: suppressionUpdated || undefined,
          task_cache: taskCacheRefreshed || undefined,
          embeddings_refreshed: embeddingsRefreshed || undefined,
          entity_embeddings_refreshed: entityEmbeddingsRefreshed || undefined,
          recency_rebuilt: recencyRebuilt || undefined,
          cooccurrence_associations: cooccurrenceAssociations,
          index_cached: indexCached || undefined,
          duration_ms: duration,
        };

        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(output, null, 2),
            },
          ],
          structuredContent: output,
        };
      } catch (err) {
        setIndexState('error');
        setIndexError(err instanceof Error ? err : new Error(String(err)));
        const duration = Date.now() - startTime;

        // Record failed index event
        const stateDb = getStateDb?.();
        if (stateDb) {
          recordIndexEvent(stateDb, {
            trigger: 'manual_refresh',
            duration_ms: duration,
            success: false,
            error: err instanceof Error ? err.message : String(err),
          });
        }

        const output: RefreshIndexOutput = {
          success: false,
          notes_count: 0,
          entities_count: 0,
          fts5_notes: 0,
          duration_ms: duration,
        };

        return {
          content: [
            {
              type: 'text',
              text: `Error refreshing index: ${err instanceof Error ? err.message : String(err)}`,
            },
          ],
          structuredContent: output,
        };
      }
    }
  );

  // list_entities retired (T43 B3+) — use entity(action: list) instead

  // suggest_entity_aliases retired (T43 B3+) — use entity(action: suggest_aliases) instead

  // unlinked_mentions_report retired (T43) — functionality absorbed into link tool (action: unlinked)
}
