import fs from 'node:fs';
import path from 'node:path';
// @ts-expect-error — picomatch has no bundled types (transitive dep of fast-glob)
import picomatch from 'picomatch';
import type { FileRow, Store } from '../../db/store.js';
import { err, ok, type TraceMcpResult, validationError } from '../../errors.js';
import { validatePath } from '../../utils/security.js';

interface SearchTextMatch {
  file: string;
  language: string | null;
  line: number;
  column: number;
  match: string;
  context: string[];
}

interface SearchTextResult {
  matches: SearchTextMatch[];
  total_matches: number;
  files_searched: number;
  files_matched: number;
  truncated: boolean;
  /** True when the wall-clock budget elapsed before all files were scanned. */
  timed_out: boolean;
}

interface SearchTextOptions {
  query: string;
  isRegex?: boolean;
  filePattern?: string;
  language?: string;
  maxResults?: number;
  contextLines?: number;
  caseSensitive?: boolean;
  /**
   * Wall-clock budget in milliseconds. Catastrophic-backtracking regexes
   * cannot pin a worker for arbitrary time. Default: 2000 (2s). Set 0 to
   * disable (not recommended on shared/public deployments).
   */
  timeoutMs?: number;
}

/** Default regex wall-clock budget. Mirrors jcodemunch v1.81.2 (2s). */
const DEFAULT_TIMEOUT_MS = 2_000;

/**
 * Full-text search across indexed files. Supports regex and glob file patterns.
 *
 * Queries the DB for the file list (avoiding full filesystem scan),
 * then streams files one at a time to keep memory bounded.
 */
export function searchText(
  store: Store,
  projectRoot: string,
  opts: SearchTextOptions,
): TraceMcpResult<SearchTextResult> {
  const {
    query,
    isRegex = false,
    filePattern,
    language,
    maxResults = 50,
    contextLines = 0,
    caseSensitive = false,
    timeoutMs = DEFAULT_TIMEOUT_MS,
  } = opts;

  const startMs = Date.now();
  const budgetMs = Math.max(0, timeoutMs);
  const budgetActive = budgetMs > 0;
  const overBudget = () => budgetActive && Date.now() - startMs > budgetMs;

  if (!query || query.length === 0) {
    return err(validationError('query is required'));
  }
  if (query.length > 1000) {
    return err(validationError('query too long (max 1000 chars)'));
  }

  // Build regex from query
  let regex: RegExp;
  try {
    const flags = caseSensitive ? 'g' : 'gi';
    regex = isRegex ? new RegExp(query, flags) : new RegExp(escapeRegex(query), flags);
  } catch (e) {
    return err(validationError(`Invalid regex: ${e instanceof Error ? e.message : String(e)}`));
  }

  // Get file list from DB (single query, no N+1)
  let files: FileRow[];
  if (language) {
    files = store.db
      .prepare('SELECT * FROM files WHERE language = ? AND status != ?')
      .all(language, 'error') as FileRow[];
  } else {
    files = store.db.prepare('SELECT * FROM files WHERE status != ?').all('error') as FileRow[];
  }

  // Apply glob filter in-memory (cheaper than SQL LIKE for globs)
  if (filePattern) {
    const isMatch = picomatch(filePattern, { dot: true });
    files = files.filter((f) => isMatch(f.path));
  }

  const matches: SearchTextMatch[] = [];
  let totalMatches = 0;
  let filesMatched = 0;
  let truncated = false;
  let timedOut = false;

  for (const file of files) {
    if (matches.length >= maxResults) {
      truncated = true;
      break;
    }
    if (overBudget()) {
      timedOut = true;
      break;
    }

    const absPath = path.resolve(projectRoot, file.path);

    // Security: ensure path stays within project root
    const pathCheck = validatePath(file.path, projectRoot);
    if (pathCheck.isErr()) continue;

    let content: string;
    try {
      // Skip large files (>1MB) to prevent memory spikes
      const stat = fs.statSync(absPath);
      if (stat.size > 1_048_576) continue;
      content = fs.readFileSync(absPath, 'utf-8');
    } catch {
      continue; // File may have been deleted since indexing
    }

    const lines = content.split('\n');
    let fileHasMatch = false;

    for (let i = 0; i < lines.length; i++) {
      if (matches.length >= maxResults) {
        truncated = true;
        break;
      }
      // Cheap budget check every 64 lines: amortizes Date.now() cost while
      // still catching catastrophic-backtracking regexes within ~milliseconds
      // of the budget on a normal-sized file.
      if (budgetActive && (i & 63) === 0 && overBudget()) {
        timedOut = true;
        break;
      }

      // Reset regex lastIndex for each line (global flag)
      regex.lastIndex = 0;
      const m = regex.exec(lines[i]);
      if (!m) continue;

      totalMatches++;
      fileHasMatch = true;

      // Build context window
      const ctxStart = Math.max(0, i - contextLines);
      const ctxEnd = Math.min(lines.length - 1, i + contextLines);
      const context: string[] = [];
      for (let ci = ctxStart; ci <= ctxEnd; ci++) {
        const prefix = ci === i ? '>' : ' ';
        context.push(`${prefix} ${ci + 1}: ${lines[ci]}`);
      }

      matches.push({
        file: file.path,
        language: file.language,
        line: i + 1,
        column: m.index + 1,
        match: m[0],
        context,
      });
    }

    if (fileHasMatch) filesMatched++;
    if (timedOut) break;
  }

  return ok({
    matches,
    total_matches: totalMatches,
    files_searched: files.length,
    files_matched: filesMatched,
    truncated,
    timed_out: timedOut,
  });
}

function escapeRegex(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
