/**
 * Entity merge tool
 * Tool: merge_entities
 *
 * Merges a source entity note into a target entity note:
 * - Adds source title as alias on target
 * - Appends source content to target
 * - Replaces all wikilinks pointing to source → target
 * - Deletes source note
 */

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { validatePath, validatePathSecure, readVaultFile, writeVaultFile, WriteConflictError, type LineEnding } from '../../core/write/writer.js';
import type { MutationResult } from '../../core/write/types.js';
import { initializeEntityIndex } from '../../core/write/wikilinks.js';
import {
  findBacklinks,
  updateBacklinksInFile,
  extractAliases,
  getTitleFromPath,
  escapeRegex,
} from './move-notes.js';
import fs from 'fs/promises';
import path from 'path';

/**
 * Register the merge_entities tool with the MCP server
 */
export function registerMergeTools(
  server: McpServer,
  getVaultPath: () => string
): void {
  server.tool(
    'merge_entities',
    'Use when consolidating two entity notes into one. Produces alias addition, content append, wikilink rewiring, and source note deletion. Returns merge result with updated link count. Does not preserve the source note — it is deleted after merge.',
    {
      source_path: z.string().describe('Vault-relative path of the note to merge FROM (will be deleted)'),
      target_path: z.string().describe('Vault-relative path of the note to merge INTO (receives alias + content)'),
      dry_run: z.boolean().optional().default(false).describe('Preview merge plan without modifying any files'),
    },
    async ({ source_path, target_path, dry_run }) => {
      try {
        const vaultPath = getVaultPath();
        // 1. Validate paths (async: follows symlinks, blocks sensitive file patterns)
        const sourceValidation = await validatePathSecure(vaultPath, source_path);
        if (!sourceValidation.valid) {
          const result: MutationResult = {
            success: false,
            message: `Invalid source path: ${sourceValidation.reason}`,
            path: source_path,
          };
          return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
        }

        const targetValidation = await validatePathSecure(vaultPath, target_path);
        if (!targetValidation.valid) {
          const result: MutationResult = {
            success: false,
            message: `Invalid target path: ${targetValidation.reason}`,
            path: target_path,
          };
          return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
        }

        // 2. Read source note
        let sourceContent: string;
        let sourceFrontmatter: Record<string, unknown>;
        // source hash not needed — source note gets deleted, not written back
        try {
          const source = await readVaultFile(vaultPath, source_path);
          sourceContent = source.content;
          sourceFrontmatter = source.frontmatter;
        } catch {
          const result: MutationResult = {
            success: false,
            message: `Source file not found: ${source_path}`,
            path: source_path,
          };
          return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
        }

        // 3. Read target note
        let targetContent: string;
        let targetFrontmatter: Record<string, unknown>;
        let targetContentHash: string;
        try {
          const target = await readVaultFile(vaultPath, target_path);
          targetContent = target.content;
          targetFrontmatter = target.frontmatter;
          targetContentHash = target.contentHash;
        } catch {
          const result: MutationResult = {
            success: false,
            message: `Target file not found: ${target_path}`,
            path: target_path,
          };
          return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
        }

        const sourceTitle = getTitleFromPath(source_path);
        const targetTitle = getTitleFromPath(target_path);

        // 4. Add source title (and source aliases) to target's aliases
        const existingAliases = extractAliases(targetFrontmatter);
        const sourceAliases = extractAliases(sourceFrontmatter);
        const allNewAliases = [sourceTitle, ...sourceAliases];
        const deduped = new Set([...existingAliases]);
        for (const alias of allNewAliases) {
          // Don't add if it matches the target title
          if (alias.toLowerCase() !== targetTitle.toLowerCase()) {
            deduped.add(alias);
          }
        }
        targetFrontmatter.aliases = Array.from(deduped);

        // 5. Append source content if non-trivial
        const trimmedSource = sourceContent.trim();
        if (trimmedSource.length > 10) {
          const mergedSection = `\n\n## Merged from ${sourceTitle}\n\n${trimmedSource}`;
          targetContent = targetContent.trimEnd() + mergedSection;
        }

        // 6. Compute backlink update plan
        const allSourceTitles = [sourceTitle, ...sourceAliases];
        const backlinks = await findBacklinks(vaultPath, sourceTitle, sourceAliases);
        let totalBacklinksUpdated = 0;
        const modifiedFiles: string[] = [];

        if (dry_run) {
          // Just count what would change
          for (const backlink of backlinks) {
            if (backlink.path === source_path || backlink.path === target_path) continue;
            totalBacklinksUpdated += backlink.links.length;
            modifiedFiles.push(backlink.path);
          }
        } else {
          for (const backlink of backlinks) {
            if (backlink.path === source_path || backlink.path === target_path) continue;

            const updateResult = await updateBacklinksInFile(
              vaultPath,
              backlink.path,
              allSourceTitles,
              targetTitle
            );

            if (updateResult.updated) {
              totalBacklinksUpdated += updateResult.linksUpdated;
              modifiedFiles.push(backlink.path);
            }
          }
        }

        // Build preview
        const previewLines = [
          `${dry_run ? 'Would merge' : 'Merged'}: "${sourceTitle}" → "${targetTitle}"`,
          `Aliases ${dry_run ? 'to add' : 'added'}: ${allNewAliases.join(', ')}`,
          `Source content ${dry_run ? 'to append' : 'appended'}: ${trimmedSource.length > 10 ? 'yes' : 'no'}`,
          `Backlinks ${dry_run ? 'to update' : 'updated'}: ${totalBacklinksUpdated}`,
        ];
        if (modifiedFiles.length > 0) {
          previewLines.push(`Files ${dry_run ? 'to modify' : 'modified'}: ${modifiedFiles.join(', ')}`);
        }

        // Dry run: return preview without writing anything
        if (dry_run) {
          const result: MutationResult & { backlinks_updated?: number } = {
            success: true,
            message: `[dry run] Would merge "${sourceTitle}" into "${targetTitle}"`,
            path: target_path,
            preview: previewLines.join('\n'),
            backlinks_updated: totalBacklinksUpdated,
            dryRun: true,
          };
          return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
        }

        // 7. Write updated target
        await writeVaultFile(vaultPath, target_path, targetContent, targetFrontmatter, 'LF', targetContentHash);

        // 8. Delete source note
        const fullSourcePath = `${vaultPath}/${source_path}`;
        await fs.unlink(fullSourcePath);

        // 9. Rebuild entity index in background
        initializeEntityIndex(vaultPath).catch(err => {
          console.error(`[Flywheel] Entity cache rebuild failed: ${err}`);
        });

        const result: MutationResult & { backlinks_updated?: number } = {
          success: true,
          message: `Merged "${sourceTitle}" into "${targetTitle}"`,
          path: target_path,
          preview: previewLines.join('\n'),
          backlinks_updated: totalBacklinksUpdated,
        };

        return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
      } catch (error) {
        const result: MutationResult = {
          success: false,
          message: `Failed to merge entities: ${error instanceof Error ? error.message : String(error)}`,
          path: source_path,
          ...(error instanceof WriteConflictError ? {
            warnings: [{
              type: 'write_conflict',
              message: error.message,
              suggestion: 'The file was modified while processing. Re-read and retry.',
            }],
          } : {}),
        };
        return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
      }
    }
  );

  // ========================================
  // Tool: absorb_as_alias
  // ========================================
  server.tool(
    'absorb_as_alias',
    'Use when two entity names should resolve to the same note without merging content. Produces an alias addition on the target note and rewrites all source links. Returns rewritten link count and updated frontmatter. Does not merge note bodies — use merge_entities for full content merge.',
    {
      source_name: z.string().describe('The entity name to absorb (e.g. "Foo")'),
      target_path: z.string().describe('Vault-relative path of the target entity note (e.g. "entities/Bar.md")'),
      dry_run: z.boolean().optional().default(false).describe('Preview what would change without modifying any files'),
    },
    async ({ source_name, target_path, dry_run }) => {
      try {
        const vaultPath = getVaultPath();
        // 1. Validate target path (async: follows symlinks, blocks sensitive file patterns)
        const absorbTargetValidation = await validatePathSecure(vaultPath, target_path);
        if (!absorbTargetValidation.valid) {
          const result: MutationResult = {
            success: false,
            message: `Invalid target path: ${absorbTargetValidation.reason}`,
            path: target_path,
          };
          return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
        }

        // 2. Read target note
        let targetContent: string;
        let targetFrontmatter: Record<string, unknown>;
        let absorbTargetHash: string;
        try {
          const target = await readVaultFile(vaultPath, target_path);
          targetContent = target.content;
          targetFrontmatter = target.frontmatter;
          absorbTargetHash = target.contentHash;
        } catch {
          const result: MutationResult = {
            success: false,
            message: `Target file not found: ${target_path}`,
            path: target_path,
          };
          return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
        }

        const targetTitle = getTitleFromPath(target_path);

        // 3. Add source_name to target's aliases (dedup, skip if matches target title)
        const existingAliases = extractAliases(targetFrontmatter);
        const deduped = new Set(existingAliases);
        if (source_name.toLowerCase() !== targetTitle.toLowerCase()) {
          deduped.add(source_name);
        }
        targetFrontmatter.aliases = Array.from(deduped);

        // 4. Find all backlinks to source_name
        const backlinks = await findBacklinks(vaultPath, source_name, []);
        let totalBacklinksUpdated = 0;
        const modifiedFiles: string[] = [];

        // Check if source has a backing note (used by both dry_run and real path)
        const sourceNoteFile = await findSourceNote(vaultPath, source_name, target_path);

        if (dry_run) {
          // Just count what would change
          for (const backlink of backlinks) {
            if (backlink.path === target_path) continue;
            totalBacklinksUpdated += backlink.links.length;
            modifiedFiles.push(backlink.path);
          }
        } else {
          // 4b. Write updated target frontmatter
          await writeVaultFile(vaultPath, target_path, targetContent, targetFrontmatter, 'LF', absorbTargetHash);

          // 5. For each file, replace [[source_name]] → [[target|source_name]]
          for (const backlink of backlinks) {
            if (backlink.path === target_path) continue;

            let fileData: { content: string; frontmatter: Record<string, unknown>; lineEnding: string; contentHash: string };
            try {
              fileData = await readVaultFile(vaultPath, backlink.path);
            } catch {
              continue; // skip unreadable files
            }

            let content = fileData.content;
            let linksUpdated = 0;

            const pattern = new RegExp(
              `\\[\\[${escapeRegex(source_name)}(\\|[^\\]]+)?\\]\\]`,
              'gi'
            );

            content = content.replace(pattern, (_match, displayPart) => {
              linksUpdated++;
              if (displayPart) {
                return `[[${targetTitle}${displayPart}]]`;
              }
              if (source_name.toLowerCase() === targetTitle.toLowerCase()) {
                return `[[${targetTitle}]]`;
              }
              return `[[${targetTitle}|${source_name}]]`;
            });

            if (linksUpdated > 0) {
              await writeVaultFile(vaultPath, backlink.path, content, fileData.frontmatter, fileData.lineEnding as LineEnding, fileData.contentHash);
              totalBacklinksUpdated += linksUpdated;
              modifiedFiles.push(backlink.path);
            }
          }

          // 6. Delete source note if it exists (and isn't the target)
          let sourceDeleted = false;
          if (sourceNoteFile) {
            await fs.unlink(`${vaultPath}/${sourceNoteFile}`);
            sourceDeleted = true;
          }

          // 7. Rebuild entity index in background
          initializeEntityIndex(vaultPath).catch(err => {
            console.error(`[Flywheel] Entity cache rebuild failed: ${err}`);
          });
        }

        // 8. Build result
        const aliasAdded = source_name.toLowerCase() !== targetTitle.toLowerCase();
        const previewLines = [
          `${dry_run ? 'Would absorb' : 'Absorbed'}: "${source_name}" → "${targetTitle}"`,
          `Alias ${dry_run ? 'to add' : 'added'}: ${aliasAdded ? source_name : 'no (matches target title)'}`,
          `Backlinks ${dry_run ? 'to update' : 'updated'}: ${totalBacklinksUpdated}`,
          sourceNoteFile ? `Source note ${dry_run ? 'to delete' : 'deleted'}: ${sourceNoteFile}` : 'Source note: none found',
        ];
        if (modifiedFiles.length > 0) {
          previewLines.push(`Files ${dry_run ? 'to modify' : 'modified'}: ${modifiedFiles.join(', ')}`);
        }

        const result: MutationResult & { backlinks_updated?: number } = {
          success: true,
          message: dry_run
            ? `[dry run] Would absorb "${source_name}" as alias of "${targetTitle}"`
            : `Absorbed "${source_name}" as alias of "${targetTitle}"`,
          path: target_path,
          preview: previewLines.join('\n'),
          backlinks_updated: totalBacklinksUpdated,
          ...(dry_run ? { dryRun: true } : {}),
        };

        return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
      } catch (error) {
        const result: MutationResult = {
          success: false,
          message: `Failed to absorb as alias: ${error instanceof Error ? error.message : String(error)}`,
          path: target_path,
          ...(error instanceof WriteConflictError ? {
            warnings: [{
              type: 'write_conflict',
              message: error.message,
              suggestion: 'The file was modified while processing. Re-read and retry.',
            }],
          } : {}),
        };
        return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
      }
    }
  );
}

/**
 * Find a vault note file matching the given entity name (case-insensitive basename match).
 * Returns the vault-relative path if found, or null.
 */
async function findSourceNote(vaultPath: string, sourceName: string, excludePath: string): Promise<string | null> {
  const targetLower = sourceName.toLowerCase();

  async function scanDir(dir: string): Promise<string | null> {
    let entries;
    try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return null; }
    for (const entry of entries) {
      if (entry.name.startsWith('.')) continue;
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        const found = await scanDir(fullPath);
        if (found) return found;
      } else if (entry.isFile() && entry.name.endsWith('.md')) {
        const basename = path.basename(entry.name, '.md');
        if (basename.toLowerCase() === targetLower) {
          const relative = path.relative(vaultPath, fullPath).replace(/\\/g, '/');
          if (relative !== excludePath) return relative;
        }
      }
    }
    return null;
  }

  return scanDir(vaultPath);
}
