/**
 * nexus-agents/mcp - Search Codebase MCP Tool
 *
 * Keyword search across an indexed codebase symbol table.
 * Returns matching functions, classes, methods, interfaces, and types
 * with relevance scoring and file locations.
 *
 * @module mcp/tools/search-codebase-tool
 */

import { resolve } from 'node:path';
import { z } from 'zod';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createLogger, formatZodError } from '../../core/index.js';
import { CodebaseIndex } from '../../indexer/codebase-search.js';
import { wrapToolWithTimeout, toSdkCallback, getToolTimeout } from '../middleware/tool-wrapper.js';
import { createSecureHandler, type HandlerContext } from '../middleware/secure-handler.js';
import {
  toolStructuredError,
  toolSuccess,
  type BaseMcpToolDeps,
  type ToolResult,
} from './tool-result.js';
import { getToolAnnotations } from '../tool-annotations.js';

// ============================================================================
// Input Schema
// ============================================================================

export const SearchCodebaseInputSchema = z.object({
  query: z.string().min(1).max(200).describe('Search query (symbol name, keyword, or pattern)'),
  directory: z
    .string()
    .min(1)
    .max(500)
    .optional()
    .describe('Directory to search (default: current working directory)'),
  limit: z.number().min(1).max(50).optional().describe('Max results (default: 20)'),
  mode: z
    .enum(['search', 'summary', 'list'])
    .optional()
    .describe('search: find symbols. summary: file overview. list: list indexed files.'),
});

export type SearchCodebaseInput = z.infer<typeof SearchCodebaseInputSchema>;
export type SearchCodebaseDeps = BaseMcpToolDeps;

// ============================================================================
// Index Cache (reuse across calls for same directory)
// ============================================================================

let cachedIndex: CodebaseIndex | undefined;
let cachedDir = '';

async function getIndex(dir: string): Promise<CodebaseIndex> {
  if (cachedIndex !== undefined && cachedDir === dir) {
    return cachedIndex;
  }
  const index = new CodebaseIndex(dir);
  await index.index(4);
  cachedIndex = index;
  cachedDir = dir;
  return index;
}

// ============================================================================
// Helpers
// ============================================================================

/** Resolve and validate directory against path traversal. */
function resolveSearchDir(directory: string | undefined): { dir: string } | { error: string } {
  const dir = resolve(directory ?? process.cwd());
  const cwdRoot = resolve('.');
  if (!dir.startsWith(cwdRoot)) {
    return { error: `Path traversal denied: directory must be within ${cwdRoot}` };
  }
  return { dir };
}

/** Format list mode output. */
function formatListOutput(index: CodebaseIndex): string {
  const files = index.listFiles();
  const output = files
    .sort((a, b) => b.symbols - a.symbols)
    .map(
      (f) =>
        `${String(f.symbols).padStart(4)} symbols  ${String(f.lines).padStart(5)} lines  ${f.path}`
    )
    .join('\n');
  return `${String(index.stats.files)} files, ${String(index.stats.symbols)} symbols indexed\n\n${output}`;
}

// ============================================================================
// Handler
// ============================================================================

async function searchCodebaseHandler(args: unknown, ctx: HandlerContext): Promise<ToolResult> {
  const parsed = SearchCodebaseInputSchema.safeParse(args);
  if (!parsed.success) {
    return toolStructuredError({
      errorCategory: 'validation',
      message: `Validation error: ${formatZodError(parsed.error)}`,
    });
  }

  const { query, directory, limit, mode } = parsed.data;
  const dirResult = resolveSearchDir(directory);
  if ('error' in dirResult) {
    // resolveSearchDir only fails on a path-traversal denial.
    return toolStructuredError({ errorCategory: 'permission', message: dirResult.error });
  }

  try {
    const index = await getIndex(dirResult.dir);

    if (mode === 'list') return toolSuccess(formatListOutput(index));

    if (mode === 'summary') {
      const summary = index.getFileSummary(query);
      if (summary === undefined) {
        return toolSuccess(
          `File "${query}" not found in index. Use mode=list to see indexed files.`
        );
      }
      return toolSuccess(JSON.stringify(summary, null, 2));
    }

    // Default: search mode
    const results = index.search(query, limit ?? 20);
    if (results.length === 0) {
      return toolSuccess(
        `No symbols matching "${query}" found in ${String(index.stats.files)} indexed files.`
      );
    }

    const output = results
      .map((r) => {
        const exp = r.symbol.exported ? 'export ' : '';
        return `[${r.matchType}] ${exp}${r.symbol.kind} ${r.symbol.name} (${r.symbol.filePath}:${String(r.symbol.startLine)})`;
      })
      .join('\n');

    return toolSuccess(`${String(results.length)} results for "${query}":\n\n${output}`);
  } catch (caught: unknown) {
    const e = caught instanceof Error ? caught : new Error(String(caught));
    ctx.logger.error('Codebase search failed', e);
    return toolStructuredError({
      errorCategory: 'internal',
      message: `Search failed: ${e.message}`,
    });
  }
}

// ============================================================================
// Testing exports (#2159)
// ============================================================================

/** Test-only surface — do not import in production code. */
export const _testing = {
  /** Raw handler for unit testing (bypasses secure-handler + timeout middleware). */
  searchCodebaseHandler,
  /** Exposes the shared dir-validation + resolution logic. */
  resolveSearchDir,
  /** Clears the module-level index cache so tests can start fresh. */
  clearIndexCache: (): void => {
    cachedIndex = undefined;
    cachedDir = '';
  },
};

// ============================================================================
// Registration
// ============================================================================

/** @category MCP */
export function registerSearchCodebaseTool(server: McpServer, deps: SearchCodebaseDeps): void {
  const logger = deps.logger ?? createLogger({ tool: 'search_codebase' });

  const toolSchema = {
    query: z.string().min(1).max(200).describe('Search query or file path'),
    directory: z.string().max(500).optional().describe('Directory to index'),
    limit: z.number().min(1).max(50).optional().describe('Max results'),
    mode: z.enum(['search', 'summary', 'list']).optional().describe('search/summary/list'),
  };

  const description =
    'Cross-file ripgrep-style search across the working directory. ' +
    'Builds an in-memory symbol index and ranks matches with relevance scoring. ' +
    'Use when you need usages of a symbol or pattern across MANY files. ' +
    'For the AST of a single file, use `extract_symbols` instead. ' +
    'Modes: search (find by keyword), summary (per-file overview), list (all indexed files).';

  const secureHandler = createSecureHandler(searchCodebaseHandler, {
    toolName: 'search_codebase',
    rateLimiter: deps.rateLimiter,
    logger,
  });

  const timeoutMs = getToolTimeout('search_codebase', deps.security);
  const wrapped = wrapToolWithTimeout('search_codebase', secureHandler, { timeoutMs, logger });

  server.registerTool(
    'search_codebase',
    { description, inputSchema: toolSchema, annotations: getToolAnnotations('search_codebase') },
    toSdkCallback(wrapped)
  );
  logger.info('Registered search_codebase tool');
}
