// ABOUTME: Schema-based fast text search tool using ripgrep for codebase analysis
// ABOUTME: Wraps ripgrep command with Zod validation and structured output

import { z } from 'zod';
import { Tool } from '../tool';
import { NonEmptyString, FilePath } from '../schemas/common';
import type { ToolResult, ToolContext, ToolAnnotations } from '../types';

const MIN_SEARCH_RESULTS = 1;
const MAX_SEARCH_RESULTS = 1000;
const DEFAULT_SEARCH_RESULTS = 50;

const MIN_CONTEXT_LINES = 0;
const MAX_CONTEXT_LINES = 10;
const DEFAULT_CONTEXT_LINES = 0;

interface SearchMatch {
  path: string;
  lineNumber: number;
  content: string;
}

const ripgrepSearchSchema = z.object({
  pattern: NonEmptyString,
  path: FilePath.default('.'),
  caseSensitive: z.boolean().default(false),
  wholeWord: z.boolean().default(false),
  literal: z
    .boolean()
    .default(true)
    .describe('Treat pattern as literal string (true) or as regex (false)'),
  includePattern: z.string().optional(),
  excludePattern: z.string().optional(),
  maxResults: z
    .number()
    .int('Must be an integer')
    .min(MIN_SEARCH_RESULTS, `Must be at least ${MIN_SEARCH_RESULTS}`)
    .max(MAX_SEARCH_RESULTS, `Must be at most ${MAX_SEARCH_RESULTS}`)
    .default(DEFAULT_SEARCH_RESULTS),
  contextLines: z
    .number()
    .int('Must be an integer')
    .min(MIN_CONTEXT_LINES, 'Cannot be negative')
    .max(MAX_CONTEXT_LINES, `Cannot exceed ${MAX_CONTEXT_LINES} lines of context`)
    .default(DEFAULT_CONTEXT_LINES),
});

export class RipgrepSearchTool extends Tool {
  name = 'ripgrep_search';
  description = `Search file contents using regex patterns. Use for text search, file_find for name patterns.
Supports glob filters (includePattern/excludePattern). Returns results using cat -n format, with line numbers starting at 1.`;
  schema = ripgrepSearchSchema;
  annotations: ToolAnnotations = {
    readOnlyHint: true,
    idempotentHint: true,
    openWorldHint: true,
    readOnlySafe: true,
  };

  protected async executeValidated(
    args: z.infer<typeof ripgrepSearchSchema>,
    context: ToolContext
  ): Promise<ToolResult> {
    if (context.signal.aborted) {
      return this.createCancellationResult();
    }
    if (!context.runtime) {
      return this.createError('Tool context missing runtime. This is a system error.');
    }

    try {
      const {
        pattern,
        path,
        caseSensitive,
        wholeWord,
        literal,
        includePattern,
        excludePattern,
        maxResults,
        contextLines,
      } = args;

      const resolvedPath = await context.runtime.paths.resolve(path);

      const ripgrepArgs = this.buildRipgrepArgs({
        pattern,
        path: resolvedPath.runtimePath,
        caseSensitive,
        wholeWord,
        literal,
        includePattern,
        excludePattern,
        maxResults,
        contextLines,
      });

      try {
        const { exitCode, stdout, stderr } = await context.runtime.process.exec(
          ['rg', ...ripgrepArgs],
          {
            cwd: context.runtime.cwd,
            env: context.processEnv,
            signal: context.signal,
          }
        );

        if (exitCode === 1) {
          return this.createResult(`No matches found for pattern: ${pattern}`);
        }

        if (exitCode !== 0) {
          const error = new Error(
            stderr || `ripgrep exited with code ${exitCode}. Check the search pattern and path.`
          );
          (error as Error & { code?: number }).code = exitCode;
          throw error;
        }

        const matches = this.parseRipgrepOutput(stdout, maxResults);
        const resultText = this.formatResults(matches, pattern, maxResults);

        return this.createResult(resultText);
      } catch (execError: unknown) {
        // ripgrep exits with code 1 when no matches found
        const err = execError as { code?: number; message?: string };
        if (err.code === 1) {
          return this.createResult(`No matches found for pattern: ${pattern}`);
        }

        // Other errors (e.g., invalid regex, file not found)
        throw execError;
      }
    } catch (error: unknown) {
      if (error instanceof Error) {
        // Check if ripgrep is not installed
        const errorCode = (error as Error & { code?: string | number }).code;
        if (
          errorCode === 'ENOENT' ||
          errorCode === 127 ||
          error.message.includes('ENOENT') ||
          error.message.includes('command not found') ||
          error.message.includes('not recognized')
        ) {
          return this.createError(
            `ripgrep (rg) command not found in ${context.runtime.label}. Install ripgrep in the active runtime to use this tool.`
          );
        }
      }

      return this.createError(
        `Search operation failed: ${error instanceof Error ? error.message : 'Unknown error occurred'}. Check the search pattern and directory path, then try again.`
      );
    }
  }

  private buildRipgrepArgs(options: {
    pattern: string;
    path: string;
    caseSensitive: boolean;
    wholeWord: boolean;
    literal: boolean;
    includePattern?: string;
    excludePattern?: string;
    maxResults: number;
    contextLines: number;
  }): string[] {
    const args = ['--line-number', '--with-filename'];

    if (!options.caseSensitive) {
      args.push('--ignore-case');
    }

    if (options.wholeWord) {
      args.push('--word-regexp');
    }

    if (options.literal) {
      args.push('--fixed-strings');
    }

    if (options.includePattern) {
      args.push('--glob', options.includePattern);
    }

    if (options.excludePattern) {
      args.push('--glob', `!${options.excludePattern}`);
    }

    // Note: We don't use --max-count here as it limits per file
    // Instead, we limit total results in parseRipgrepOutput

    if (options.contextLines > 0) {
      args.push('--context', options.contextLines.toString());
    }

    // Add pattern and path (no quotes needed with execFile)
    args.push(options.pattern);
    args.push(options.path);

    return args;
  }

  private parseRipgrepOutput(output: string, maxResults: number): SearchMatch[] {
    if (!output.trim()) {
      return [];
    }

    const lines = output.trim().split('\n');
    const matches: SearchMatch[] = [];

    for (const line of lines) {
      // ripgrep format: path:lineNumber:content
      const match = line.match(/^([^:]+):(\d+):(.*)$/);
      if (match) {
        matches.push({
          path: match[1],
          lineNumber: parseInt(match[2], 10),
          content: match[3],
        });

        // Limit total results across all files
        if (matches.length >= maxResults) {
          break;
        }
      }
    }

    return matches;
  }

  private formatResults(matches: SearchMatch[], pattern: string, maxResults: number): string {
    if (matches.length === 0) {
      return `No matches found for pattern: ${pattern}`;
    }

    const resultLines = [`Found ${matches.length} match${matches.length === 1 ? '' : 'es'}:\n`];

    // Add truncation message if we hit the limit
    if (matches.length === maxResults) {
      resultLines.push(`Results limited to ${maxResults}. Use maxResults parameter to see more.\n`);
    }

    // Group matches by file
    const fileGroups = new Map<string, SearchMatch[]>();
    for (const match of matches) {
      if (!fileGroups.has(match.path)) {
        fileGroups.set(match.path, []);
      }
      fileGroups.get(match.path)!.push(match);
    }

    for (const [filePath, fileMatches] of fileGroups) {
      resultLines.push(`${filePath}:`);

      // Calculate max line number width for right-alignment
      const maxLineNum = Math.max(...fileMatches.map((m) => m.lineNumber));
      const width = String(maxLineNum).length;

      for (const match of fileMatches) {
        const lineNumStr = String(match.lineNumber).padStart(width, ' ');
        resultLines.push(`${lineNumStr}\t${match.content}`);
      }
      resultLines.push('');
    }

    return resultLines.join('\n');
  }
}
