import path from "node:path"
import { readdir, readFile, stat } from "node:fs/promises"
import { execFile } from "node:child_process"
import { prisma } from "@/lib/prisma"
import type { TraceData } from "./get-trace-data"

interface Candidate {
  id: string
  code: string
  source: string
  model?: string
  explanation?: string
  rank?: number
  isBest: boolean
  parentId?: string | null
}

interface LlmCallSummary {
  id: string
  call_type: string | null
  model_name: string | null
  status: string
  latency_ms: number | null
  llm_cost: number | null
  total_tokens: number | null
  context: Record<string, unknown> | null
}

export interface IndexedTraceData {
  functionName: string | null
  filePath: string | null
  originalCode: string | null
  testFramework: string | null
  generatedTests: string[]
  instrumentedTests: string[]
  instrumentedPerfTests: string[]
  candidates: Candidate[]
  candidatesBySource: Map<string, Candidate[]>
  candidatesById: Map<string, Candidate>
  testModelMap: Map<number, string | null>
  totalCost: number
  totalTokens: number
  ranking: { ranking: string[]; explanation: string } | null
  usedForPr: boolean
  errors: Array<{
    error_type: string | null
    severity: string | null
    error_message: string | null
    context: Record<string, unknown> | null
  }>
  llmCalls: LlmCallSummary[]
}

export function indexTraceData(traceData: TraceData): IndexedTraceData {
  const { rawLlmCalls, errors, optimizationFeatures, optimizationEvent } = traceData

  const metadata = optimizationFeatures?.metadata as Record<string, unknown> | undefined
  const functionName =
    (metadata?.function_to_optimize as string) ?? optimizationEvent?.function_name ?? null
  const filePath =
    optimizationEvent?.file_path ?? (metadata?.file_path as string | undefined) ?? null

  const optimizationsOrigin =
    (optimizationFeatures?.optimizations_origin as Record<
      string,
      { source: string; model?: string; call_sequence?: number; parent?: string }
    >) || {}

  const candidateExplanations =
    (optimizationFeatures?.explanations_post as Record<string, string>) || {}

  const rankingData = optimizationFeatures?.ranking as {
    ranking?: string[]
    explanation?: string
  } | null

  const rankingList = rankingData?.ranking ?? []
  const rankMap = new Map(rankingList.map((id, i) => [id, i + 1]))
  const bestId = rankingList[0] ?? null

  const candidates = optimizationFeatures?.optimizations_post
    ? Object.entries(optimizationFeatures.optimizations_post as Record<string, string>)
        .map(([id, code]) => ({
          id,
          code: typeof code === "string" ? code : "",
          source: optimizationsOrigin[id]?.source || "OPTIMIZE",
          model: optimizationsOrigin[id]?.model,
          explanation: candidateExplanations[id],
          rank: rankMap.get(id),
          isBest: id === bestId,
          parentId: optimizationsOrigin[id]?.parent ?? null,
          callSequence: optimizationsOrigin[id]?.call_sequence,
        }))
        .sort((a, b) => (a.callSequence ?? Infinity) - (b.callSequence ?? Infinity))
    : []

  const pullRequestRaw = optimizationFeatures?.pull_request
  const usedForPr = Boolean(
    pullRequestRaw != null &&
    typeof pullRequestRaw === "object" &&
    !Array.isArray(pullRequestRaw) &&
    Object.keys(pullRequestRaw as Record<string, unknown>).length > 0,
  )

  // Pre-build lookup maps for O(1) access in tool calls
  const candidatesBySource = new Map<string, typeof candidates>()
  const candidatesById = new Map<string, (typeof candidates)[0]>()
  for (const c of candidates) {
    candidatesById.set(c.id, c)
    const group = candidatesBySource.get(c.source)
    if (group) group.push(c)
    else candidatesBySource.set(c.source, [c])
  }

  const llmCalls = rawLlmCalls.map(
    (c: {
      id: string
      call_type: string | null
      model_name: string | null
      status: string
      latency_ms: number | null
      llm_cost: number | null
      total_tokens: number | null
      context: unknown
    }) => ({
      id: c.id,
      call_type: c.call_type,
      model_name: c.model_name,
      status: c.status,
      latency_ms: c.latency_ms,
      llm_cost: c.llm_cost,
      total_tokens: c.total_tokens,
      context: c.context as Record<string, unknown> | null,
    }),
  )

  // Pre-compute test group → model mapping and cost/token totals in single passes
  const testModelMap = new Map<number, string | null>()
  let totalCost = 0
  let totalTokens = 0
  for (const c of llmCalls) {
    totalCost += c.llm_cost ?? 0
    totalTokens += c.total_tokens ?? 0
    if (c.call_type === "test_generation" || c.call_type === "testgen") {
      const ctx = c.context
      if (ctx?.test_index != null) {
        testModelMap.set(ctx.test_index as number, c.model_name)
      }
    }
  }

  return {
    functionName,
    filePath,
    originalCode: optimizationFeatures?.original_code ?? null,
    testFramework: optimizationFeatures?.test_framework ?? null,
    generatedTests: optimizationFeatures?.generated_test ?? [],
    instrumentedTests: optimizationFeatures?.instrumented_generated_test ?? [],
    instrumentedPerfTests: optimizationFeatures?.instrumented_perf_test ?? [],
    candidates,
    candidatesBySource,
    candidatesById,
    testModelMap,
    totalCost,
    totalTokens,
    ranking: rankingData?.ranking
      ? { ranking: rankingData.ranking, explanation: rankingData.explanation ?? "" }
      : null,
    usedForPr,
    errors: errors.map(
      (e: {
        error_type: string | null
        severity: string | null
        error_message: string | null
        context: unknown
      }) => ({
        error_type: e.error_type,
        severity: e.severity,
        error_message: e.error_message,
        context: e.context as Record<string, unknown> | null,
      }),
    ),
    llmCalls,
  }
}

// --- Codebase browsing helpers ---

function getRepoRoot(repo: string): string | null {
  if (repo === "codeflash-internal") return process.env.AISERVICE_DIR || null
  if (repo === "codeflash") return process.env.CODEFLASH_DIR || null
  return null
}

function resolveAndValidatePath(
  repoRoot: string,
  relativePath: string,
): { resolved: string } | { error: string } {
  const normalized = path.normalize(relativePath)
  if (normalized.startsWith("..") || path.isAbsolute(normalized)) {
    return { error: "Path traversal is not allowed." }
  }
  const resolved = path.resolve(repoRoot, normalized)
  if (!resolved.startsWith(repoRoot)) {
    return { error: "Path traversal is not allowed." }
  }
  return { resolved }
}

function truncate(s: string, max: number): string {
  if (s.length <= max) return s
  return s.slice(0, max) + `\n... [truncated at ${max} chars]`
}

// --- System prompt ---

export function buildSummaryPrompt(data: IndexedTraceData): string {
  const lines: string[] = []

  // === TRACE DATA (top — long-context best practice: data before instructions) ===
  lines.push("<trace_data>")

  if (data.functionName) lines.push(`Function: ${data.functionName}`)
  if (data.filePath) lines.push(`File: ${data.filePath}`)

  lines.push(
    `LLM calls: ${data.llmCalls.length} (total cost: $${data.totalCost.toFixed(4)}, tokens: ${data.totalTokens})`,
  )

  if (data.testFramework) lines.push(`Test framework: ${data.testFramework}`)
  if (data.generatedTests.length > 0) {
    lines.push(`Generated test groups: ${data.generatedTests.length}`)
    for (let i = 0; i < data.generatedTests.length; i++) {
      const model = data.testModelMap.get(i)
      lines.push(`  - Test group ${i + 1}: model=${model ?? "unknown"}`)
    }
  } else {
    lines.push(`Generated tests: 0`)
  }

  const candidateGroups = [
    { source: "OPTIMIZE", label: "Optimization candidates", prefix: "OPTIMIZE" },
    { source: "OPTIMIZE_LP", label: "Line profiler candidates", prefix: "LP" },
    { source: "REFINE", label: "Refinement candidates", prefix: "Refinement" },
  ]

  for (const { source, label, prefix } of candidateGroups) {
    const candidates = data.candidatesBySource.get(source) ?? []
    if (candidates.length > 0) {
      lines.push(`${label}: ${candidates.length}`)
      for (let i = 0; i < candidates.length; i++) {
        const c = candidates[i]
        const rank = c.rank ? ` (rank #${c.rank})` : ""
        const best = c.isBest ? " [BEST]" : ""
        lines.push(`  - ${prefix} #${i + 1}: model=${c.model ?? "unknown"}${rank}${best}`)
      }
    }
  }

  if (data.ranking) {
    const bestCandidate = data.candidatesById.get(data.ranking.ranking[0] ?? "")
    lines.push(
      `Ranking: ${data.ranking.ranking.length} candidates ranked. Best: ${bestCandidate?.source ?? "unknown"}`,
    )
    lines.push(`Used for PR: ${data.usedForPr ? "Yes" : "No"}`)
  }

  if (data.errors.length > 0) {
    lines.push(`Errors: ${data.errors.length}`)
    for (const e of data.errors) {
      lines.push(
        `  - [${e.severity ?? "?"}] ${e.error_type}: ${(e.error_message ?? "").slice(0, 120)}`,
      )
    }
  }

  if (data.llmCalls.length > 0) {
    lines.push("")
    lines.push("LLM call IDs (for get_llm_call_detail):")
    for (const c of data.llmCalls) {
      const cost = c.llm_cost != null ? ` $${c.llm_cost.toFixed(4)}` : ""
      lines.push(
        `  - ${c.id}: ${c.call_type ?? "unknown"} (${c.model_name ?? "unknown"}, ${c.status}${cost})`,
      )
    }
  }

  lines.push("</trace_data>")

  // === ROLE (concise — Opus 4.6 is more responsive, no aggressive language needed) ===
  lines.push("")
  lines.push("<role>")
  lines.push(
    "You are a senior optimization engineer investigating Codeflash traces. " +
      "Your goal is to diagnose problems, trace them to root causes in code or prompts, " +
      "and recommend specific, actionable fixes with file paths and line numbers.",
  )
  lines.push("</role>")

  // === DOMAIN KNOWLEDGE ===
  lines.push("")
  lines.push("<domain_knowledge>")
  lines.push(
    "Codeflash testing principles:\n" +
      "- Tests must use real objects. Mock, MagicMock, SimpleNamespace, patch, and all other " +
      "fake/stub objects are equally bad — they hide real behavior and silently pass on wrong " +
      "attribute access. Flag any of these as a test quality issue. Do not describe one fake " +
      "approach as 'better' than another.\n" +
      "- The correct approach is constructing real instances using actual class constructors.\n" +
      "- If many candidates fail or are unranked, check whether mock/fake objects in tests could " +
      "be the cause before blaming the optimizations.\n\n" +
      "Key paths in codeflash-internal:\n" +
      "- django/aiservice/core/shared/ — optimizer_router, testgen_router, ranker\n" +
      "- django/aiservice/core/languages/python/optimizer/ — Python optimizer pipeline\n" +
      "- django/aiservice/core/languages/python/testgen/ — test generation pipeline\n" +
      "- django/aiservice/aiservice/llm.py — LLM provider abstraction\n" +
      "- Prompt templates are .md files alongside their modules (rendered with Jinja2)",
  )
  lines.push("</domain_knowledge>")

  // === GUIDELINES (general approach — let adaptive thinking handle the details) ===
  lines.push("")
  lines.push("<guidelines>")
  lines.push(
    "When investigating a problem, go deeper than the surface-level observation. " +
      "Use trace data tools to understand what happened, then use get_llm_call_detail to read " +
      "the actual prompts and responses involved. If the root cause is in a prompt or pipeline, " +
      "use search_code and read_file to find the source file and reference specific lines.\n\n" +
      "Ground your recommendations in code you actually read — name the file, quote the relevant " +
      "section, and describe what to change. Generic advice like 'strengthen the prompt' is not " +
      "sufficient when you can find the actual file.\n\n" +
      "Trust user-provided information (CLI output, errors, logs) as ground truth, even when " +
      "trace data doesn't confirm it. The trace is incomplete.\n\n" +
      "Be concise. Reference specific candidates, file paths, and line numbers.",
  )
  lines.push("</guidelines>")

  lines.push("")
  lines.push("<use_parallel_tool_calls>")
  lines.push(
    "If you intend to call multiple tools and there are no dependencies between the calls, " +
      "make all independent calls in parallel. For example, get_errors and get_test_code can " +
      "run simultaneously. Do not guess parameters — only parallelize when inputs are known.",
  )
  lines.push("</use_parallel_tool_calls>")

  return lines.join("\n")
}

export const anthropicToolDefinitions = [
  {
    name: "get_original_code",
    description:
      "Returns the original source code of the function being optimized. Useful for comparing " +
      "with candidate code or understanding patterns that explain optimization failures.",
    input_schema: { type: "object" as const, properties: {} },
  },
  {
    name: "get_candidate_code",
    description:
      "Returns the optimized code, explanation, model, and rank for a specific candidate. " +
      "Useful for investigating why a candidate was ranked low or failed tests.",
    input_schema: {
      type: "object" as const,
      properties: {
        source_type: {
          type: "string",
          enum: ["OPTIMIZE", "OPTIMIZE_LP", "REFINE"],
          description:
            "The type of candidate: OPTIMIZE, OPTIMIZE_LP (line profiler), or REFINE (refinement).",
        },
        index: {
          type: "number",
          description: "1-based index of the candidate within its type.",
        },
      },
      required: ["source_type", "index"],
    },
  },
  {
    name: "get_test_code",
    description:
      "Returns the generated test code for a specific test group with model info. " +
      "Useful for investigating test quality (mocks, bad assertions) or when candidates fail tests.",
    input_schema: {
      type: "object" as const,
      properties: {
        index: {
          type: "number",
          description: "1-based index of the test group.",
        },
      },
      required: ["index"],
    },
  },
  {
    name: "get_ranking_details",
    description:
      "Returns the full ranking explanation and ordered candidate list with models and sources.",
    input_schema: { type: "object" as const, properties: {} },
  },
  {
    name: "get_errors",
    description:
      "Returns all errors from the optimization: test failures, parsing errors, pipeline errors, " +
      "and their context. Good starting point when investigating failures.",
    input_schema: { type: "object" as const, properties: {} },
  },
  {
    name: "get_llm_call_detail",
    description:
      "Fetches the full system prompt, user prompt, raw LLM response, parsed response, and " +
      "parsing results for any LLM call in this trace. Shows exactly what instructions the " +
      "model received and what it produced. Useful for investigating bad tests, bad " +
      "optimizations, parsing failures, and ranking decisions.",
    input_schema: {
      type: "object" as const,
      properties: {
        call_id: {
          type: "string",
          description: "UUID of the LLM call (from the trace overview).",
        },
      },
      required: ["call_id"],
    },
  },
  {
    name: "read_file",
    description:
      "Read a file from the codeflash-internal or codeflash CLI repository with line numbers. " +
      "Useful for reading full context around search matches, prompt templates (.md files), " +
      "and pipeline code.",
    input_schema: {
      type: "object" as const,
      properties: {
        repo: {
          type: "string",
          enum: ["codeflash-internal", "codeflash"],
          description: "Which repository to read from.",
        },
        path: {
          type: "string",
          description:
            "Relative path within the repo (e.g., 'django/aiservice/core/shared/optimizer_router.py').",
        },
        start_line: {
          type: "number",
          description: "1-based start line (default: 1).",
        },
        end_line: {
          type: "number",
          description: "1-based end line (default: start_line + 499).",
        },
      },
      required: ["repo", "path"],
    },
  },
  {
    name: "search_code",
    description:
      "Ripgrep search across a repository. Returns matching lines with file paths and line " +
      "numbers. Useful for finding prompt templates, pipeline code, or specific implementations. " +
      "Search for distinctive phrases from LLM prompts to locate template files.",
    input_schema: {
      type: "object" as const,
      properties: {
        repo: {
          type: "string",
          enum: ["codeflash-internal", "codeflash"],
          description: "Which repository to search.",
        },
        pattern: {
          type: "string",
          description:
            "Regex pattern to search for (ripgrep syntax). Be specific — use distinctive phrases.",
        },
        glob: {
          type: "string",
          description:
            "File glob filter (e.g., '*.py', '*.md', '*.ts'). Use '*.md' for prompt templates.",
        },
        max_results: {
          type: "number",
          description: "Maximum number of matching lines to return (default: 30, max: 100).",
        },
      },
      required: ["repo", "pattern"],
    },
  },
  {
    name: "list_directory",
    description:
      "List files and directories in a repository path. Useful for orienting in unfamiliar " +
      "parts of the codebase or discovering prompt template files alongside pipeline modules.",
    input_schema: {
      type: "object" as const,
      properties: {
        repo: {
          type: "string",
          enum: ["codeflash-internal", "codeflash"],
          description: "Which repository to browse.",
        },
        path: {
          type: "string",
          description: "Relative path within the repo (default: root).",
        },
      },
      required: ["repo"],
    },
  },
]

export async function resolveToolCall(
  name: string,
  args: Record<string, unknown>,
  data: IndexedTraceData,
): Promise<string> {
  switch (name) {
    case "get_original_code":
      return data.originalCode ?? "No original code available."

    case "get_candidate_code": {
      const sourceType = args.source_type as string
      const index = args.index as number
      const filtered = data.candidatesBySource.get(sourceType) ?? []
      const candidate = filtered[index - 1]
      if (!candidate) {
        return `No ${sourceType} candidate at index ${index}. Available: ${filtered.length}`
      }
      const parts = [`Source: ${candidate.source}`, `Rank: ${candidate.rank ?? "unranked"}`]
      if (candidate.isBest) parts.push("This is the BEST candidate.")
      if (candidate.model) parts.push(`Model: ${candidate.model}`)
      if (candidate.explanation) parts.push(`\nExplanation: ${candidate.explanation}`)
      parts.push(`\nCode:\n\`\`\`python\n${candidate.code}\n\`\`\``)
      return parts.join("\n")
    }

    case "get_test_code": {
      const testIndex = (args.index as number) - 1
      const parts: string[] = []
      const gen = data.generatedTests[testIndex]
      const instr = data.instrumentedTests[testIndex]
      const instrPerf = data.instrumentedPerfTests[testIndex]

      if (!gen && !instr && !instrPerf) {
        return `No test at index ${testIndex + 1}. Available: ${data.generatedTests.length}`
      }
      const model = data.testModelMap.get(testIndex)
      parts.push(`Model: ${model ?? "unknown"}`)
      if (gen) parts.push(`Generated test:\n\`\`\`python\n${gen}\n\`\`\``)
      if (instr) parts.push(`Instrumented test:\n\`\`\`python\n${instr}\n\`\`\``)
      if (instrPerf)
        parts.push(`Performance-instrumented test:\n\`\`\`python\n${instrPerf}\n\`\`\``)
      return parts.join("\n\n")
    }

    case "get_ranking_details": {
      if (!data.ranking) return "No ranking data available."
      const parts = [`Explanation: ${data.ranking.explanation}`, "", "Ordered ranking:"]
      for (let i = 0; i < data.ranking.ranking.length; i++) {
        const id = data.ranking.ranking[i]
        const cand = data.candidatesById.get(id)
        const label = cand ? `${cand.source} (model: ${cand.model ?? "unknown"})` : id
        parts.push(`  #${i + 1}: ${label}${i === 0 ? " [BEST]" : ""}`)
      }
      parts.push(`\nUsed for PR: ${data.usedForPr ? "Yes" : "No"}`)
      return parts.join("\n")
    }

    case "get_errors": {
      if (data.errors.length === 0) return "No errors in this trace."
      const parts = data.errors.map(e => {
        const lines = [`[${e.severity ?? "?"}] ${e.error_type}: ${e.error_message}`]
        if (e.context) {
          const ctx = e.context as Record<string, string>
          if (ctx.test_name) lines.push(`  Test: ${ctx.test_name}`)
          if (ctx.failure_reason) lines.push(`  Reason: ${ctx.failure_reason}`)
          if (ctx.test_output) lines.push(`  Output: ${ctx.test_output.slice(0, 500)}`)
        }
        return lines.join("\n")
      })
      return parts.join("\n\n")
    }

    case "get_llm_call_detail": {
      const callId = args.call_id as string
      if (!data.llmCalls.some(c => c.id === callId)) {
        return `LLM call ${callId} not found in this trace. Available IDs: ${data.llmCalls.map(c => c.id).join(", ")}`
      }
      const row = await prisma.llm_calls.findUnique({
        where: { id: callId },
        select: {
          call_type: true,
          model_name: true,
          status: true,
          system_prompt: true,
          user_prompt: true,
          messages: true,
          raw_response: true,
          parsed_response: true,
          temperature: true,
          n_candidates: true,
          max_tokens: true,
          prompt_tokens: true,
          completion_tokens: true,
          total_tokens: true,
          llm_cost: true,
          latency_ms: true,
          parsing_status: true,
          candidates_generated: true,
          candidates_valid: true,
          parsing_errors: true,
          error_type: true,
          error_message: true,
        },
      })
      if (!row) return `LLM call ${callId} not found in database.`

      const MAX_FIELD = 15_000
      const parts: string[] = []
      parts.push(`Call type: ${row.call_type}`)
      parts.push(`Model: ${row.model_name}`)
      parts.push(`Status: ${row.status}`)
      if (row.temperature != null) parts.push(`Temperature: ${row.temperature}`)
      if (row.n_candidates != null) parts.push(`N candidates requested: ${row.n_candidates}`)
      if (row.max_tokens != null) parts.push(`Max tokens: ${row.max_tokens}`)
      parts.push(
        `Tokens: ${row.prompt_tokens ?? "?"}p / ${row.completion_tokens ?? "?"}c / ${row.total_tokens ?? "?"}t`,
      )
      if (row.llm_cost != null) parts.push(`Cost: $${row.llm_cost.toFixed(4)}`)
      if (row.latency_ms != null) parts.push(`Latency: ${row.latency_ms}ms`)

      if (row.system_prompt) {
        parts.push(`\n--- System Prompt ---\n${truncate(row.system_prompt, MAX_FIELD)}`)
      }
      if (row.user_prompt) {
        parts.push(`\n--- User Prompt ---\n${truncate(row.user_prompt, MAX_FIELD)}`)
      }
      if (row.messages) {
        const messagesStr = JSON.stringify(row.messages, null, 2)
        parts.push(`\n--- Messages ---\n${truncate(messagesStr, MAX_FIELD)}`)
      }
      if (row.raw_response) {
        parts.push(`\n--- Raw Response ---\n${truncate(row.raw_response, MAX_FIELD)}`)
      }
      if (row.parsed_response) {
        const parsedStr = JSON.stringify(row.parsed_response, null, 2)
        parts.push(`\n--- Parsed Response ---\n${truncate(parsedStr, MAX_FIELD)}`)
      }

      if (row.parsing_status) {
        parts.push(`\nParsing status: ${row.parsing_status}`)
        if (row.candidates_generated != null)
          parts.push(`Candidates generated: ${row.candidates_generated}`)
        if (row.candidates_valid != null) parts.push(`Candidates valid: ${row.candidates_valid}`)
        if (row.parsing_errors) {
          parts.push(
            `Parsing errors: ${truncate(JSON.stringify(row.parsing_errors, null, 2), 5000)}`,
          )
        }
      }

      if (row.error_type) parts.push(`\nError: [${row.error_type}] ${row.error_message ?? ""}`)

      return parts.join("\n")
    }

    case "read_file": {
      const repoRoot = getRepoRoot(args.repo as string)
      if (!repoRoot)
        return `Repository path not configured. Set AISERVICE_DIR or CODEFLASH_DIR env var.`

      const pathResult = resolveAndValidatePath(repoRoot, args.path as string)
      if ("error" in pathResult) return pathResult.error

      try {
        const fileStat = await stat(pathResult.resolved)
        if (!fileStat.isFile()) return `Not a file: ${args.path}`
        if (fileStat.size > 1_000_000) return `File too large (${fileStat.size} bytes). Max 1MB.`

        const content = await readFile(pathResult.resolved, "utf-8")
        const allLines = content.split("\n")
        const startLine = Math.max(1, (args.start_line as number) || 1)
        const endLine = Math.min(allLines.length, (args.end_line as number) || startLine + 499)
        const slice = allLines.slice(startLine - 1, endLine)

        const numbered = slice.map((line, i) => `${startLine + i}: ${line}`).join("\n")
        const header = `File: ${args.path} (lines ${startLine}-${endLine} of ${allLines.length})`
        return `${header}\n\n${numbered}`
      } catch (err) {
        if ((err as NodeJS.ErrnoException).code === "ENOENT") return `File not found: ${args.path}`
        return `Error reading file: ${(err as Error).message}`
      }
    }

    case "search_code": {
      const repoRoot = getRepoRoot(args.repo as string)
      if (!repoRoot)
        return `Repository path not configured. Set AISERVICE_DIR or CODEFLASH_DIR env var.`

      const maxResults = Math.min(Math.max(1, (args.max_results as number) || 30), 100)
      const rgArgs = [
        "--no-heading",
        "--line-number",
        "--color=never",
        "--max-count",
        String(maxResults),
        "--glob",
        "!.git",
        "--glob",
        "!node_modules",
        "--glob",
        "!__pycache__",
        "--glob",
        "!.next",
        "--glob",
        "!dist",
      ]
      if (args.glob) {
        rgArgs.push("--glob", args.glob as string)
      }
      rgArgs.push(args.pattern as string, repoRoot)

      return new Promise<string>(resolve => {
        execFile("rg", rgArgs, { timeout: 10_000, maxBuffer: 1_000_000 }, (err, stdout, stderr) => {
          if (err && !stdout) {
            if ((err as NodeJS.ErrnoException).code === "ENOENT") {
              resolve("ripgrep (rg) not found. Install it to use search_code.")
              return
            }
            if (stderr) {
              resolve(`Search error: ${stderr.slice(0, 500)}`)
              return
            }
            resolve("No matches found.")
            return
          }
          // Strip the repo root prefix from output paths for readability
          const cleaned = stdout.replace(
            new RegExp(repoRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/", "g"),
            "",
          )
          resolve(cleaned.trim() || "No matches found.")
        })
      })
    }

    case "list_directory": {
      const repoRoot = getRepoRoot(args.repo as string)
      if (!repoRoot)
        return `Repository path not configured. Set AISERVICE_DIR or CODEFLASH_DIR env var.`

      const relativePath = (args.path as string) || "."
      const pathResult = resolveAndValidatePath(repoRoot, relativePath)
      if ("error" in pathResult) return pathResult.error

      try {
        const entries = await readdir(pathResult.resolved, { withFileTypes: true })
        const dirs: string[] = []
        const files: string[] = []
        for (const entry of entries) {
          if (entry.name === ".git") continue
          if (entry.isDirectory()) dirs.push(entry.name + "/")
          else files.push(entry.name)
        }
        dirs.sort()
        files.sort()
        const all = [...dirs, ...files]
        if (all.length === 0) return `Empty directory: ${relativePath}`
        const capped = all.slice(0, 200)
        const suffix = all.length > 200 ? `\n... and ${all.length - 200} more entries` : ""
        return `Directory: ${relativePath}\n\n${capped.join("\n")}${suffix}`
      } catch (err) {
        if ((err as NodeJS.ErrnoException).code === "ENOENT")
          return `Directory not found: ${relativePath}`
        if ((err as NodeJS.ErrnoException).code === "ENOTDIR")
          return `Not a directory: ${relativePath}`
        return `Error listing directory: ${(err as Error).message}`
      }
    }

    default:
      return `Unknown tool: ${name}`
  }
}
