/**
 * Memento MCP Server - Optimized Tool Definitions & Handlers
 *
 * Performance optimizations:
 * - O(1) API key lookup with caching
 * - Batch operations for imports
 * - Parallel promise execution
 * - HTTP timeout wrapper
 * - N+1 query fixes
 * - Connection pooling
 */

import { generateApiKey, listApiKeys, revokeApiKey, resolveUser, validateApiKey, clearAuthCaches } from './auth.js';

import {
  CallToolRequestSchema,
  ErrorCode,
  ListToolsRequestSchema,
  McpError,
} from '@modelcontextprotocol/sdk/types.js';

// ─── Configuration ─────────────────────────────────────────────────────────

const DEFAULT_TIMEOUT = 10000; // 10 seconds for HTTP requests
const DEFAULT_SEARCH_LIMIT = 50;
const DEFAULT_NOTES_LIMIT = 100;

// ─── Helpers ────────────────────────────────────────────────────────────────

/**
 * Fetch with timeout wrapper
 * Prevents hanging on slow/unresponsive endpoints
 */
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
    });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error(`Request timeout after ${timeoutMs}ms`);
    }
    throw error;
  }
}

export function parseNote(dbNote) {
  if (!dbNote) return null;
  return {
    ...dbNote,
    checkItems: dbNote.checkItems ?? null,
    labels: dbNote.labels ?? null,
    images: dbNote.images ?? null,
    links: dbNote.links ?? null,
  };
}

export function parseNoteLightweight(dbNote) {
  if (!dbNote) return null;
  const images = Array.isArray(dbNote.images) ? dbNote.images : [];
  const labels = Array.isArray(dbNote.labels) ? dbNote.labels : null;
  const checkItems = Array.isArray(dbNote.checkItems) ? dbNote.checkItems : [];
  return {
    id: dbNote.id,
    title: dbNote.title,
    content: dbNote.content?.length > 200 ? dbNote.content.substring(0, 200) + '...' : dbNote.content,
    color: dbNote.color,
    type: dbNote.type,
    isPinned: dbNote.isPinned,
    isArchived: dbNote.isArchived,
    hasImages: images.length > 0,
    imageCount: images.length,
    labels,
    hasCheckItems: checkItems.length > 0,
    checkItemsCount: checkItems.length,
    reminder: dbNote.reminder,
    isReminderDone: dbNote.isReminderDone,
    isMarkdown: dbNote.isMarkdown,
    size: dbNote.size,
    createdAt: dbNote.createdAt,
    updatedAt: dbNote.updatedAt,
    notebookId: dbNote.notebookId,
  };
}

function textResult(data) {
  return {
    content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
  };
}

// ─── Tool Schemas ───────────────────────────────────────────────────────────

const NOTE_COLORS = ['default', 'red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'pink', 'gray'];
const LABEL_COLORS = ['red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'pink', 'gray'];

const toolDefinitions = [
  // ═══════════════════════════════════════════════════════════
  // NOTE TOOLS
  // ═══════════════════════════════════════════════════════════
  {
    name: 'create_note',
    description: 'Create a new note. Supports text and checklist types, colors, labels, images, links, reminders, markdown, and notebook assignment.',
    inputSchema: {
      type: 'object',
      properties: {
        title: { type: 'string', description: 'Note title (optional)' },
        content: { type: 'string', description: 'Note content (required)' },
        color: { type: 'string', description: `Note color: ${NOTE_COLORS.join(', ')}`, default: 'default' },
        type: { type: 'string', enum: ['text', 'checklist'], description: 'Note type', default: 'text' },
        checkItems: {
          type: 'array',
          description: 'Checklist items (when type is checklist)',
          items: {
            type: 'object',
            properties: { id: { type: 'string' }, text: { type: 'string' }, checked: { type: 'boolean' } },
            required: ['id', 'text', 'checked'],
          },
        },
        labels: { type: 'array', description: 'Note labels/tags', items: { type: 'string' } },
        isPinned: { type: 'boolean', description: 'Pin the note', default: false },
        isArchived: { type: 'boolean', description: 'Create as archived', default: false },
        images: { type: 'array', description: 'Image URLs or base64 strings', items: { type: 'string' } },
        links: { type: 'array', description: 'URLs attached to the note', items: { type: 'string' } },
        reminder: { type: 'string', description: 'Reminder datetime (ISO 8601)' },
        isReminderDone: { type: 'boolean', default: false },
        reminderRecurrence: { type: 'string', description: 'Recurrence: daily, weekly, monthly, yearly' },
        reminderLocation: { type: 'string', description: 'Location-based reminder' },
        isMarkdown: { type: 'boolean', description: 'Enable markdown rendering', default: false },
        size: { type: 'string', enum: ['small', 'medium', 'large'], default: 'small' },
        notebookId: { type: 'string', description: 'Notebook to assign the note to' },
      },
      required: ['content'],
    },
  },
  {
    name: 'get_notes',
    description: 'Get notes with optional filters. Returns lightweight format by default (truncated content, no images). Use fullDetails=true for complete data.',
    inputSchema: {
      type: 'object',
      properties: {
        includeArchived: { type: 'boolean', description: 'Include archived notes', default: false },
        search: { type: 'string', description: 'Filter by keyword in title/content' },
        notebookId: { type: 'string', description: 'Filter by notebook ID. Use "inbox" for notes without a notebook' },
        fullDetails: { type: 'boolean', description: 'Return full details including images (large payload)', default: false },
        limit: { type: 'number', description: `Max notes to return (default ${DEFAULT_NOTES_LIMIT})`, default: DEFAULT_NOTES_LIMIT },
      },
    },
  },
  {
    name: 'get_note',
    description: 'Get a single note by ID with full details.',
    inputSchema: {
      type: 'object',
      properties: { id: { type: 'string', description: 'Note ID' } },
      required: ['id'],
    },
  },
  {
    name: 'update_note',
    description: 'Update an existing note. Only include fields you want to change.',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Note ID' },
        title: { type: 'string' },
        content: { type: 'string' },
        color: { type: 'string', description: `One of: ${NOTE_COLORS.join(', ')}` },
        type: { type: 'string', enum: ['text', 'checklist'] },
        checkItems: {
          type: 'array',
          items: {
            type: 'object',
            properties: { id: { type: 'string' }, text: { type: 'string' }, checked: { type: 'boolean' } },
          },
        },
        labels: { type: 'array', items: { type: 'string' } },
        isPinned: { type: 'boolean' },
        isArchived: { type: 'boolean' },
        images: { type: 'array', items: { type: 'string' } },
        links: { type: 'array', items: { type: 'string' } },
        reminder: { type: 'string', description: 'ISO 8601 datetime' },
        isReminderDone: { type: 'boolean' },
        reminderRecurrence: { type: 'string' },
        reminderLocation: { type: 'string' },
        isMarkdown: { type: 'boolean' },
        size: { type: 'string', enum: ['small', 'medium', 'large'] },
        notebookId: { type: 'string' },
      },
      required: ['id'],
    },
  },
  {
    name: 'delete_note',
    description: 'Delete a note by ID.',
    inputSchema: {
      type: 'object',
      properties: { id: { type: 'string', description: 'Note ID' } },
      required: ['id'],
    },
  },
  {
    name: 'delete_all_notes',
    description: 'Delete ALL notes, labels, and notebooks for the configured user. Use with caution.',
    inputSchema: { type: 'object', properties: {} },
  },
  {
    name: 'search_notes',
    description: 'Search notes by keyword in title, content, and labels. Returns lightweight format.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Search query' },
        notebookId: { type: 'string', description: 'Limit search to a notebook' },
        includeArchived: { type: 'boolean', default: false },
      },
      required: ['query'],
    },
  },
  {
    name: 'move_note',
    description: 'Move a note to a different notebook. Pass null for notebookId to move to Inbox.',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Note ID' },
        notebookId: { type: 'string', description: 'Target notebook ID, or null/empty for Inbox' },
      },
      required: ['id'],
    },
  },
  {
    name: 'toggle_pin',
    description: 'Toggle the pin status of a note.',
    inputSchema: {
      type: 'object',
      properties: { id: { type: 'string', description: 'Note ID' } },
      required: ['id'],
    },
  },
  {
    name: 'toggle_archive',
    description: 'Toggle the archive status of a note.',
    inputSchema: {
      type: 'object',
      properties: { id: { type: 'string', description: 'Note ID' } },
      required: ['id'],
    },
  },
  {
    name: 'export_notes',
    description: 'Export all notes, labels, and notebooks as a JSON object.',
    inputSchema: { type: 'object', properties: {} },
  },
  {
    name: 'import_notes',
    description: 'Import notes from a previously exported JSON object. Skips duplicates by name.',
    inputSchema: {
      type: 'object',
      properties: {
        data: {
          type: 'object',
          description: 'The exported JSON data (from export_notes)',
          properties: {
            version: { type: 'string' },
            data: {
              type: 'object',
              properties: {
                notes: { type: 'array' },
                labels: { type: 'array' },
                notebooks: { type: 'array' },
              },
            },
          },
        },
      },
      required: ['data'],
    },
  },

  // ═══════════════════════════════════════════════════════════
  // NOTEBOOK TOOLS
  // ═══════════════════════════════════════════════════════════
  {
    name: 'create_notebook',
    description: 'Create a new notebook.',
    inputSchema: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Notebook name' },
        icon: { type: 'string', description: 'Notebook icon (emoji)', default: '📁' },
        color: { type: 'string', description: 'Hex color code', default: '#3B82F6' },
        order: { type: 'number', description: 'Sort position (auto-assigned if omitted)' },
      },
      required: ['name'],
    },
  },
  {
    name: 'get_notebooks',
    description: 'Get all notebooks with label and note counts.',
    inputSchema: { type: 'object', properties: {} },
  },
  {
    name: 'get_notebook',
    description: 'Get a notebook by ID, including its notes.',
    inputSchema: {
      type: 'object',
      properties: { id: { type: 'string', description: 'Notebook ID' } },
      required: ['id'],
    },
  },
  {
    name: 'update_notebook',
    description: 'Update a notebook\'s name, icon, color, or order.',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Notebook ID' },
        name: { type: 'string' },
        icon: { type: 'string' },
        color: { type: 'string' },
        order: { type: 'number' },
      },
      required: ['id'],
    },
  },
  {
    name: 'delete_notebook',
    description: 'Delete a notebook. Notes inside will be moved to Inbox.',
    inputSchema: {
      type: 'object',
      properties: { id: { type: 'string', description: 'Notebook ID' } },
      required: ['id'],
    },
  },
  {
    name: 'reorder_notebooks',
    description: 'Reorder notebooks. Pass an ordered array of notebook IDs.',
    inputSchema: {
      type: 'object',
      properties: {
        notebookIds: {
          type: 'array',
          description: 'Notebook IDs in the desired order',
          items: { type: 'string' },
        },
      },
      required: ['notebookIds'],
    },
  },

  // ═══════════════════════════════════════════════════════════
  // LABEL TOOLS
  // ═══════════════════════════════════════════════════════════
  {
    name: 'create_label',
    description: 'Create a label in a notebook.',
    inputSchema: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Label name' },
        color: { type: 'string', description: `Label color: ${LABEL_COLORS.join(', ')}` },
        notebookId: { type: 'string', description: 'Notebook to attach the label to' },
      },
      required: ['name', 'notebookId'],
    },
  },
  {
    name: 'get_labels',
    description: 'Get all labels, optionally filtered by notebook.',
    inputSchema: {
      type: 'object',
      properties: {
        notebookId: { type: 'string', description: 'Filter labels by notebook ID' },
      },
    },
  },
  {
    name: 'update_label',
    description: 'Update a label\'s name or color.',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Label ID' },
        name: { type: 'string' },
        color: { type: 'string' },
      },
      required: ['id'],
    },
  },
  {
    name: 'delete_label',
    description: 'Delete a label by ID.',
    inputSchema: {
      type: 'object',
      properties: { id: { type: 'string', description: 'Label ID' } },
      required: ['id'],
    },
  },

  // ═══════════════════════════════════════════════════════════
  // AI TOOLS
  // ═══════════════════════════════════════════════════════════
  {
    name: 'generate_title_suggestions',
    description: 'Use AI to generate 3 title suggestions for a note based on its content.',
    inputSchema: {
      type: 'object',
      properties: {
        content: { type: 'string', description: 'Note content (at least 10 words recommended)' },
      },
      required: ['content'],
    },
  },
  {
    name: 'reformulate_text',
    description: 'Use AI to reformulate text: clarify meaning, shorten, or improve writing style.',
    inputSchema: {
      type: 'object',
      properties: {
        text: { type: 'string', description: 'Text to reformulate' },
        option: { type: 'string', enum: ['clarify', 'shorten', 'improve'], description: 'Reformulation mode' },
      },
      required: ['text', 'option'],
    },
  },
  {
    name: 'generate_tags',
    description: 'Use AI to generate tags/labels for content. Optionally contextual to a notebook\'s existing labels.',
    inputSchema: {
      type: 'object',
      properties: {
        content: { type: 'string', description: 'Content to analyze' },
        notebookId: { type: 'string', description: 'Notebook ID for contextual tagging (uses existing labels)' },
        language: { type: 'string', description: 'Language hint (e.g. "en", "fr")', default: 'en' },
      },
      required: ['content'],
    },
  },
  {
    name: 'suggest_notebook',
    description: 'Use AI to suggest which notebook a note should belong to based on its content.',
    inputSchema: {
      type: 'object',
      properties: {
        noteContent: { type: 'string', description: 'Note content (at least 20 words recommended)' },
        language: { type: 'string', default: 'en' },
      },
      required: ['noteContent'],
    },
  },
  {
    name: 'get_notebook_summary',
    description: 'Generate an AI summary of all notes in a notebook.',
    inputSchema: {
      type: 'object',
      properties: {
        notebookId: { type: 'string', description: 'Notebook ID' },
        language: { type: 'string', default: 'en' },
      },
      required: ['notebookId'],
    },
  },
  {
    name: 'get_memory_echo',
    description: 'Get the next Memory Echo insight — AI-discovered connections between your notes based on semantic similarity.',
    inputSchema: { type: 'object', properties: {} },
  },
  {
    name: 'get_note_connections',
    description: 'Get all semantically related notes (Memory Echo connections) for a specific note. Supports pagination.',
    inputSchema: {
      type: 'object',
      properties: {
        noteId: { type: 'string', description: 'Note ID' },
        page: { type: 'number', description: 'Page number (default 1)', default: 1 },
        limit: { type: 'number', description: 'Results per page (default 10, max 50)', default: 10 },
      },
      required: ['noteId'],
    },
  },
  {
    name: 'dismiss_connection',
    description: 'Dismiss a Memory Echo connection between two notes.',
    inputSchema: {
      type: 'object',
      properties: {
        noteId: { type: 'string', description: 'First note ID' },
        connectedNoteId: { type: 'string', description: 'Connected note ID to dismiss' },
      },
      required: ['noteId', 'connectedNoteId'],
    },
  },
  {
    name: 'fuse_notes',
    description: 'Use AI to merge/fuse multiple notes into a single unified note.',
    inputSchema: {
      type: 'object',
      properties: {
        noteIds: {
          type: 'array',
          description: 'Array of note IDs to fuse (minimum 2)',
          items: { type: 'string' },
        },
        prompt: { type: 'string', description: 'Optional instructions for the fusion' },
      },
      required: ['noteIds'],
    },
  },
  {
    name: 'batch_organize',
    description: 'Create an AI-powered organization plan to move inbox notes into appropriate notebooks, or apply a previously created plan.',
    inputSchema: {
      type: 'object',
      properties: {
        action: { type: 'string', enum: ['create_plan', 'apply_plan'], description: 'Create a plan or apply it' },
        language: { type: 'string', default: 'en' },
        plan: { type: 'object', description: 'The plan object (required for apply_plan)' },
        selectedNoteIds: { type: 'array', items: { type: 'string' }, description: 'Note IDs to move (required for apply_plan)' },
      },
      required: ['action'],
    },
  },
  {
    name: 'suggest_auto_labels',
    description: 'Use AI to suggest new labels for a notebook based on its notes\' content (requires 15+ notes), or create selected labels.',
    inputSchema: {
      type: 'object',
      properties: {
        action: { type: 'string', enum: ['suggest', 'create'], description: 'Suggest labels or create selected ones' },
        notebookId: { type: 'string', description: 'Notebook ID' },
        language: { type: 'string', default: 'en' },
        selectedLabels: { type: 'array', items: { type: 'string' }, description: 'Label names to create (for create action)' },
        suggestions: { type: 'object', description: 'Suggestions object from suggest step (for create action)' },
      },
      required: ['action', 'notebookId'],
    },
  },

  // ═══════════════════════════════════════════════════════════
  // REMINDER TOOLS
  // ═══════════════════════════════════════════════════════════
  {
    name: 'get_due_reminders',
    description: 'Get all notes with due reminders that haven\'t been processed yet. Designed for cron/automation use.',
    inputSchema: { type: 'object', properties: {} },
  },

  // ═══════════════════════════════════════════════════════════
  // API KEY MANAGEMENT
  // ═══════════════════════════════════════════════════════════
  {
    name: 'generate_api_key',
    description: 'Generate a new API key for a user. The raw key is only shown once — store it securely.',
    inputSchema: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Human-readable name for this key (e.g. "N8N Automation")' },
        userId: { type: 'string', description: 'User ID to link this key to. If omitted, uses the default user.' },
        userEmail: { type: 'string', description: 'Alternatively, identify the user by email.' },
      },
    },
  },
  {
    name: 'list_api_keys',
    description: 'List all API keys (without revealing the actual keys). Can filter by user.',
    inputSchema: {
      type: 'object',
      properties: {
        userId: { type: 'string', description: 'Filter keys by user ID' },
      },
    },
  },
  {
    name: 'revoke_api_key',
    description: 'Revoke an API key by its short ID. The key will no longer be usable.',
    inputSchema: {
      type: 'object',
      properties: {
        shortId: { type: 'string', description: 'The short ID of the key (e.g. "a1b2c3d4")' },
      },
      required: ['shortId'],
    },
  },
];

// ─── Tool Handlers ──────────────────────────────────────────────────────────

/**
 * Register all tools and handlers on an MCP Server instance.
 *
 * @param {import('@modelcontextprotocol/sdk/server/index.js').Server} server
 * @param {import('@prisma/client').PrismaClient} prisma
 * @param {object} [options]
 * @param {string} [options.userId] - Optional user ID for multi-user filtering
 * @param {string} [options.appBaseUrl] - Optional base URL of the Next.js app for AI API calls
 */
export function registerTools(server, prisma, options = {}) {
  const { userId = null, appBaseUrl = null } = options;

  // Resolve userId: if not provided, auto-detect the first user
  let resolvedUserId = userId;
  let userIdPromise = null;
  
  const ensureUserId = async () => {
    if (resolvedUserId) return resolvedUserId;
    if (userIdPromise) return userIdPromise;
    
    userIdPromise = prisma.user.findFirst({ select: { id: true } }).then(u => {
      if (u) resolvedUserId = u.id;
      return resolvedUserId;
    });
    
    return userIdPromise;
  };

  // ── List Tools ────────────────────────────────────────────────────────────
  server.setRequestHandler(ListToolsRequestSchema, async () => {
    return { tools: toolDefinitions };
  });

  // ── Call Tools ────────────────────────────────────────────────────────────
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    try {
      switch (name) {

        // ═══════════════════════════════════════════════════════
        //  NOTES
        // ═══════════════════════════════════════════════════════
        case 'create_note': {
          const data = {
            title: args.title || null,
            content: args.content,
            color: args.color || 'default',
            type: args.type || 'text',
            checkItems: args.checkItems ?? null,
            labels: args.labels ?? null,
            isPinned: args.isPinned || false,
            isArchived: args.isArchived || false,
            images: args.images ?? null,
            links: args.links ?? null,
            reminder: args.reminder ? new Date(args.reminder) : null,
            isReminderDone: args.isReminderDone || false,
            reminderRecurrence: args.reminderRecurrence || null,
            reminderLocation: args.reminderLocation || null,
            isMarkdown: args.isMarkdown || false,
            size: args.size || 'small',
            notebookId: args.notebookId || null,
          };
          if (resolvedUserId) data.userId = resolvedUserId;
          else data.userId = await ensureUserId();

          const note = await prisma.note.create({ data });
          return textResult(parseNote(note));
        }

        case 'get_notes': {
          const where = {};
          if (resolvedUserId) where.userId = resolvedUserId;
          if (!args?.includeArchived) where.isArchived = false;
          if (args?.search) {
            where.OR = [
              { title: { contains: args.search } },
              { content: { contains: args.search } },
            ];
          }
          if (args?.notebookId) {
            where.notebookId = args.notebookId === 'inbox' ? null : args.notebookId;
          }

          const limit = Math.min(args?.limit || DEFAULT_NOTES_LIMIT, 500); // Max 500
          const notes = await prisma.note.findMany({
            where,
            orderBy: [{ isPinned: 'desc' }, { order: 'asc' }, { updatedAt: 'desc' }],
            take: limit,
          });

          const parsed = args?.fullDetails ? notes.map(parseNote) : notes.map(parseNoteLightweight);
          return textResult(parsed);
        }

        case 'get_note': {
          const where = { id: args.id };
          if (resolvedUserId) where.userId = resolvedUserId;
          const note = await prisma.note.findUnique({ where });
          if (!note) throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
          return textResult(parseNote(note));
        }

        case 'update_note': {
          const updateData = {};
          const fields = ['title', 'color', 'type', 'isPinned', 'isArchived', 'isMarkdown', 'size', 'notebookId', 'isReminderDone', 'reminderRecurrence', 'reminderLocation'];
          for (const f of fields) {
            if (f in args) updateData[f] = args[f];
          }
          if ('content' in args) updateData.content = args.content;
          if ('checkItems' in args) updateData.checkItems = args.checkItems ?? null;
          if ('labels' in args) updateData.labels = args.labels ?? null;
          if ('images' in args) updateData.images = args.images ?? null;
          if ('links' in args) updateData.links = args.links ?? null;
          if ('reminder' in args) updateData.reminder = args.reminder ? new Date(args.reminder) : null;
          updateData.updatedAt = new Date();

          const where = { id: args.id };
          if (resolvedUserId) where.userId = resolvedUserId;
          const note = await prisma.note.update({ where, data: updateData });
          return textResult(parseNote(note));
        }

        case 'delete_note': {
          const where = { id: args.id };
          if (resolvedUserId) where.userId = resolvedUserId;
          await prisma.note.delete({ where });
          return textResult({ success: true, message: 'Note deleted' });
        }

        case 'delete_all_notes': {
          const where = {};
          if (resolvedUserId) where.userId = resolvedUserId;

          const [deletedNotes] = await prisma.$transaction([
            prisma.note.deleteMany({ where }),
            resolvedUserId 
              ? prisma.label.deleteMany({ where: { notebook: { userId: resolvedUserId } } })
              : prisma.label.deleteMany({}),
            resolvedUserId
              ? prisma.notebook.deleteMany({ where: { userId: resolvedUserId } })
              : prisma.notebook.deleteMany({}),
          ]);
          
          return textResult({ success: true, deletedNotes: deletedNotes.count });
        }

        case 'search_notes': {
          const where = {
            isArchived: args.includeArchived || false,
            OR: [
              { title: { contains: args.query } },
              { content: { contains: args.query } },
            ],
          };
          if (resolvedUserId) where.userId = resolvedUserId;
          if (args.notebookId) where.notebookId = args.notebookId;

          const notes = await prisma.note.findMany({
            where,
            orderBy: [{ isPinned: 'desc' }, { updatedAt: 'desc' }],
            take: DEFAULT_SEARCH_LIMIT,
          });
          return textResult(notes.map(parseNoteLightweight));
        }

        case 'move_note': {
          const noteWhere = { id: args.id };
          if (resolvedUserId) noteWhere.userId = resolvedUserId;

          const targetNotebookId = args.notebookId || null;
          
          // Optimized: Parallel execution
          const [note, notebook] = await Promise.all([
            prisma.note.update({
              where: noteWhere,
              data: { notebookId: targetNotebookId, updatedAt: new Date() },
            }),
            targetNotebookId 
              ? prisma.notebook.findUnique({ where: { id: targetNotebookId }, select: { name: true } })
              : Promise.resolve(null),
          ]);

          const notebookName = notebook?.name || 'Inbox';

          return textResult({
            success: true,
            data: { id: note.id, notebookId: note.notebookId, notebook: { name: notebookName } },
            message: `Note moved to ${notebookName}`,
          });
        }

        case 'toggle_pin': {
          const where = { id: args.id };
          if (resolvedUserId) where.userId = resolvedUserId;
          const note = await prisma.note.findUnique({ where });
          if (!note) throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
          const updated = await prisma.note.update({
            where,
            data: { isPinned: !note.isPinned },
          });
          return textResult(parseNote(updated));
        }

        case 'toggle_archive': {
          const where = { id: args.id };
          if (resolvedUserId) where.userId = resolvedUserId;
          const note = await prisma.note.findUnique({ where });
          if (!note) throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
          const updated = await prisma.note.update({
            where,
            data: { isArchived: !note.isArchived },
          });
          return textResult(parseNote(updated));
        }

        case 'export_notes': {
          const noteWhere = {};
          const nbWhere = {};
          if (resolvedUserId) { noteWhere.userId = resolvedUserId; nbWhere.userId = resolvedUserId; }

          // Optimized: Parallel queries
          const [notes, notebooks, labels] = await Promise.all([
            prisma.note.findMany({ 
              where: noteWhere, 
              orderBy: { updatedAt: 'desc' },
              select: {
                id: true,
                title: true,
                content: true,
                color: true,
                type: true,
                isPinned: true,
                isArchived: true,
                isMarkdown: true,
                size: true,
                labels: true,
                notebookId: true,
                createdAt: true,
                updatedAt: true,
              },
            }),
            prisma.notebook.findMany({
              where: nbWhere,
              include: { _count: { select: { notes: true } } },
              orderBy: { order: 'asc' },
            }),
            prisma.label.findMany({
              include: { notebook: { select: { id: true, name: true, userId: true } } },
            }),
          ]);

          // Filter labels by userId in memory (faster than multiple queries)
          const filteredLabels = resolvedUserId
            ? labels.filter(l => l.notebook?.userId === resolvedUserId)
            : labels;

          return textResult({
            version: '2.0',
            exportDate: new Date().toISOString(),
            data: {
              notes: notes.map(n => ({
                id: n.id,
                title: n.title,
                content: n.content,
                color: n.color,
                type: n.type,
                isPinned: n.isPinned,
                isArchived: n.isArchived,
                isMarkdown: n.isMarkdown,
                size: n.size,
                labels: Array.isArray(n.labels) ? n.labels : [],
                notebookId: n.notebookId,
                createdAt: n.createdAt,
                updatedAt: n.updatedAt,
              })),
              notebooks: notebooks.map(nb => ({
                id: nb.id,
                name: nb.name,
                icon: nb.icon,
                color: nb.color,
                noteCount: nb._count.notes,
              })),
              labels: filteredLabels.map(l => ({
                id: l.id,
                name: l.name,
                color: l.color,
                notebookId: l.notebookId,
              })),
            },
          });
        }

        case 'import_notes': {
          const importData = args.data;
          let importedNotes = 0, importedLabels = 0, importedNotebooks = 0;

          // OPTIMIZED: Batch operations with Promise.all for notebooks
          if (importData.data?.notebooks?.length > 0) {
            const existingNotebooks = await prisma.notebook.findMany({
              where: resolvedUserId ? { userId: resolvedUserId } : {},
              select: { name: true },
            });
            const existingNames = new Set(existingNotebooks.map(nb => nb.name));

            const notebooksToCreate = importData.data.notebooks
              .filter(nb => !existingNames.has(nb.name))
              .map(nb => prisma.notebook.create({
                data: {
                  name: nb.name,
                  icon: nb.icon || '📁',
                  color: nb.color || '#3B82F6',
                  ...(resolvedUserId ? { userId: resolvedUserId } : {}),
                },
              }));

            await Promise.all(notebooksToCreate);
            importedNotebooks = notebooksToCreate.length;
          }

          // OPTIMIZED: Batch labels
          if (importData.data?.labels?.length > 0) {
            const notebooks = await prisma.notebook.findMany({
              where: resolvedUserId ? { userId: resolvedUserId } : {},
              select: { id: true },
            });
            const notebookIds = new Set(notebooks.map(nb => nb.id));

            const existingLabels = await prisma.label.findMany({
              where: { notebookId: { in: Array.from(notebookIds) } },
              select: { name: true, notebookId: true },
            });
            const existingLabelKeys = new Set(existingLabels.map(l => `${l.notebookId}:${l.name}`));

            const labelsToCreate = [];
            for (const label of importData.data.labels) {
              if (label.notebookId && notebookIds.has(label.notebookId)) {
                const key = `${label.notebookId}:${label.name}`;
                if (!existingLabelKeys.has(key)) {
                  labelsToCreate.push(prisma.label.create({
                    data: { name: label.name, color: label.color, notebookId: label.notebookId },
                  }));
                }
              }
            }

            await Promise.all(labelsToCreate);
            importedLabels = labelsToCreate.length;
          }

          // OPTIMIZED: Batch notes with createMany if available, else Promise.all
          if (importData.data?.notes?.length > 0) {
            const notesData = importData.data.notes.map(note => ({
              title: note.title,
              content: note.content,
              color: note.color || 'default',
              type: note.type || 'text',
              isPinned: note.isPinned || false,
              isArchived: note.isArchived || false,
              isMarkdown: note.isMarkdown || false,
              size: note.size || 'small',
              labels: note.labels ?? null,
              notebookId: note.notebookId || null,
              ...(resolvedUserId ? { userId: resolvedUserId } : {}),
            }));

            // Try createMany first (faster), fall back to Promise.all
            try {
              const result = await prisma.note.createMany({
                data: notesData,
                skipDuplicates: true,
              });
              importedNotes = result.count;
            } catch {
              // Fallback to individual creates
              const creates = notesData.map(data => 
                prisma.note.create({ data }).catch(() => null)
              );
              const results = await Promise.all(creates);
              importedNotes = results.filter(r => r !== null).length;
            }
          }

          return textResult({
            success: true,
            imported: { notes: importedNotes, labels: importedLabels, notebooks: importedNotebooks },
          });
        }

        // ═══════════════════════════════════════════════════════
        //  NOTEBOOKS
        // ═══════════════════════════════════════════════════════
        case 'create_notebook': {
          const highestOrder = await prisma.notebook.findFirst({
            where: resolvedUserId ? { userId: resolvedUserId } : {},
            orderBy: { order: 'desc' },
            select: { order: true },
          });
          const nextOrder = args.order !== undefined ? args.order : (highestOrder?.order ?? -1) + 1;

          const data = {
            name: args.name.trim(),
            icon: args.icon || '📁',
            color: args.color || '#3B82F6',
            order: nextOrder,
          };
          data.userId = resolvedUserId || await ensureUserId();

          const notebook = await prisma.notebook.create({
            data,
            include: { labels: true, _count: { select: { notes: true } } },
          });

          return textResult({ ...notebook, notesCount: notebook._count.notes });
        }

        case 'get_notebooks': {
          const where = {};
          if (resolvedUserId) where.userId = resolvedUserId;

          const notebooks = await prisma.notebook.findMany({
            where,
            include: {
              labels: { orderBy: { name: 'asc' } },
              _count: { select: { notes: true } },
            },
            orderBy: { order: 'asc' },
          });

          return textResult(notebooks.map(nb => ({ ...nb, notesCount: nb._count.notes })));
        }

        case 'get_notebook': {
          const where = { id: args.id };
          if (resolvedUserId) where.userId = resolvedUserId;

          const notebook = await prisma.notebook.findUnique({
            where,
            include: { labels: true, notes: true, _count: { select: { notes: true } } },
          });
          if (!notebook) throw new McpError(ErrorCode.InvalidRequest, 'Notebook not found');

          return textResult({
            ...notebook,
            notes: notebook.notes.map(parseNoteLightweight),
            notesCount: notebook._count.notes,
          });
        }

        case 'update_notebook': {
          const updateData = {};
          if ('name' in args) updateData.name = args.name.trim();
          if ('icon' in args) updateData.icon = args.icon;
          if ('color' in args) updateData.color = args.color;
          if ('order' in args) updateData.order = args.order;

          const where = { id: args.id };
          if (resolvedUserId) where.userId = resolvedUserId;

          const notebook = await prisma.notebook.update({
            where,
            data: updateData,
            include: { labels: true, _count: { select: { notes: true } } },
          });

          return textResult({ ...notebook, notesCount: notebook._count.notes });
        }

        case 'delete_notebook': {
          const where = { id: args.id };
          if (resolvedUserId) where.userId = resolvedUserId;

          // Move notes to inbox before deleting
          await prisma.$transaction([
            prisma.note.updateMany({
              where: { notebookId: args.id, ...(resolvedUserId ? { userId: resolvedUserId } : {}) },
              data: { notebookId: null },
            }),
            prisma.notebook.delete({ where }),
          ]);
          
          return textResult({ success: true, message: 'Notebook deleted, notes moved to Inbox' });
        }

        case 'reorder_notebooks': {
          const ids = args.notebookIds;
          
          // Optimized: Verify ownership in one query
          const where = { id: { in: ids } };
          if (resolvedUserId) where.userId = resolvedUserId;
          
          const existingNotebooks = await prisma.notebook.findMany({
            where,
            select: { id: true },
          });
          
          const existingIds = new Set(existingNotebooks.map(nb => nb.id));
          const missingIds = ids.filter(id => !existingIds.has(id));
          
          if (missingIds.length > 0) {
            throw new McpError(ErrorCode.InvalidRequest, `Notebooks not found: ${missingIds.join(', ')}`);
          }

          await prisma.$transaction(
            ids.map((id, index) =>
              prisma.notebook.update({ where: { id }, data: { order: index } })
            )
          );
          return textResult({ success: true, message: 'Notebooks reordered' });
        }

        // ═══════════════════════════════════════════════════════
        //  LABELS - OPTIMIZED to fix N+1 query
        // ═══════════════════════════════════════════════════════
        case 'create_label': {
          const existing = await prisma.label.findFirst({
            where: { name: args.name.trim(), notebookId: args.notebookId },
          });
          if (existing) throw new McpError(ErrorCode.InvalidRequest, 'Label already exists in this notebook');

          const label = await prisma.label.create({
            data: {
              name: args.name.trim(),
              color: args.color || LABEL_COLORS[Math.floor(Math.random() * LABEL_COLORS.length)],
              notebookId: args.notebookId,
            },
          });
          return textResult(label);
        }

        case 'get_labels': {
          const where = {};
          if (args?.notebookId) where.notebookId = args.notebookId;

          // OPTIMIZED: Single query with include, then filter in memory
          const labels = await prisma.label.findMany({
            where,
            include: { notebook: { select: { id: true, name: true, userId: true } } },
            orderBy: { name: 'asc' },
          });

          // Filter by userId in memory (much faster than N+1 queries)
          let filteredLabels = labels;
          if (resolvedUserId) {
            filteredLabels = labels.filter(l => l.notebook?.userId === resolvedUserId);
          }

          return textResult(filteredLabels);
        }

        case 'update_label': {
          const updateData = {};
          if ('name' in args) updateData.name = args.name.trim();
          if ('color' in args) updateData.color = args.color;

          const label = await prisma.label.update({
            where: { id: args.id },
            data: updateData,
          });
          return textResult(label);
        }

        case 'delete_label': {
          await prisma.label.delete({ where: { id: args.id } });
          return textResult({ success: true, message: 'Label deleted' });
        }

        // ═══════════════════════════════════════════════════════
        //  AI TOOLS - OPTIMIZED with timeout
        // ═══════════════════════════════════════════════════════
        case 'generate_title_suggestions': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/title-suggestions`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ content: args.content }),
          });
          const data = await resp.json();
          if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Title suggestion failed');
          return textResult(data);
        }

        case 'reformulate_text': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/reformulate`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ text: args.text, option: args.option }),
          });
          const data = await resp.json();
          if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Reformulation failed');
          return textResult(data);
        }

        case 'generate_tags': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/tags`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ content: args.content, notebookId: args.notebookId, language: args.language || 'en' }),
          });
          const data = await resp.json();
          if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Tag generation failed');
          return textResult(data);
        }

        case 'suggest_notebook': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/suggest-notebook`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ noteContent: args.noteContent, language: args.language || 'en' }),
          });
          const data = await resp.json();
          if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Notebook suggestion failed');
          return textResult(data);
        }

        case 'get_notebook_summary': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/notebook-summary`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ notebookId: args.notebookId, language: args.language || 'en' }),
          });
          const data = await resp.json();
          if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Summary generation failed');
          return textResult(data);
        }

        case 'get_memory_echo': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo`, {
            method: 'GET',
            headers: { 'Content-Type': 'application/json' },
          });
          const data = await resp.json();
          return textResult(data);
        }

        case 'get_note_connections': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          const params = new URLSearchParams({ noteId: args.noteId, page: String(args.page || 1), limit: String(Math.min(args.limit || 10, 50)) });
          const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo/connections?${params}`);
          const data = await resp.json();
          if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Connection fetch failed');
          return textResult(data);
        }

        case 'dismiss_connection': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo/dismiss`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ noteId: args.noteId, connectedNoteId: args.connectedNoteId }),
          });
          const data = await resp.json();
          if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Dismiss failed');
          return textResult(data);
        }

        case 'fuse_notes': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          if (!args.noteIds || args.noteIds.length < 2) {
            throw new McpError(ErrorCode.InvalidRequest, 'At least 2 note IDs required');
          }
          const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo/fusion`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ noteIds: args.noteIds, prompt: args.prompt }),
          });
          const data = await resp.json();
          if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Fusion failed');
          return textResult(data);
        }

        case 'batch_organize': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          if (args.action === 'create_plan') {
            const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/batch-organize`, {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ language: args.language || 'en' }),
            });
            const data = await resp.json();
            if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Plan creation failed');
            return textResult(data);
          } else if (args.action === 'apply_plan') {
            const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/batch-organize`, {
              method: 'PUT',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ plan: args.plan, selectedNoteIds: args.selectedNoteIds }),
            });
            const data = await resp.json();
            if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Plan application failed');
            return textResult(data);
          } else {
            throw new McpError(ErrorCode.InvalidRequest, 'Invalid action. Use "create_plan" or "apply_plan"');
          }
        }

        case 'suggest_auto_labels': {
          if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
          if (args.action === 'suggest') {
            const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/auto-labels`, {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ notebookId: args.notebookId, language: args.language || 'en' }),
            });
            const data = await resp.json();
            if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Label suggestion failed');
            return textResult(data);
          } else if (args.action === 'create') {
            const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/auto-labels`, {
              method: 'PUT',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ suggestions: args.suggestions, selectedLabels: args.selectedLabels }),
            });
            const data = await resp.json();
            if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Label creation failed');
            return textResult(data);
          } else {
            throw new McpError(ErrorCode.InvalidRequest, 'Invalid action. Use "suggest" or "create"');
          }
        }

        // ═══════════════════════════════════════════════════════
        //  REMINDERS
        // ═══════════════════════════════════════════════════════
        case 'get_due_reminders': {
          const where = {
            reminder: { not: null, lte: new Date() },
            isReminderDone: false,
            isArchived: false,
          };
          if (resolvedUserId) where.userId = resolvedUserId;

          const reminders = await prisma.note.findMany({
            where,
            select: { id: true, title: true, content: true, reminder: true },
            orderBy: { reminder: 'asc' },
          });

          // Mark them as done
          if (reminders.length > 0) {
            await prisma.note.updateMany({
              where: { id: { in: reminders.map(r => r.id) } },
              data: { isReminderDone: true },
            });
          }

          return textResult({ success: true, count: reminders.length, reminders });
        }

        // ═══════════════════════════════════════════════════════
        //  API KEY MANAGEMENT
        // ═══════════════════════════════════════════════════════
        case 'generate_api_key': {
          // Resolve target user
          let targetUserId = args?.userId || resolvedUserId;
          if (!targetUserId && args?.userEmail) {
            const user = await resolveUser(prisma, args.userEmail);
            if (user) targetUserId = user.id;
          }
          if (!targetUserId) targetUserId = await ensureUserId();

          const result = await generateApiKey(prisma, {
            name: args?.name,
            userId: targetUserId,
          });

          return textResult({
            warning: 'Store this raw key securely — it will NOT be shown again.',
            rawKey: result.rawKey,
            info: result.info,
          });
        }

        case 'list_api_keys': {
          const keys = await listApiKeys(prisma, { userId: args?.userId });
          return textResult(keys);
        }

        case 'revoke_api_key': {
          const revoked = await revokeApiKey(prisma, args.shortId);
          if (!revoked) throw new McpError(ErrorCode.InvalidRequest, 'Key not found or already revoked');
          return textResult({ success: true, message: `Key ${args.shortId} revoked` });
        }

        default:
          throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
      }
    } catch (error) {
      if (error instanceof McpError) throw error;
      throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message}`);
    }
  });
}

// Export clear caches function for testing
export { clearAuthCaches };
