import { parseJSON } from "partial-json"

import { type ToolName, toolNames, type FileEntry } from "@roo-code/types"
import { fixBrowserLaunchAction } from "../../utils/fixbrowserLaunchAction"
import { customToolRegistry } from "@roo-code/core"

import {
	type ToolUse,
	type McpToolUse,
	type ToolParamName,
	type NativeToolArgs,
	toolParamNames,
} from "../../shared/tools"
import { resolveToolAlias } from "../prompts/tools/filter-tools-for-mode"
import type {
	ApiStreamToolCallStartChunk,
	ApiStreamToolCallDeltaChunk,
	ApiStreamToolCallEndChunk,
} from "../../api/transform/stream"
import { fixAskMultipleChoiceFinalToolUseResult, fixNativeToolname } from "../../utils/fixNativeToolname"
import { MCP_TOOL_PREFIX, MCP_TOOL_SEPARATOR, parseMcpToolName, normalizeMcpToolName } from "../../utils/mcp-name"
import { defaultModeSlug } from "../../shared/modes"

/**
 * Helper type to extract properly typed native arguments for a given tool.
 * Returns the type from NativeToolArgs if the tool is defined there, otherwise never.
 */
type NativeArgsFor<TName extends ToolName> = TName extends keyof NativeToolArgs ? NativeToolArgs[TName] : never

/**
 * Parser for native tool calls (OpenAI-style function calling).
 * Converts native tool call format to ToolUse format for compatibility
 * with existing tool execution infrastructure.
 *
 * For tools with refactored parsers (e.g., read_file), this parser provides
 * typed arguments via nativeArgs. Tool-specific handlers should consume
 * nativeArgs directly rather than relying on synthesized legacy params.
 */
/**
 * Event types returned from raw chunk processing.
 */
export type ToolCallStreamEvent = ApiStreamToolCallStartChunk | ApiStreamToolCallDeltaChunk | ApiStreamToolCallEndChunk

/**
 * Parser for native tool calls (OpenAI-style function calling).
 * Converts native tool call format to ToolUse format for compatibility
 * with existing tool execution infrastructure.
 *
 * For tools with refactored parsers (e.g., read_file), this parser provides
 * typed arguments via nativeArgs. Tool-specific handlers should consume
 * nativeArgs directly rather than relying on synthesized legacy params.
 *
 * This class also handles raw tool call chunk processing, converting
 * provider-level raw chunks into start/delta/end events.
 */
export class NativeToolCallParser {
	// Streaming state management for argument accumulation (keyed by tool call id)
	// Note: name is string to accommodate dynamic MCP tools (mcp--serverName--toolName)
	private static streamingToolCalls = new Map<
		string,
		{
			id: string
			name: string
			argumentsAccumulator: string
		}
	>()

	// Raw chunk tracking state (keyed by index from API stream)
	private static rawChunkTracker = new Map<
		number,
		{
			id: string
			name: string
			hasStarted: boolean
			deltaBuffer: string[]
		}
	>()

	private static coerceOptionalBoolean(value: unknown): boolean | undefined {
		if (typeof value === "boolean") {
			return value
		}
		if (typeof value === "string") {
			const lower = value.trim().toLowerCase()
			if (lower === "true") {
				return true
			}
			if (lower === "false") {
				return false
			}
		}
		return undefined
	}

	/**
	 * Normalize parameter value to the expected type.
	 * Handles common LLM type mismatches:
	 * - Stringified objects/arrays: '[1,2,3]' -> [1,2,3], '{"a":1}' -> {a:1}
	 * - Stringified primitive strings: '"hello"' -> 'hello'
	 * - Already correct types: returned as-is
	 *
	 * @param value - The value to normalize
	 * @returns The normalized value
	 */
	private static normalizeTypeValue(value: unknown): any {
		// If value is not a string, return as-is
		if (typeof value !== "string") {
			return value
		}

		const trimmed = value.trim()

		// Check if it's a JSON string (starts with { or [ or " for quoted strings)
		if (
			(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
			(trimmed.startsWith("[") && trimmed.endsWith("]")) ||
			(trimmed.startsWith('"') && trimmed.endsWith('"'))
		) {
			try {
				const rst = JSON.parse(trimmed)
				if (typeof rst === "string") {
					return rst
				}
				return value
			} catch {
				// If parsing fails, return original string
				return value
			}
		}

		// Return as-is for plain strings
		return value
	}

	/**
	 * Process a raw tool call chunk from the API stream.
	 * Handles tracking, buffering, and emits start/delta/end events.
	 *
	 * This is the entry point for providers that emit tool_call_partial chunks.
	 * Returns an array of events to be processed by the consumer.
	 */
	public static processRawChunk(chunk: {
		index: number
		id?: string
		name?: string
		arguments?: string
	}): ToolCallStreamEvent[] {
		const events: ToolCallStreamEvent[] = []
		const { index, id, name, arguments: args } = chunk

		let tracked = this.rawChunkTracker.get(index)

		// Initialize new tool call tracking when we receive an id
		if (id && !tracked) {
			tracked = {
				id,
				name: name || "",
				hasStarted: false,
				deltaBuffer: [],
			}
			this.rawChunkTracker.set(index, tracked)
		}

		if (!tracked) {
			return events
		}

		// Update name if present in chunk and not yet set
		if (name) {
			tracked.name = name
		}

		// Emit start event when we have the name
		if (!tracked.hasStarted && tracked.name) {
			events.push({
				type: "tool_call_start",
				id: tracked.id,
				name: tracked.name,
			})
			tracked.hasStarted = true

			// Flush buffered deltas
			for (const bufferedDelta of tracked.deltaBuffer) {
				events.push({
					type: "tool_call_delta",
					id: tracked.id,
					delta: bufferedDelta,
				})
			}
			tracked.deltaBuffer = []
		}

		// Emit delta event for argument chunks
		if (args) {
			if (tracked.hasStarted) {
				events.push({
					type: "tool_call_delta",
					id: tracked.id,
					delta: args,
				})
			} else {
				tracked.deltaBuffer.push(args)
			}
		}

		return events
	}

	/**
	 * Process stream finish reason.
	 * Emits end events when finish_reason is 'tool_calls'.
	 */
	public static processFinishReason(finishReason: string | null | undefined): ToolCallStreamEvent[] {
		const events: ToolCallStreamEvent[] = []

		if (finishReason === "tool_calls" && this.rawChunkTracker.size > 0) {
			for (const [, tracked] of this.rawChunkTracker.entries()) {
				events.push({
					type: "tool_call_end",
					id: tracked.id,
				})
			}
		}

		return events
	}

	/**
	 * Finalize any remaining tool calls that weren't explicitly ended.
	 * Should be called at the end of stream processing.
	 */
	public static finalizeRawChunks(): ToolCallStreamEvent[] {
		const events: ToolCallStreamEvent[] = []

		if (this.rawChunkTracker.size > 0) {
			for (const [, tracked] of this.rawChunkTracker.entries()) {
				if (tracked.hasStarted) {
					events.push({
						type: "tool_call_end",
						id: tracked.id,
					})
				}
			}
			this.rawChunkTracker.clear()
		}

		return events
	}

	/**
	 * Clear all raw chunk tracking state.
	 * Should be called when a new API request starts.
	 */
	public static clearRawChunkState(): void {
		this.rawChunkTracker.clear()
	}

	/**
	 * Start streaming a new tool call.
	 * Initializes tracking for incremental argument parsing.
	 * Accepts string to support both ToolName and dynamic MCP tools (mcp--serverName--toolName).
	 */
	public static startStreamingToolCall(id: string, name: string): void {
		this.streamingToolCalls.set(id, {
			id,
			name: fixNativeToolname(name),
			argumentsAccumulator: "",
		})
	}

	/**
	 * Clear all streaming tool call state.
	 * Should be called when a new API request starts to prevent memory leaks
	 * from interrupted streams.
	 */
	public static clearAllStreamingToolCalls(): void {
		this.streamingToolCalls.clear()
	}

	/**
	 * Check if there are any active streaming tool calls.
	 * Useful for debugging and testing.
	 */
	public static hasActiveStreamingToolCalls(): boolean {
		return this.streamingToolCalls.size > 0
	}

	/**
	 * Process a chunk of JSON arguments for a streaming tool call.
	 * Uses partial-json-parser to extract values from incomplete JSON immediately.
	 * Returns a partial ToolUse with currently parsed parameters.
	 */
	public static processStreamingChunk(id: string, chunk: string): ToolUse | null {
		const toolCall = this.streamingToolCalls.get(id)
		if (!toolCall) {
			return null
		}

		// Accumulate the JSON string
		toolCall.argumentsAccumulator += chunk

		// For dynamic MCP tools, we don't return partial updates - wait for final
		const mcpPrefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR
		if (toolCall.name.startsWith(mcpPrefix)) {
			return null
		}

		// Parse whatever we can from the incomplete JSON!
		// partial-json-parser extracts partial values (strings, arrays, objects) immediately
		try {
			const partialArgs = parseJSON(toolCall.argumentsAccumulator)

			// Resolve tool alias to canonical name
			const resolvedName = resolveToolAlias(toolCall.name) as ToolName
			// Preserve original name if it differs from resolved (i.e., it was an alias)
			const originalName = toolCall.name !== resolvedName ? toolCall.name : undefined

			// Create partial ToolUse with extracted values
			return this.createPartialToolUse(
				toolCall.id,
				resolvedName,
				partialArgs || {},
				true, // partial
				originalName,
			)
		} catch {
			// Even partial-json-parser can fail on severely malformed JSON
			// Return null and wait for next chunk
			return null
		}
	}

	/**
	 * Finalize a streaming tool call.
	 * Parses the complete JSON and returns the final ToolUse or McpToolUse.
	 */
	public static finalizeStreamingToolCall(id: string, isCostrict?: boolean): ToolUse | McpToolUse | null {
		const toolCall = this.streamingToolCalls.get(id)
		if (!toolCall) {
			return null
		}

		// Parse the complete accumulated JSON
		// Cast to any for the name since parseToolCall handles both ToolName and dynamic MCP tools
		const finalToolUse = this.parseToolCall(
			{
				id: toolCall.id,
				name: toolCall.name as ToolName,
				arguments:
					(toolCall.name as ToolName) === "ask_multiple_choice" && isCostrict
						? fixAskMultipleChoiceFinalToolUseResult(toolCall.argumentsAccumulator)
						: toolCall.argumentsAccumulator,
			},
			isCostrict,
		)
		// Clean up streaming state
		this.streamingToolCalls.delete(id)

		return finalToolUse
	}

	private static coerceOptionalNumber(value: unknown): number | undefined {
		if (typeof value === "number" && Number.isFinite(value)) {
			return value
		}
		if (typeof value === "string") {
			const n = Number(value)
			if (Number.isFinite(n)) {
				return n
			}
		}
		return undefined
	}

	/**
	 * Convert raw file entries from API (with line_ranges) to FileEntry objects
	 * (with lineRanges). Handles multiple formats for backward compatibility:
	 *
	 * New tuple format: { path: string, line_ranges: [[1, 50], [100, 150]] }
	 * Object format: { path: string, line_ranges: [{ start: 1, end: 50 }] }
	 * Legacy string format: { path: string, line_ranges: ["1-50"] }
	 *
	 * Returns: { path: string, lineRanges: [{ start: 1, end: 50 }] }
	 */
	private static convertFileEntries(files: unknown[] = []): FileEntry[] {
		return files?.map((file: unknown) => {
			const f = file as Record<string, unknown>
			const entry: FileEntry = { path: f.path as string }
			if (f.line_ranges && Array.isArray(f.line_ranges)) {
				entry.lineRanges = (f.line_ranges as unknown[])
					.map((range: unknown) => {
						// Handle tuple format: [start, end]
						if (Array.isArray(range) && range.length >= 2) {
							return { start: Number(range[0]), end: Number(range[1]) }
						}
						// Handle object format: { start: number, end: number }
						if (typeof range === "object" && range !== null && "start" in range && "end" in range) {
							const r = range as { start: unknown; end: unknown }
							return { start: Number(r.start), end: Number(r.end) }
						}
						// Handle legacy string format: "1-50"
						if (typeof range === "string") {
							const match = range.match(/^(\d+)-(\d+)$/)
							if (match) {
								return { start: parseInt(match[1], 10), end: parseInt(match[2], 10) }
							}
						}
						return null
					})
					.filter((r): r is { start: number; end: number } => r !== null)
			}
			return entry
		})
	}

	/**
	 * Create a partial ToolUse from currently parsed arguments.
	 * Used during streaming to show progress.
	 * @param originalName - The original tool name as called by the model (if different from canonical name)
	 */
	private static createPartialToolUse(
		id: string,
		name: ToolName,
		partialArgs: Record<string, any>,
		partial: boolean,
		originalName?: string,
	): ToolUse | null {
		// Build stringified params for display/partial-progress UI.
		// NOTE: For streaming partial updates, we MUST populate params even for complex types
		// because tool.handlePartial() methods rely on params to show UI updates.
		const params: Partial<Record<ToolParamName, string>> = {}

		for (const [key, value] of Object.entries(partialArgs)) {
			if (toolParamNames.includes(key as ToolParamName)) {
				params[key as ToolParamName] = typeof value === "string" ? value : JSON.stringify(value)
			}
		}

		// Build partial nativeArgs based on what we have so far
		let nativeArgs: any = undefined

		// Track if legacy format was used (for telemetry)
		let usedLegacyFormat = false

		switch (name) {
			case "fake_tool_call": {
				// fake_tool_call is a virtual tool for compatibility with models that don't support native function calls
				// It doesn't need actual nativeArgs because it's just a placeholder
				// Actual tool calls are handled in Task.ts by parsing <tool_call> tags
				break
			}
			case "read_file":
				// Check for legacy format first: { files: [...] }
				// Handle both array and stringified array (some models double-stringify)
				if (partialArgs.files !== undefined) {
					let filesArray: unknown[] | null = null

					if (Array.isArray(partialArgs.files)) {
						filesArray = partialArgs.files
					} else if (typeof partialArgs.files === "string") {
						// Handle double-stringified case: files is a string containing JSON array
						try {
							const parsed = JSON.parse(partialArgs.files)
							if (Array.isArray(parsed)) {
								filesArray = parsed
							}
						} catch {
							// Not valid JSON, ignore
						}
					}

					if (filesArray && filesArray.length > 0) {
						usedLegacyFormat = true
						nativeArgs = {
							files: this.convertFileEntries(filesArray),
							_legacyFormat: true as const,
						}
					}
				}
				// New format: { path: "...", mode: "..." }
				if (!nativeArgs && partialArgs.path !== undefined) {
					nativeArgs = {
						path: partialArgs.path,
						mode: partialArgs.mode,
						offset: this.coerceOptionalNumber(partialArgs.offset),
						limit: this.coerceOptionalNumber(partialArgs.limit),
						indentation:
							partialArgs.indentation && typeof partialArgs.indentation === "object"
								? {
										anchor_line: this.coerceOptionalNumber(partialArgs.indentation.anchor_line),
										max_levels: this.coerceOptionalNumber(partialArgs.indentation.max_levels),
										max_lines: this.coerceOptionalNumber(partialArgs.indentation.max_lines),
										include_siblings: this.coerceOptionalBoolean(
											partialArgs.indentation.include_siblings,
										),
										include_header: this.coerceOptionalBoolean(
											partialArgs.indentation.include_header,
										),
									}
								: undefined,
					}
				}
				break

			case "attempt_completion":
				if (partialArgs.result) {
					nativeArgs = { result: partialArgs.result }
				}
				break

			case "execute_command":
				if (partialArgs.command) {
					nativeArgs = {
						command: partialArgs.command,
						cwd: partialArgs.cwd,
						timeout: partialArgs.timeout,
					}
				}
				break

			case "write_to_file":
				if (partialArgs.path || partialArgs.content) {
					nativeArgs = {
						path: partialArgs.path,
						content: partialArgs.content,
					}
				}
				break

			case "ask_followup_question":
				if (partialArgs.question !== undefined || partialArgs.follow_up !== undefined) {
					nativeArgs = {
						question: partialArgs.question,
						follow_up: Array.isArray(partialArgs.follow_up) ? partialArgs.follow_up : undefined,
					}
				}
				break

			case "ask_multiple_choice":
				if (partialArgs.questions !== undefined) {
					nativeArgs = {
						title: partialArgs.title,
						questions: Array.isArray(partialArgs.questions) ? partialArgs.questions : undefined,
					}
				}
				break

			case "costrict_checkpoint":
				if (partialArgs.action !== undefined) {
					nativeArgs = {
						action: partialArgs.action,
						message: partialArgs.message,
						commit_hash: partialArgs.commit_hash,
						files: partialArgs.files,
					}
				}
				break

			case "apply_diff":
				if (partialArgs.path !== undefined || partialArgs.diff !== undefined) {
					nativeArgs = {
						path: partialArgs.path,
						diff: partialArgs.diff,
					}
				}
				break

			case "codebase_search":
				if (partialArgs.query !== undefined) {
					nativeArgs = {
						query: partialArgs.query,
						path: partialArgs.path,
					}
				}
				break

			case "generate_image":
				if (partialArgs.prompt !== undefined || partialArgs.path !== undefined) {
					nativeArgs = {
						prompt: partialArgs.prompt,
						path: partialArgs.path,
						image: partialArgs.image,
					}
				}
				break

			case "run_slash_command":
				if (partialArgs.command !== undefined) {
					nativeArgs = {
						command: partialArgs.command,
						args: partialArgs.args,
					}
				}
				break

			case "skill":
				if (partialArgs.skill !== undefined) {
					nativeArgs = {
						skill: partialArgs.skill,
						args: partialArgs.args,
					}
				}
				break

			case "search_files":
				if (partialArgs.path !== undefined || partialArgs.regex !== undefined) {
					nativeArgs = {
						path: partialArgs.path,
						regex: partialArgs.regex,
						file_pattern: partialArgs.file_pattern,
					}
				}
				break

			case "switch_mode":
				if (partialArgs.mode_slug !== undefined || partialArgs.reason !== undefined) {
					nativeArgs = {
						mode_slug: partialArgs.mode_slug,
						reason: partialArgs.reason,
					}
				}
				break

			case "update_todo_list":
				if (partialArgs.todos !== undefined) {
					nativeArgs = {
						todos: partialArgs.todos,
					}
				}
				break

			case "use_mcp_tool":
				if (partialArgs.server_name !== undefined || partialArgs.tool_name !== undefined) {
					nativeArgs = {
						server_name: partialArgs.server_name,
						tool_name: partialArgs.tool_name,
						arguments: partialArgs.arguments,
					}
				}
				break

			case "apply_patch":
				if (partialArgs.patch !== undefined) {
					nativeArgs = {
						patch: partialArgs.patch,
					}
				}
				break

			case "search_replace":
				if (
					partialArgs.file_path !== undefined ||
					partialArgs.old_string !== undefined ||
					partialArgs.new_string !== undefined
				) {
					nativeArgs = {
						file_path: partialArgs.file_path,
						old_string: partialArgs.old_string,
						new_string: partialArgs.new_string,
					}
				}
				break

			case "edit":
			case "search_and_replace":
				if (
					partialArgs.file_path !== undefined ||
					partialArgs.old_string !== undefined ||
					partialArgs.new_string !== undefined
				) {
					nativeArgs = {
						file_path: partialArgs.file_path,
						old_string: partialArgs.old_string,
						new_string: partialArgs.new_string,
						replace_all: this.coerceOptionalBoolean(partialArgs.replace_all),
					}
				}
				break

			case "edit_file":
				if (
					partialArgs.file_path !== undefined ||
					partialArgs.old_string !== undefined ||
					partialArgs.new_string !== undefined
				) {
					nativeArgs = {
						file_path: partialArgs.file_path,
						old_string: partialArgs.old_string,
						new_string: partialArgs.new_string,
						expected_replacements: partialArgs.expected_replacements,
					}
				}
				break

			case "list_files":
				if (partialArgs.path !== undefined) {
					nativeArgs = {
						path: partialArgs.path,
						recursive: this.coerceOptionalBoolean(partialArgs.recursive),
					}
				}
				break

			case "new_task":
				if (partialArgs.mode !== undefined || partialArgs.message !== undefined) {
					nativeArgs = {
						mode: partialArgs.mode,
						message: partialArgs.message,
						todos: partialArgs.todos,
					}
				}
				break

			case "file_outline":
				if (partialArgs.file_path !== undefined) {
					nativeArgs = {
						file_path: partialArgs.file_path,
						include_docstrings: this.coerceOptionalBoolean(partialArgs.include_docstrings),
					}
				}
				break

			case "sequential_thinking":
				if (partialArgs.thought !== undefined || partialArgs.nextThoughtNeeded !== undefined) {
					nativeArgs = {
						thought: partialArgs.thought,
						nextThoughtNeeded: partialArgs.nextThoughtNeeded,
						thoughtNumber: partialArgs.thoughtNumber,
						totalThoughts: partialArgs.totalThoughts,
						isRevision: this.coerceOptionalBoolean(partialArgs.isRevision),
						needsMoreThoughts: this.coerceOptionalBoolean(partialArgs.needsMoreThoughts),
						branchFromThought: this.coerceOptionalNumber(partialArgs.branchFromThought),
						revisesThought: this.coerceOptionalNumber(partialArgs.revisesThought),
						branchId: partialArgs.branchId,
					}
				}
				break

			default:
				break
		}

		const result: ToolUse = {
			type: "tool_use" as const,
			id, // Set the tool call ID required by the validation in presentAssistantMessage.ts
			name,
			params,
			partial,
			nativeArgs,
		}

		// Preserve original name for API history when an alias was used
		if (originalName) {
			result.originalName = originalName
		}

		// Track legacy format usage for telemetry
		if (usedLegacyFormat) {
			result.usedLegacyFormat = true
		}

		return result
	}

	/**
	 * Convert a native tool call chunk to a ToolUse object.
	 *
	 * @param toolCall - The native tool call from the API stream
	 * @returns A properly typed ToolUse object
	 */
	public static parseToolCall<TName extends ToolName>(
		toolCall: {
			id: string
			name: TName
			arguments: string
		},
		isCostrict?: boolean,
	): ToolUse<TName> | McpToolUse | null {
		// Check if this is a dynamic MCP tool (mcp--serverName--toolName)
		// Also handle models that output underscores instead of hyphens (mcp__serverName__toolName)
		const mcpPrefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR

		if (typeof toolCall.name === "string") {
			// Normalize the tool name to handle models that output underscores instead of hyphens
			const normalizedName = normalizeMcpToolName(toolCall.name)
			if (normalizedName.startsWith(mcpPrefix)) {
				// Pass the original tool call but with normalized name for parsing
				return this.parseDynamicMcpTool({ ...toolCall, name: normalizedName })
			}
		}

		// Resolve tool alias to canonical name
		let resolvedName = resolveToolAlias(toolCall.name as string) as TName

		// Validate tool name (after alias resolution).
		const matchBuiltinToolName = (toolNames.find(
			(name) => name === resolvedName || resolvedName.indexOf(name) > -1,
		) ?? "") as TName
		const matchCustomToolName = (customToolRegistry
			.list()
			.find((name) => name === resolvedName || resolvedName.indexOf(name) > -1) ?? "") as TName

		const _resolvedName = matchBuiltinToolName || matchCustomToolName

		if (!_resolvedName) {
			console.error(
				`Invalid tool name: ${toolCall.name} (resolved: ${resolvedName}) | toolCall arguments: ${toolCall.arguments}`,
			)
			console.error(`Valid tool names:`, toolNames)
			return null
		} else {
			if (toolCall.name !== _resolvedName) {
				console.warn(`Resolved tool alias '${toolCall.name}' to '${_resolvedName}'`)
			}
			resolvedName = _resolvedName
		}

		try {
			// Parse the arguments JSON string
			const args = toolCall.arguments === "" ? {} : parseJSON(toolCall.arguments)

			// Normalize values to handle type mismatches from LLM
			// (e.g. stringified objects/arrays: '"[1,2,3]"' -> [1,2,3])
			const normalizedArgs: Record<string, any> = {}
			for (const [key, value] of Object.entries(args)) {
				let _key = key
				if (isCostrict && _key.includes("<arg_key>")) {
					_key = (key.split("<arg_key>").pop() as string) ?? _key
					console.log(`${toolCall.name}|${toolCall.id}: ${key} -> ${_key}`)
				}
				normalizedArgs[_key] = this.normalizeTypeValue(value)
			}

			// Build stringified params for display/logging.
			// Tool execution MUST use nativeArgs (typed) and does not support legacy fallbacks.
			const params: Partial<Record<ToolParamName, string>> = {}

			for (const [key, value] of Object.entries(args)) {
				// Validate parameter name
				if (!toolParamNames.includes(key as ToolParamName) && !customToolRegistry.has(resolvedName)) {
					console.warn(`Unknown parameter '${key}' for tool '${resolvedName}'`)
					console.warn(`Valid param names:`, toolParamNames)
					continue
				}

				// Convert to string for legacy params format
				const stringValue = typeof value === "string" ? value : JSON.stringify(value)
				params[key as ToolParamName] = stringValue
			}

			// Build typed nativeArgs for tool execution.
			// Each case validates the minimum required parameters and constructs a properly typed
			// nativeArgs object. If validation fails, we treat the tool call as invalid and fail fast.
			let nativeArgs: NativeArgsFor<TName> | undefined = undefined

			// Track if legacy format was used (for telemetry)
			let usedLegacyFormat = false

			switch (resolvedName) {
				case "read_file":
					// Check for legacy format first: { files: [...] }
					// Handle both array and stringified array (some models double-stringify)
					if (args.files !== undefined) {
						let filesArray: unknown[] | null = null

						if (Array.isArray(args.files)) {
							filesArray = args.files
						} else if (typeof args.files === "string") {
							// Handle double-stringified case: files is a string containing JSON array
							try {
								const parsed = JSON.parse(args.files)
								if (Array.isArray(parsed)) {
									filesArray = parsed
								}
							} catch {
								// Not valid JSON, ignore
							}
						}

						if (filesArray && filesArray.length > 0) {
							usedLegacyFormat = true
							nativeArgs = {
								files: this.convertFileEntries(filesArray),
								_legacyFormat: true as const,
							} as NativeArgsFor<TName>
						}
					}
					// New format: { path: "...", mode: "..." }
					if (!nativeArgs && args.path !== undefined) {
						nativeArgs = {
							path: args.path,
							mode: args.mode,
							offset: this.coerceOptionalNumber(args.offset),
							limit: this.coerceOptionalNumber(args.limit),
							indentation:
								args.indentation && typeof args.indentation === "object"
									? {
											anchor_line: this.coerceOptionalNumber(args.indentation.anchor_line),
											max_levels: this.coerceOptionalNumber(args.indentation.max_levels),
											max_lines: this.coerceOptionalNumber(args.indentation.max_lines),
											include_siblings: this.coerceOptionalBoolean(
												args.indentation.include_siblings,
											),
											include_header: this.coerceOptionalBoolean(args.indentation.include_header),
										}
									: undefined,
						} as NativeArgsFor<TName>
					}
					break

				case "attempt_completion":
					if (normalizedArgs.result) {
						nativeArgs = { result: normalizedArgs.result } as NativeArgsFor<TName>
					}
					break

				case "execute_command":
					if (normalizedArgs.command) {
						nativeArgs = {
							command: normalizedArgs.command,
							cwd: normalizedArgs.cwd,
							timeout: normalizedArgs.timeout,
						} as NativeArgsFor<TName>
					}
					break

				case "apply_diff":
					if (normalizedArgs.path !== undefined && normalizedArgs.diff !== undefined) {
						nativeArgs = {
							path: normalizedArgs.path,
							diff: normalizedArgs.diff,
						} as NativeArgsFor<TName>
					}
					break

				case "edit":
				case "search_and_replace":
					if (
						normalizedArgs.file_path !== undefined &&
						normalizedArgs.old_string !== undefined &&
						normalizedArgs.new_string !== undefined
					) {
						nativeArgs = {
							file_path: normalizedArgs.file_path,
							old_string: normalizedArgs.old_string,
							new_string: normalizedArgs.new_string,
							replace_all: this.coerceOptionalBoolean(normalizedArgs.replace_all),
						} as NativeArgsFor<TName>
					}
					break

				case "ask_followup_question":
					if (normalizedArgs.question !== undefined && normalizedArgs.follow_up !== undefined) {
						nativeArgs = {
							question: normalizedArgs.question,
							follow_up: normalizedArgs.follow_up,
						} as NativeArgsFor<TName>
					}
					break
				case "ask_multiple_choice":
					if (
						normalizedArgs.questions !== undefined &&
						Array.isArray(normalizedArgs.questions) &&
						normalizedArgs.questions.length > 0 &&
						normalizedArgs.questions.filter((q) => Object.keys(q).length > 0).length > 0
					) {
						nativeArgs = {
							title: normalizedArgs.title,
							questions: normalizedArgs.questions,
						} as NativeArgsFor<TName>
					}
					break

				case "costrict_checkpoint":
					nativeArgs = {
						action: normalizedArgs.action || "list",
						message: normalizedArgs.message,
						commit_hash: normalizedArgs.commit_hash,
						files: normalizedArgs.files,
					} as NativeArgsFor<TName>
					break

				case "codebase_search":
					if (normalizedArgs.query !== undefined) {
						nativeArgs = {
							query: normalizedArgs.query,
							path: normalizedArgs.path,
						} as NativeArgsFor<TName>
					}
					break

				case "generate_image":
					if (normalizedArgs.prompt !== undefined && normalizedArgs.path !== undefined) {
						nativeArgs = {
							prompt: normalizedArgs.prompt,
							path: normalizedArgs.path,
							image: normalizedArgs.image,
						} as NativeArgsFor<TName>
					}
					break

				case "run_slash_command":
					if (normalizedArgs.command !== undefined) {
						nativeArgs = {
							command: normalizedArgs.command,
							args: normalizedArgs.args,
						} as NativeArgsFor<TName>
					}
					break

				case "skill":
					if (args.skill !== undefined) {
						nativeArgs = {
							skill: args.skill,
							args: args.args,
						} as NativeArgsFor<TName>
					}
					break

				case "search_files":
					if (normalizedArgs.path !== undefined && normalizedArgs.regex !== undefined) {
						nativeArgs = {
							path: normalizedArgs.path,
							regex: normalizedArgs.regex,
							file_pattern: normalizedArgs.file_pattern,
						} as NativeArgsFor<TName>
					}
					break

				case "switch_mode":
					if (normalizedArgs.mode_slug !== undefined && normalizedArgs.reason !== undefined) {
						nativeArgs = {
							mode_slug: normalizedArgs.mode_slug,
							reason: normalizedArgs.reason,
						} as NativeArgsFor<TName>
					}
					break

				case "update_todo_list":
					if (normalizedArgs.todos !== undefined) {
						nativeArgs = {
							todos: normalizedArgs.todos,
						} as NativeArgsFor<TName>
					}
					break

				case "read_command_output":
					if (args.artifact_id !== undefined) {
						nativeArgs = {
							artifact_id: args.artifact_id,
							search: args.search,
							offset: args.offset,
							limit: args.limit,
						} as NativeArgsFor<TName>
					}
					break

				case "write_to_file":
					if (normalizedArgs.path !== undefined && normalizedArgs.content !== undefined) {
						nativeArgs = {
							path: normalizedArgs.path,
							content: normalizedArgs.content,
						} as NativeArgsFor<TName>
					}
					break

				case "use_mcp_tool":
					if (normalizedArgs.server_name !== undefined && normalizedArgs.tool_name !== undefined) {
						nativeArgs = {
							server_name: normalizedArgs.server_name,
							tool_name: normalizedArgs.tool_name,
							arguments: normalizedArgs.arguments,
						} as NativeArgsFor<TName>
					}
					break

				case "access_mcp_resource":
					if (normalizedArgs.server_name !== undefined && normalizedArgs.uri !== undefined) {
						nativeArgs = {
							server_name: normalizedArgs.server_name,
							uri: normalizedArgs.uri,
						} as NativeArgsFor<TName>
					}
					break

				case "apply_patch":
					if (normalizedArgs.patch !== undefined) {
						nativeArgs = {
							patch: normalizedArgs.patch,
						} as NativeArgsFor<TName>
					}
					break

				case "search_replace":
					if (
						normalizedArgs.file_path !== undefined &&
						normalizedArgs.old_string !== undefined &&
						normalizedArgs.new_string !== undefined
					) {
						nativeArgs = {
							file_path: normalizedArgs.file_path,
							old_string: normalizedArgs.old_string,
							new_string: normalizedArgs.new_string,
						} as NativeArgsFor<TName>
					}
					break

				case "edit_file":
					if (
						normalizedArgs.file_path !== undefined &&
						normalizedArgs.old_string !== undefined &&
						normalizedArgs.new_string !== undefined
					) {
						nativeArgs = {
							file_path: normalizedArgs.file_path,
							old_string: normalizedArgs.old_string,
							new_string: normalizedArgs.new_string,
							expected_replacements: normalizedArgs.expected_replacements,
						} as NativeArgsFor<TName>
					}
					break

				case "list_files":
					if (normalizedArgs.path !== undefined) {
						nativeArgs = {
							path: normalizedArgs.path,
							recursive: this.coerceOptionalBoolean(normalizedArgs.recursive),
						} as NativeArgsFor<TName>
					}
					break

				case "new_task":
					if (normalizedArgs.message !== undefined) {
						nativeArgs = {
							mode: normalizedArgs.mode ?? defaultModeSlug,
							message: normalizedArgs.message,
							todos: normalizedArgs.todos,
						} as NativeArgsFor<TName>
					}
					break

				default:
					if (customToolRegistry.has(resolvedName)) {
						nativeArgs = normalizedArgs as NativeArgsFor<TName>
					}

					break
			}

			// Native-only: core tools must always have typed nativeArgs.
			// If we couldn't construct it, the model produced an invalid tool call payload.
			if (!nativeArgs && !customToolRegistry.has(resolvedName)) {
				throw new Error(
					`[NativeToolCallParser] Invalid arguments for tool '${resolvedName}'. ` +
						`Native tool calls require a valid JSON payload matching the tool schema. ` +
						`Received: ${JSON.stringify(args)}`,
				)
			}

			const result: ToolUse<TName> = {
				type: "tool_use" as const,
				id: toolCall.id, // Set the tool call ID required by the validation in presentAssistantMessage.ts
				name: resolvedName,
				params,
				partial: false, // Native tool calls are always complete when yielded
				nativeArgs,
			}

			// Preserve original name for API history when an alias was used
			if (toolCall.name !== resolvedName) {
				result.originalName = toolCall.name
			}

			// Track legacy format usage for telemetry
			if (usedLegacyFormat) {
				result.usedLegacyFormat = true
			}

			return result
		} catch (error) {
			console.error(
				`Failed to parse tool call arguments: ${error instanceof Error ? error.message : String(error)}`,
			)

			console.error(`Tool call: ${JSON.stringify(toolCall, null, 2)}`)
			return null
		}
	}

	/**
	 * Parse dynamic MCP tools (named mcp--serverName--toolName).
	 * These are generated dynamically by getMcpServerTools() and are returned
	 * as McpToolUse objects that preserve the original tool name.
	 */
	public static parseDynamicMcpTool(toolCall: { id: string; name: string; arguments: string }): McpToolUse | null {
		try {
			// Parse the arguments - these are the actual tool arguments passed directly
			const args = JSON.parse(toolCall.arguments || "{}")

			// Normalize the tool name to handle models that output underscores instead of hyphens
			// e.g., mcp__serverName__toolName -> mcp--serverName--toolName
			const normalizedName = normalizeMcpToolName(toolCall.name)

			// Extract server_name and tool_name from the tool name itself
			// Format: mcp--serverName--toolName (using -- separator)
			const parsed = parseMcpToolName(normalizedName)
			if (!parsed) {
				console.error(`Invalid dynamic MCP tool name format: ${toolCall.name} (normalized: ${normalizedName})`)
				return null
			}

			const { serverName, toolName } = parsed

			const result: McpToolUse = {
				type: "mcp_tool_use" as const,
				id: toolCall.id,
				// Keep the original tool name (e.g., "mcp--serverName--toolName") for API history
				name: toolCall.name,
				serverName,
				toolName,
				arguments: args,
				partial: false,
			}

			return result
		} catch (error) {
			console.error(`Failed to parse dynamic MCP tool:`, error)
			return null
		}
	}
}
