/**
 * @license
 * Copyright 2025 Qwen
 * SPDX-License-Identifier: Apache-2.0
 */

import { reportError } from '../utils/errorReporting.js';
import { Config } from '../config/config.js';
import { ToolCallRequestInfo } from '../core/turn.js';
import { executeToolCall } from '../core/nonInteractiveToolExecutor.js';
import { createContentGenerator } from '../core/contentGenerator.js';
import { getEnvironmentContext } from '../utils/environmentContext.js';
import {
  Content,
  Part,
  FunctionCall,
  GenerateContentConfig,
  FunctionDeclaration,
  GenerateContentResponseUsageMetadata,
} from '@google/genai';
import { GeminiChat } from '../core/geminiChat.js';
import {
  SubagentTerminateMode,
  PromptConfig,
  ModelConfig,
  RunConfig,
  ToolConfig,
} from './types.js';
import {
  SubAgentEventEmitter,
  SubAgentEventType,
  SubAgentFinishEvent,
  SubAgentRoundEvent,
  SubAgentStartEvent,
  SubAgentToolCallEvent,
  SubAgentToolResultEvent,
  SubAgentStreamTextEvent,
  SubAgentErrorEvent,
} from './subagent-events.js';
import {
  SubagentStatistics,
  SubagentStatsSummary,
} from './subagent-statistics.js';
import { SubagentHooks } from './subagent-hooks.js';
import { logSubagentExecution } from '../telemetry/loggers.js';
import { SubagentExecutionEvent } from '../telemetry/types.js';

/**
 * @fileoverview Defines the configuration interfaces for a subagent.
 *
 * These interfaces specify the structure for defining the subagent's prompt,
 * the model parameters, and the execution settings.
 */

interface ExecutionStats {
  startTimeMs: number;
  totalDurationMs: number;
  rounds: number;
  totalToolCalls: number;
  successfulToolCalls: number;
  failedToolCalls: number;
  inputTokens?: number;
  outputTokens?: number;
  totalTokens?: number;
  estimatedCost?: number;
}

/**
 * Manages the runtime context state for the subagent.
 * This class provides a mechanism to store and retrieve key-value pairs
 * that represent the dynamic state and variables accessible to the subagent
 * during its execution.
 */
export class ContextState {
  private state: Record<string, unknown> = {};

  /**
   * Retrieves a value from the context state.
   *
   * @param key - The key of the value to retrieve.
   * @returns The value associated with the key, or undefined if the key is not found.
   */
  get(key: string): unknown {
    return this.state[key];
  }

  /**
   * Sets a value in the context state.
   *
   * @param key - The key to set the value under.
   * @param value - The value to set.
   */
  set(key: string, value: unknown): void {
    this.state[key] = value;
  }

  /**
   * Retrieves all keys in the context state.
   *
   * @returns An array of all keys in the context state.
   */
  get_keys(): string[] {
    return Object.keys(this.state);
  }
}

/**
 * Replaces `${...}` placeholders in a template string with values from a context.
 *
 * This function identifies all placeholders in the format `${key}`, validates that
 * each key exists in the provided `ContextState`, and then performs the substitution.
 *
 * @param template The template string containing placeholders.
 * @param context The `ContextState` object providing placeholder values.
 * @returns The populated string with all placeholders replaced.
 * @throws {Error} if any placeholder key is not found in the context.
 */
function templateString(template: string, context: ContextState): string {
  const placeholderRegex = /\$\{(\w+)\}/g;

  // First, find all unique keys required by the template.
  const requiredKeys = new Set(
    Array.from(template.matchAll(placeholderRegex), (match) => match[1]),
  );

  // Check if all required keys exist in the context.
  const contextKeys = new Set(context.get_keys());
  const missingKeys = Array.from(requiredKeys).filter(
    (key) => !contextKeys.has(key),
  );

  if (missingKeys.length > 0) {
    throw new Error(
      `Missing context values for the following keys: ${missingKeys.join(
        ', ',
      )}`,
    );
  }

  // Perform the replacement using a replacer function.
  return template.replace(placeholderRegex, (_match, key) =>
    String(context.get(key)),
  );
}

/**
 * Represents the scope and execution environment for a subagent.
 * This class orchestrates the subagent's lifecycle, managing its chat interactions,
 * runtime context, and the collection of its outputs.
 */
export class SubAgentScope {
  executionStats: ExecutionStats = {
    startTimeMs: 0,
    totalDurationMs: 0,
    rounds: 0,
    totalToolCalls: 0,
    successfulToolCalls: 0,
    failedToolCalls: 0,
    inputTokens: 0,
    outputTokens: 0,
    totalTokens: 0,
    estimatedCost: 0,
  };
  private toolUsage = new Map<
    string,
    {
      count: number;
      success: number;
      failure: number;
      lastError?: string;
      totalDurationMs?: number;
      averageDurationMs?: number;
    }
  >();
  private eventEmitter?: SubAgentEventEmitter;
  private finalText: string = '';
  private terminateMode: SubagentTerminateMode = SubagentTerminateMode.ERROR;
  private readonly stats = new SubagentStatistics();
  private hooks?: SubagentHooks;
  private readonly subagentId: string;

  /**
   * Constructs a new SubAgentScope instance.
   * @param name - The name for the subagent, used for logging and identification.
   * @param runtimeContext - The shared runtime configuration and services.
   * @param promptConfig - Configuration for the subagent's prompt and behavior.
   * @param modelConfig - Configuration for the generative model parameters.
   * @param runConfig - Configuration for the subagent's execution environment.
   * @param toolConfig - Optional configuration for tools available to the subagent.
   */
  private constructor(
    readonly name: string,
    readonly runtimeContext: Config,
    private readonly promptConfig: PromptConfig,
    private readonly modelConfig: ModelConfig,
    private readonly runConfig: RunConfig,
    private readonly toolConfig?: ToolConfig,
    eventEmitter?: SubAgentEventEmitter,
    hooks?: SubagentHooks,
  ) {
    const randomPart = Math.random().toString(36).slice(2, 8);
    this.subagentId = `${this.name}-${randomPart}`;
    this.eventEmitter = eventEmitter;
    this.hooks = hooks;
  }

  /**
   * Creates and validates a new SubAgentScope instance.
   * This factory method ensures that all tools provided in the prompt configuration
   * are valid for non-interactive use before creating the subagent instance.
   * @param {string} name - The name of the subagent.
   * @param {Config} runtimeContext - The shared runtime configuration and services.
   * @param {PromptConfig} promptConfig - Configuration for the subagent's prompt and behavior.
   * @param {ModelConfig} modelConfig - Configuration for the generative model parameters.
   * @param {RunConfig} runConfig - Configuration for the subagent's execution environment.
   * @param {ToolConfig} [toolConfig] - Optional configuration for tools.
   * @returns {Promise<SubAgentScope>} A promise that resolves to a valid SubAgentScope instance.
   * @throws {Error} If any tool requires user confirmation.
   */
  static async create(
    name: string,
    runtimeContext: Config,
    promptConfig: PromptConfig,
    modelConfig: ModelConfig,
    runConfig: RunConfig,
    toolConfig?: ToolConfig,
    eventEmitter?: SubAgentEventEmitter,
    hooks?: SubagentHooks,
  ): Promise<SubAgentScope> {
    // Validate tools for non-interactive use
    if (toolConfig?.tools) {
      const toolRegistry = runtimeContext.getToolRegistry();

      for (const toolItem of toolConfig.tools) {
        if (typeof toolItem !== 'string') {
          continue; // Skip inline function declarations
        }
        const tool = toolRegistry.getTool(toolItem);
        if (!tool) {
          continue; // Skip unknown tools
        }

        // Check if tool has required parameters
        const hasRequiredParams =
          tool.schema?.parameters?.required &&
          Array.isArray(tool.schema.parameters.required) &&
          tool.schema.parameters.required.length > 0;

        if (hasRequiredParams) {
          // Can't check interactivity without parameters, log warning and continue
          console.warn(
            `Cannot check tool "${toolItem}" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.`,
          );
          continue;
        }

        // Try to build the tool to check if it requires confirmation
        try {
          const toolInstance = tool.build({});
          const confirmationDetails = await toolInstance.shouldConfirmExecute(
            new AbortController().signal,
          );

          if (confirmationDetails) {
            throw new Error(
              `Tool "${toolItem}" requires user confirmation and cannot be used in a non-interactive subagent.`,
            );
          }
        } catch (error) {
          // If we can't build the tool, assume it's safe
          if (
            error instanceof Error &&
            error.message.includes('requires user confirmation')
          ) {
            throw error; // Re-throw confirmation errors
          }
          // For other build errors, log warning and continue
          console.warn(
            `Cannot check tool "${toolItem}" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.`,
          );
        }
      }
    }

    return new SubAgentScope(
      name,
      runtimeContext,
      promptConfig,
      modelConfig,
      runConfig,
      toolConfig,
      eventEmitter,
      hooks,
    );
  }

  /**
   * Runs the subagent in a non-interactive mode.
   * This method orchestrates the subagent's execution loop, including prompt templating,
   * tool execution, and termination conditions.
   * @param {ContextState} context - The current context state containing variables for prompt templating.
   * @returns {Promise<void>} A promise that resolves when the subagent has completed its execution.
   */
  async runNonInteractive(
    context: ContextState,
    externalSignal?: AbortSignal,
  ): Promise<void> {
    const chat = await this.createChatObject(context);

    if (!chat) {
      this.terminateMode = SubagentTerminateMode.ERROR;
      return;
    }

    const abortController = new AbortController();
    const onAbort = () => abortController.abort();
    if (externalSignal) {
      if (externalSignal.aborted) {
        abortController.abort();
        this.terminateMode = SubagentTerminateMode.CANCELLED;
        return;
      }
      externalSignal.addEventListener('abort', onAbort, { once: true });
    }
    const toolRegistry = this.runtimeContext.getToolRegistry();

    // Prepare the list of tools available to the subagent.
    // If no explicit toolConfig or it contains "*" or is empty, inherit all tools.
    const toolsList: FunctionDeclaration[] = [];
    if (this.toolConfig) {
      const asStrings = this.toolConfig.tools.filter(
        (t): t is string => typeof t === 'string',
      );
      const hasWildcard = asStrings.includes('*');
      const onlyInlineDecls = this.toolConfig.tools.filter(
        (t): t is FunctionDeclaration => typeof t !== 'string',
      );

      if (hasWildcard || asStrings.length === 0) {
        toolsList.push(...toolRegistry.getFunctionDeclarations());
      } else {
        toolsList.push(
          ...toolRegistry.getFunctionDeclarationsFiltered(asStrings),
        );
      }
      toolsList.push(...onlyInlineDecls);
    } else {
      // Inherit all available tools by default when not specified.
      toolsList.push(...toolRegistry.getFunctionDeclarations());
    }

    const initialTaskText = String(
      (context.get('task_prompt') as string) ?? 'Get Started!',
    );
    let currentMessages: Content[] = [
      { role: 'user', parts: [{ text: initialTaskText }] },
    ];

    const startTime = Date.now();
    this.executionStats.startTimeMs = startTime;
    this.stats.start(startTime);
    let turnCounter = 0;
    try {
      // Emit start event
      this.eventEmitter?.emit(SubAgentEventType.START, {
        subagentId: this.subagentId,
        name: this.name,
        model: this.modelConfig.model,
        tools: (this.toolConfig?.tools || ['*']).map((t) =>
          typeof t === 'string' ? t : t.name,
        ),
        timestamp: Date.now(),
      } as SubAgentStartEvent);

      // Log telemetry for subagent start
      const startEvent = new SubagentExecutionEvent(this.name, 'started');
      logSubagentExecution(this.runtimeContext, startEvent);
      while (true) {
        // Check termination conditions.
        if (
          this.runConfig.max_turns &&
          turnCounter >= this.runConfig.max_turns
        ) {
          this.terminateMode = SubagentTerminateMode.MAX_TURNS;
          break;
        }
        let durationMin = (Date.now() - startTime) / (1000 * 60);
        if (
          this.runConfig.max_time_minutes &&
          durationMin >= this.runConfig.max_time_minutes
        ) {
          this.terminateMode = SubagentTerminateMode.TIMEOUT;
          break;
        }

        const promptId = `${this.runtimeContext.getSessionId()}#${this.subagentId}#${turnCounter++}`;
        const messageParams = {
          message: currentMessages[0]?.parts || [],
          config: {
            abortSignal: abortController.signal,
            tools: [{ functionDeclarations: toolsList }],
          },
        };

        const responseStream = await chat.sendMessageStream(
          messageParams,
          promptId,
        );
        this.eventEmitter?.emit(SubAgentEventType.ROUND_START, {
          subagentId: this.subagentId,
          round: turnCounter,
          promptId,
          timestamp: Date.now(),
        } as SubAgentRoundEvent);

        const functionCalls: FunctionCall[] = [];
        let roundText = '';
        let lastUsage: GenerateContentResponseUsageMetadata | undefined =
          undefined;
        for await (const resp of responseStream) {
          if (abortController.signal.aborted) {
            this.terminateMode = SubagentTerminateMode.CANCELLED;
            return;
          }
          if (resp.functionCalls) functionCalls.push(...resp.functionCalls);
          const content = resp.candidates?.[0]?.content;
          const parts = content?.parts || [];
          for (const p of parts) {
            const txt = (p as Part & { text?: string }).text;
            if (txt) roundText += txt;
            if (txt)
              this.eventEmitter?.emit(SubAgentEventType.STREAM_TEXT, {
                subagentId: this.subagentId,
                round: turnCounter,
                text: txt,
                timestamp: Date.now(),
              } as SubAgentStreamTextEvent);
          }
          if (resp.usageMetadata) lastUsage = resp.usageMetadata;
        }
        this.executionStats.rounds = turnCounter;
        this.stats.setRounds(turnCounter);

        durationMin = (Date.now() - startTime) / (1000 * 60);
        if (
          this.runConfig.max_time_minutes &&
          durationMin >= this.runConfig.max_time_minutes
        ) {
          this.terminateMode = SubagentTerminateMode.TIMEOUT;
          break;
        }

        // Update token usage if available
        if (lastUsage) {
          const inTok = Number(lastUsage.promptTokenCount || 0);
          const outTok = Number(lastUsage.candidatesTokenCount || 0);
          if (isFinite(inTok) || isFinite(outTok)) {
            this.stats.recordTokens(
              isFinite(inTok) ? inTok : 0,
              isFinite(outTok) ? outTok : 0,
            );
            // mirror legacy fields for compatibility
            this.executionStats.inputTokens =
              (this.executionStats.inputTokens || 0) +
              (isFinite(inTok) ? inTok : 0);
            this.executionStats.outputTokens =
              (this.executionStats.outputTokens || 0) +
              (isFinite(outTok) ? outTok : 0);
            this.executionStats.totalTokens =
              (this.executionStats.inputTokens || 0) +
              (this.executionStats.outputTokens || 0);
            this.executionStats.estimatedCost =
              (this.executionStats.inputTokens || 0) * 3e-5 +
              (this.executionStats.outputTokens || 0) * 6e-5;
          }
        }

        if (functionCalls.length > 0) {
          currentMessages = await this.processFunctionCalls(
            functionCalls,
            abortController,
            promptId,
            turnCounter,
          );
        } else {
          // No tool calls — treat this as the model's final answer.
          if (roundText && roundText.trim().length > 0) {
            this.finalText = roundText.trim();
            this.terminateMode = SubagentTerminateMode.GOAL;
            break;
          }
          // Otherwise, nudge the model to finalize a result.
          currentMessages = [
            {
              role: 'user',
              parts: [
                {
                  text: 'Please provide the final result now and stop calling tools.',
                },
              ],
            },
          ];
        }
        this.eventEmitter?.emit(SubAgentEventType.ROUND_END, {
          subagentId: this.subagentId,
          round: turnCounter,
          promptId,
          timestamp: Date.now(),
        } as SubAgentRoundEvent);
      }
    } catch (error) {
      console.error('Error during subagent execution:', error);
      this.terminateMode = SubagentTerminateMode.ERROR;
      this.eventEmitter?.emit(SubAgentEventType.ERROR, {
        subagentId: this.subagentId,
        error: error instanceof Error ? error.message : String(error),
        timestamp: Date.now(),
      } as SubAgentErrorEvent);

      // Log telemetry for subagent error
      const errorEvent = new SubagentExecutionEvent(this.name, 'failed', {
        terminate_reason: SubagentTerminateMode.ERROR,
        result: error instanceof Error ? error.message : String(error),
      });
      logSubagentExecution(this.runtimeContext, errorEvent);

      throw error;
    } finally {
      if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
      this.executionStats.totalDurationMs = Date.now() - startTime;
      const summary = this.stats.getSummary(Date.now());
      this.eventEmitter?.emit(SubAgentEventType.FINISH, {
        subagentId: this.subagentId,
        terminate_reason: this.terminateMode,
        timestamp: Date.now(),
        rounds: summary.rounds,
        totalDurationMs: summary.totalDurationMs,
        totalToolCalls: summary.totalToolCalls,
        successfulToolCalls: summary.successfulToolCalls,
        failedToolCalls: summary.failedToolCalls,
        inputTokens: summary.inputTokens,
        outputTokens: summary.outputTokens,
        totalTokens: summary.totalTokens,
      } as SubAgentFinishEvent);

      const completionEvent = new SubagentExecutionEvent(
        this.name,
        this.terminateMode === SubagentTerminateMode.GOAL
          ? 'completed'
          : 'failed',
        {
          terminate_reason: this.terminateMode,
          result: this.finalText,
          execution_summary: this.stats.formatCompact(
            'Subagent execution completed',
          ),
        },
      );
      logSubagentExecution(this.runtimeContext, completionEvent);

      await this.hooks?.onStop?.({
        subagentId: this.subagentId,
        name: this.name,
        terminateReason: this.terminateMode,
        summary: summary as unknown as Record<string, unknown>,
        timestamp: Date.now(),
      });
    }
  }

  /**
   * Processes a list of function calls, executing each one and collecting their responses.
   * This method iterates through the provided function calls, executes them using the
   * `executeToolCall` function (or handles `self.emitvalue` internally), and aggregates
   * their results. It also manages error reporting for failed tool executions.
   * @param {FunctionCall[]} functionCalls - An array of `FunctionCall` objects to process.
   * @param {ToolRegistry} toolRegistry - The tool registry to look up and execute tools.
   * @param {AbortController} abortController - An `AbortController` to signal cancellation of tool executions.
   * @returns {Promise<Content[]>} A promise that resolves to an array of `Content` parts representing the tool responses,
   *          which are then used to update the chat history.
   */
  private async processFunctionCalls(
    functionCalls: FunctionCall[],
    abortController: AbortController,
    promptId: string,
    currentRound: number,
  ): Promise<Content[]> {
    const toolResponseParts: Part[] = [];

    for (const functionCall of functionCalls) {
      const toolName = String(functionCall.name || 'unknown');
      const callId = functionCall.id ?? `${functionCall.name}-${Date.now()}`;
      const requestInfo: ToolCallRequestInfo = {
        callId,
        name: functionCall.name as string,
        args: (functionCall.args ?? {}) as Record<string, unknown>,
        isClientInitiated: true,
        prompt_id: promptId,
      };

      // Get tool description before execution
      const description = this.getToolDescription(toolName, requestInfo.args);

      // Emit tool call event BEFORE execution
      this.eventEmitter?.emit(SubAgentEventType.TOOL_CALL, {
        subagentId: this.subagentId,
        round: currentRound,
        callId,
        name: toolName,
        args: requestInfo.args,
        description,
        timestamp: Date.now(),
      } as SubAgentToolCallEvent);

      // Execute tools with timing and hooks
      const start = Date.now();
      await this.hooks?.preToolUse?.({
        subagentId: this.subagentId,
        name: this.name,
        toolName,
        args: requestInfo.args,
        timestamp: Date.now(),
      });
      const toolResponse = await executeToolCall(
        this.runtimeContext,
        requestInfo,
        abortController.signal,
      );
      const duration = Date.now() - start;
      // Update tool call stats
      this.executionStats.totalToolCalls += 1;
      if (toolResponse.error) {
        this.executionStats.failedToolCalls += 1;
      } else {
        this.executionStats.successfulToolCalls += 1;
      }

      // Update per-tool usage
      const tu = this.toolUsage.get(toolName) || {
        count: 0,
        success: 0,
        failure: 0,
        totalDurationMs: 0,
        averageDurationMs: 0,
      };
      tu.count += 1;
      if (toolResponse?.error) {
        tu.failure += 1;
        tu.lastError = toolResponse.error?.message || 'Unknown error';
      } else {
        tu.success += 1;
      }
      if (typeof tu.totalDurationMs === 'number') {
        tu.totalDurationMs += duration;
        tu.averageDurationMs =
          tu.count > 0 ? tu.totalDurationMs / tu.count : tu.totalDurationMs;
      } else {
        tu.totalDurationMs = duration;
        tu.averageDurationMs = duration;
      }
      this.toolUsage.set(toolName, tu);

      // Emit tool result event
      this.eventEmitter?.emit(SubAgentEventType.TOOL_RESULT, {
        subagentId: this.subagentId,
        round: currentRound,
        callId,
        name: toolName,
        success: !toolResponse?.error,
        error: toolResponse?.error?.message,
        resultDisplay: toolResponse?.resultDisplay
          ? typeof toolResponse.resultDisplay === 'string'
            ? toolResponse.resultDisplay
            : JSON.stringify(toolResponse.resultDisplay)
          : undefined,
        durationMs: duration,
        timestamp: Date.now(),
      } as SubAgentToolResultEvent);

      // Update statistics service
      this.stats.recordToolCall(
        toolName,
        !toolResponse?.error,
        duration,
        this.toolUsage.get(toolName)?.lastError,
      );

      // post-tool hook
      await this.hooks?.postToolUse?.({
        subagentId: this.subagentId,
        name: this.name,
        toolName,
        args: requestInfo.args,
        success: !toolResponse?.error,
        durationMs: duration,
        errorMessage: toolResponse?.error?.message,
        timestamp: Date.now(),
      });

      if (toolResponse.error) {
        console.error(
          `Error executing tool ${functionCall.name}: ${toolResponse.error.message}`,
        );
      }

      if (toolResponse.responseParts) {
        const parts = Array.isArray(toolResponse.responseParts)
          ? toolResponse.responseParts
          : [toolResponse.responseParts];
        for (const part of parts) {
          if (typeof part === 'string') {
            toolResponseParts.push({ text: part });
          } else if (part) {
            toolResponseParts.push(part);
          }
        }
      }
    }
    // If all tool calls failed, inform the model so it can re-evaluate.
    if (functionCalls.length > 0 && toolResponseParts.length === 0) {
      toolResponseParts.push({
        text: 'All tool calls failed. Please analyze the errors and try an alternative approach.',
      });
    }

    return [{ role: 'user', parts: toolResponseParts }];
  }

  getEventEmitter() {
    return this.eventEmitter;
  }

  getStatistics() {
    const total = this.executionStats.totalToolCalls;
    const successRate =
      total > 0 ? (this.executionStats.successfulToolCalls / total) * 100 : 0;
    return {
      ...this.executionStats,
      successRate,
      toolUsage: Array.from(this.toolUsage.entries()).map(([name, v]) => ({
        name,
        ...v,
      })),
    };
  }

  getExecutionSummary(): SubagentStatsSummary {
    return this.stats.getSummary();
  }

  getFinalText(): string {
    return this.finalText;
  }

  getTerminateMode(): SubagentTerminateMode {
    return this.terminateMode;
  }

  private async createChatObject(context: ContextState) {
    if (!this.promptConfig.systemPrompt && !this.promptConfig.initialMessages) {
      throw new Error(
        'PromptConfig must have either `systemPrompt` or `initialMessages` defined.',
      );
    }
    if (this.promptConfig.systemPrompt && this.promptConfig.initialMessages) {
      throw new Error(
        'PromptConfig cannot have both `systemPrompt` and `initialMessages` defined.',
      );
    }

    const envParts = await getEnvironmentContext(this.runtimeContext);
    const envHistory: Content[] = [
      { role: 'user', parts: envParts },
      { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
    ];

    const start_history = [
      ...envHistory,
      ...(this.promptConfig.initialMessages ?? []),
    ];

    const systemInstruction = this.promptConfig.systemPrompt
      ? this.buildChatSystemPrompt(context)
      : undefined;

    try {
      const generationConfig: GenerateContentConfig & {
        systemInstruction?: string | Content;
      } = {
        temperature: this.modelConfig.temp,
        topP: this.modelConfig.top_p,
      };

      if (systemInstruction) {
        generationConfig.systemInstruction = systemInstruction;
      }

      const contentGenerator = await createContentGenerator(
        this.runtimeContext.getContentGeneratorConfig(),
        this.runtimeContext,
        this.runtimeContext.getSessionId(),
      );

      if (this.modelConfig.model) {
        this.runtimeContext.setModel(this.modelConfig.model);
      }

      return new GeminiChat(
        this.runtimeContext,
        contentGenerator,
        generationConfig,
        start_history,
      );
    } catch (error) {
      await reportError(
        error,
        'Error initializing Gemini chat session.',
        start_history,
        'startChat',
      );
      // The calling function will handle the undefined return.
      return undefined;
    }
  }

  /**
   * Safely retrieves the description of a tool by attempting to build it.
   * Returns an empty string if any error occurs during the process.
   *
   * @param toolName The name of the tool to get description for.
   * @param args The arguments that would be passed to the tool.
   * @returns The tool description or empty string if error occurs.
   */
  private getToolDescription(
    toolName: string,
    args: Record<string, unknown>,
  ): string {
    try {
      const toolRegistry = this.runtimeContext.getToolRegistry();
      const tool = toolRegistry.getTool(toolName);
      if (!tool) {
        return '';
      }

      const toolInstance = tool.build(args);
      return toolInstance.getDescription() || '';
    } catch {
      // Safely ignore all runtime errors and return empty string
      return '';
    }
  }

  private buildChatSystemPrompt(context: ContextState): string {
    if (!this.promptConfig.systemPrompt) {
      // This should ideally be caught in createChatObject, but serves as a safeguard.
      return '';
    }

    let finalPrompt = templateString(this.promptConfig.systemPrompt, context);

    // Add general non-interactive instructions.
    finalPrompt += `

Important Rules:
 - You operate in non-interactive mode: do not ask the user questions; proceed with available context.
 - Use tools only when necessary to obtain facts or make changes.
 - When the task is complete, return the final result as a normal model response (not a tool call) and stop.`;

    return finalPrompt;
  }
}
