// ABOUTME: Chat service supporting streaming completions with multi-provider routing.
// ABOUTME: Routes requests through provider abstraction for Seren, Anthropic, OpenAI, Gemini.

import {
  extractEvidenceFromToolLoopMessages,
  type FinalOutputValidationReport,
  validateFinalOutput,
} from "@/lib/agent-output-validation";
import { isTextMime, toDataUrl } from "@/lib/images/attachments";
import { retrieveCodeContext } from "@/lib/indexing/context-retrieval";
import {
  buildChatRequest,
  sendProviderMessage,
  streamProviderMessage,
} from "@/lib/providers";
import { sendMessageWithTools as sendWithTools } from "@/lib/providers/seren";
import type {
  Attachment,
  ChatMessageWithTools,
  ChatResponse,
  ContentBlock,
  ProviderId,
  ToolCall,
  ToolResult,
} from "@/lib/providers/types";
import { executeTools, getAllTools } from "@/lib/tools";
import {
  getCallablePublisherSlugs,
  getGatewayTools,
  isGatewayInitInFlight,
  isGatewayInitialized,
  waitForGatewayReady,
} from "@/services/mcp-gateway";
import {
  bootstrapMemoryContext,
  processAssistantResponseMemory,
} from "@/services/memory";
import { authStore } from "@/stores/auth.store";
import { conversationStore } from "@/stores/conversation.store";
import { fileTreeState } from "@/stores/fileTree";
import { providerStore } from "@/stores/provider.store";
import {
  getActiveToolsetPublishers,
  settingsStore,
} from "@/stores/settings.store";
import { skillsStore } from "@/stores/skills.store";

export type ChatRole = "user" | "assistant" | "system";

export interface ChatContextRange {
  startLine: number;
  endLine: number;
}

export interface ChatContext {
  content: string;
  file?: string | null;
  range?: ChatContextRange | null;
}

export interface Message {
  id: string;
  role: ChatRole;
  content: string;
  images?: Attachment[];
  thinking?: string;
  model?: string;
  /** Producer provenance — the provider that emitted this message. */
  provider?: string;
  timestamp: number;
  status?: "pending" | "streaming" | "complete" | "error";
  error?: string | null;
  attemptCount?: number;
  /** Duration in milliseconds for how long the response took */
  duration?: number;
  request?: {
    prompt: string;
    context?: ChatContext;
  };
}

export const CHAT_MAX_RETRIES = 3;
const INITIAL_DELAY = 1000;
const TRANSIENT_STATUS_CODES = ["408", "429", "500", "502", "503", "504"];

/**
 * Maximum time to wait for the MCP gateway to finish initializing before
 * assembling the first system prompt. Short enough that the user does not
 * perceive a hang, long enough for a warm login to complete.
 */
export const GATEWAY_READY_TIMEOUT_MS = 5000;

/**
 * Tone and behavior rules injected into every chat system prompt.
 *
 * Kept here as a single source of truth for the JS direct-provider path.
 * The Rust orchestrator path (src-tauri/src/orchestrator/chat_model_worker.rs)
 * carries a matching copy — update both when changing this block.
 */
export const TONE_INSTRUCTIONS =
  "\n\nTone and behavior:\n" +
  "- Be concise. Lead with the answer, not preamble.\n" +
  '- Never open with "Great question," "Excellent," "Perfect," or "You\'re absolutely right."\n' +
  "- Do not use emojis unless the user uses them first.\n" +
  "- Push back honestly on bad ideas. The user wants candor, not validation.\n" +
  "- Never claim a tool or capability is unavailable without first checking your actual tool list. " +
  "If the tool list is empty or still loading, say so — do not assert the capability does not exist.";

/**
 * Check if an error is transient and should be retried.
 */
function isTransientError(message: string): boolean {
  if (
    message.includes("401") ||
    message.includes("403") ||
    message.includes("API key")
  ) {
    return false;
  }
  return TRANSIENT_STATUS_CODES.some((code) => message.includes(code));
}

/**
 * Call sendWithTools with retry on transient failures (408, 429, 5xx).
 */
async function sendWithToolsRetry(
  messages: ChatMessageWithTools[],
  model: string,
  tools: ReturnType<typeof getAllTools> | undefined,
  toolChoice: "auto" | undefined,
): Promise<ChatResponse> {
  let lastError: Error | null = null;

  for (let attempt = 1; attempt <= CHAT_MAX_RETRIES; attempt++) {
    try {
      return await sendWithTools(messages, model, tools, toolChoice);
    } catch (error) {
      lastError = error as Error;
      const msg = lastError.message || "";

      if (!isTransientError(msg)) {
        throw lastError;
      }

      if (attempt < CHAT_MAX_RETRIES) {
        const delay = INITIAL_DELAY * 2 ** (attempt - 1);
        console.warn(
          `[sendWithToolsRetry] Attempt ${attempt} failed (${msg}), retrying in ${delay}ms...`,
        );
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }

  throw lastError ?? new Error("Tool-use request failed after retries");
}

/**
 * Send a non-streaming message through the given provider.
 *
 * The provider is the per-thread runtime binding, passed explicitly by
 * callers so they cannot silently inherit the user's globally-selected
 * provider when a thread has its own setting.
 */
export async function sendMessage(
  content: string,
  model: string,
  provider: ProviderId,
  context?: ChatContext,
  history?: Message[],
): Promise<string> {
  const request = buildChatRequest(content, model, context, history);
  return sendProviderMessage(provider, request);
}

/**
 * Stream a message through the given provider. The provider is the
 * per-thread runtime binding, passed explicitly by callers.
 */
export async function* streamMessage(
  content: string,
  model: string,
  provider: ProviderId,
  context?: ChatContext,
  history?: Message[],
): AsyncGenerator<string> {
  const request = buildChatRequest(content, model, context, history);
  request.stream = true;
  yield* streamProviderMessage(provider, request);
}

/**
 * Send a message with automatic retry on transient failures.
 */
export async function sendMessageWithRetry(
  content: string,
  model: string,
  provider: ProviderId,
  context: ChatContext | undefined,
  onRetry?: (attempt: number) => void,
  history?: Message[],
): Promise<string> {
  let lastError: Error | null = null;

  for (let attempt = 1; attempt <= CHAT_MAX_RETRIES; attempt++) {
    try {
      return await sendMessage(content, model, provider, context, history);
    } catch (error) {
      lastError = error as Error;

      const message = lastError.message || "";
      if (!isTransientError(message)) {
        throw lastError;
      }

      if (attempt < CHAT_MAX_RETRIES) {
        const delay = INITIAL_DELAY * 2 ** (attempt - 1);
        onRetry?.(attempt);
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }

  throw lastError ?? new Error("Chat request failed");
}

/**
 * Get the currently active provider ID.
 */
export function getActiveProvider(): string {
  return providerStore.activeProvider;
}

/**
 * Get the currently active model ID.
 */
export function getActiveModel(): string {
  return providerStore.activeModel;
}

// ============================================================================
// Tool-aware Chat Functions
// ============================================================================

/**
 * State needed to continue a paused tool iteration loop.
 */
export interface ToolIterationState {
  messages: ChatMessageWithTools[];
  model: string;
  tools: ReturnType<typeof getAllTools> | undefined;
  fullContent: string;
  iteration: number;
}

/**
 * Event types yielded during tool-aware message streaming.
 */
export type ToolStreamEvent =
  | { type: "content"; content: string }
  | { type: "thinking"; thinking: string }
  | { type: "tool_calls"; toolCalls: ToolCall[] }
  | { type: "tool_results"; results: ToolResult[] }
  | {
      type: "complete";
      finalContent: string;
      finalThinking?: string;
      finalOutputValidation?: FinalOutputValidationReport;
    }
  | {
      type: "iteration_limit";
      currentIteration: number;
      maxIterations: number;
      continueState: ToolIterationState;
    };

/**
 * Check if a MIME type is supported for vision/multimodal content blocks.
 * Anthropic API only supports: image/jpeg, image/png, image/gif, image/webp
 */
function isVisionCompatibleMime(mimeType: string): boolean {
  return ["image/jpeg", "image/png", "image/gif", "image/webp"].includes(
    mimeType,
  );
}

/**
 * Build multimodal content blocks from text and optional attachments.
 * - Vision-compatible images (jpg, png, gif, webp) become image_url content blocks
 * - PDFs become document content blocks (Anthropic format)
 * - Text/code files are inlined as code blocks
 * - Other formats (SVG, etc.) are noted as unsupported
 */
function buildUserContent(
  text: string,
  attachments?: Attachment[],
): string | ContentBlock[] {
  if (!attachments || attachments.length === 0) {
    return text;
  }

  // Separate files by type
  const visionImages: Attachment[] = [];
  const pdfDocuments: Attachment[] = [];
  const inlinedParts: string[] = [];
  const unsupportedFiles: string[] = [];

  for (const att of attachments) {
    if (isTextMime(att.mimeType)) {
      // Decode base64 text and inline as a code block
      const decoded = atob(att.base64);
      const ext = att.name.split(".").pop() || "";
      inlinedParts.push(`\`\`\`${ext} (${att.name})\n${decoded}\n\`\`\``);
    } else if (att.mimeType === "application/pdf") {
      // PDFs use document content blocks (Anthropic format)
      pdfDocuments.push(att);
    } else if (isVisionCompatibleMime(att.mimeType)) {
      // Vision-compatible image formats
      visionImages.push(att);
    } else {
      // SVGs and other unsupported formats
      unsupportedFiles.push(`${att.name} (${att.mimeType})`);
    }
  }

  // Build text parts
  const textParts: string[] = [];
  if (inlinedParts.length > 0) {
    textParts.push(inlinedParts.join("\n\n"));
  }
  if (unsupportedFiles.length > 0) {
    textParts.push(
      `[Note: The following files cannot be processed: ${unsupportedFiles.join(", ")}. Supported formats: images (jpg, png, gif, webp), PDFs, and text/code files.]`,
    );
  }
  textParts.push(text);

  const fullText = textParts.join("\n\n");

  // If no media files, return plain text
  if (visionImages.length === 0 && pdfDocuments.length === 0) {
    return fullText;
  }

  // Build content blocks for images and PDFs
  const blocks: ContentBlock[] = [];

  // Add PDF documents first
  for (const att of pdfDocuments) {
    blocks.push({
      type: "document",
      source: {
        type: "base64",
        media_type: "application/pdf",
        data: att.base64,
      },
    });
  }

  // Add vision-compatible images
  for (const att of visionImages) {
    blocks.push({
      type: "image_url",
      image_url: { url: toDataUrl(att) },
    });
  }

  // Add text content last
  blocks.push({ type: "text", text: fullText });

  return blocks;
}

/**
 * Send a message with tool support enabled.
 * Implements the tool execution loop: send → tool_calls → execute → send results → repeat.
 *
 * @param content - User's message content
 * @param model - Model ID to use
 * @param context - Optional code context
 * @param enableTools - Whether to enable tools (default true)
 * @param history - Previous messages in the conversation
 * @param images - Optional image attachments
 */
export async function* streamMessageWithTools(
  content: string,
  model: string,
  context?: ChatContext,
  enableTools = true,
  history: Message[] = [],
  images?: Attachment[],
): AsyncGenerator<ToolStreamEvent> {
  // Build initial messages array
  const messages: ChatMessageWithTools[] = [];

  // Wait for the MCP gateway to finish initializing before reading the
  // publisher cache. Without this, the first message after login races the
  // background initializeGateway() call and lands while the cache is empty,
  // producing a system prompt that falsely claims no Seren tools are
  // available. waitForGatewayReady is a no-op once the cache is hot.
  if (authStore.isAuthenticated && !isGatewayInitialized()) {
    await waitForGatewayReady(GATEWAY_READY_TIMEOUT_MS);
  }

  // Build Seren MCP publishers context dynamically based on active toolset.
  // Uses the canonical publisher inventory (all callable publishers) as the
  // source of truth, not just publishers that expose first-class MCP tools.
  // This prevents false "not available" responses for publishers like Gmail
  // that are callable via call_publisher but don't appear in gateway tools.
  const buildPublishersContext = (): string => {
    // Canonical source: all publishers from list_agent_publishers
    const allPublisherSlugs = getCallablePublisherSlugs();

    // Also include publishers derived from gateway tools (belt and suspenders)
    const toolPublishers = [
      ...new Set(getGatewayTools().map((t) => t.publisher)),
    ];

    // Merge both sources, deduplicate
    const allSlugs = [...new Set([...allPublisherSlugs, ...toolPublishers])];

    // Filter by active toolset if one is selected
    const activePublishers = getActiveToolsetPublishers();
    const publishers = activePublishers
      ? allSlugs.filter((slug) => activePublishers.includes(slug))
      : allSlugs;

    if (publishers.length === 0) {
      // Honest fallback: distinguish "still loading / unreachable" from
      // "user has actually disabled all publishers". The former must NOT
      // make the model deny access — it should tell the user to retry.
      if (
        authStore.isAuthenticated &&
        (isGatewayInitInFlight() || !isGatewayInitialized())
      ) {
        return (
          "\n\nIMPORTANT — Seren MCP Gateway Status:\n" +
          "The Seren MCP gateway is still initializing or temporarily unreachable. " +
          "Do NOT claim that Seren publishers or tools are unavailable. " +
          "Tell the user the tools are still loading and to retry their request in a moment."
        );
      }
      return "";
    }

    const publisherList = publishers.sort().join(", ");
    return (
      "\n\nIMPORTANT - Available Seren MCP Publishers:\n" +
      "This application connects to Seren MCP publishers - third-party data services. " +
      `You have access to the following publishers: ${publisherList}. ` +
      "These publishers are callable via the call_publisher tool. " +
      "ONLY use tools from these publishers. Do NOT suggest or attempt to use publishers that are not in this list. " +
      "When users mention publisher names, interpret them in the Seren MCP context unless they explicitly ask about the technology/framework itself."
    );
  };
  const serenPublishersContext = buildPublishersContext();

  // Build system message - conditional based on actual tool availability
  const toolCount = enableTools ? getAllTools(model).length : 0;
  let systemContent: string;

  if (toolCount > 0) {
    // Tools are available - describe capabilities accurately
    systemContent =
      `You are a helpful coding assistant running inside Seren Desktop with access to ${toolCount} tools. ` +
      "You can read, write, and create files and directories on the user's computer using the available tools. " +
      "When the user asks you to save, export, or write content to a file, use the write_file tool to save it to their filesystem. " +
      "Always ask for the desired file path if the user doesn't specify one.\n\n" +
      "IMPORTANT — Tool Usage Guidelines:\n" +
      "- ALWAYS use your tools proactively to accomplish tasks. Do NOT tell the user you cannot do something if a tool can help.\n" +
      "- For web searches: use seren_web_fetch with a search engine URL like " +
      "'https://html.duckduckgo.com/html/?q=your+search+terms' to find information.\n" +
      "- For fetching web pages: use seren_web_fetch with the page URL.\n" +
      "- Chain tool calls when needed: search first to find URLs, then fetch those URLs for full content.\n" +
      "- NEVER say 'I cannot browse the web' or 'I need a URL' — you CAN search by constructing search engine URLs.\n" +
      serenPublishersContext;
  } else {
    // No tools available - don't claim tool capabilities
    systemContent =
      "You are a helpful coding assistant running inside Seren Desktop. " +
      "Note: File system tools are currently not available. " +
      "You can help with code questions, explanations, and provide code snippets, but cannot directly read or write files." +
      serenPublishersContext;
  }

  // Add Seren product identity context
  systemContent +=
    "\n\nIMPORTANT — Seren Product Identity:\n" +
    '"Seren" refers to the Seren platform at https://www.serendb.com — a serverless Postgres database platform with AI agents, ' +
    "MCP tool integration, and a publisher marketplace. It is NOT seren.xyz or any other project. " +
    "When users ask about Seren, always answer in the context of SerenDB (https://www.serendb.com). " +
    "Key Seren concepts: SerenBucks (billing credits), Publishers (third-party data services), " +
    "Skills (installable prompt-based capabilities from the seren-skills repo), " +
    "Gateway API (AI model access), MCP servers (tool integration), and Seren Desktop (this application).";

  // Tone and behavior instructions — must match Rust orchestrator copy at
  // src-tauri/src/orchestrator/chat_model_worker.rs TONE_INSTRUCTIONS.
  systemContent += TONE_INSTRUCTIONS;

  // Add user-provided context if available
  if (context) {
    if (context.file) {
      systemContent += `\n\nThe user has selected code from ${context.file}`;
      if (context.range) {
        systemContent += ` (lines ${context.range.startLine}-${context.range.endLine})`;
      }
      systemContent += `:\n\n\`\`\`\n${context.content}\n\`\`\``;
    } else {
      systemContent += `\n\nThe user has selected this code:\n\n\`\`\`\n${context.content}\n\`\`\``;
    }
  }

  // Retrieve and inject semantic code context if available
  try {
    const projectPath = fileTreeState.rootPath;
    const semanticContext = await retrieveCodeContext(projectPath, content);
    if (semanticContext) {
      systemContent += semanticContext;
    }
  } catch (error) {
    // Silently fail - semantic context is optional
    console.warn("[Chat] Failed to retrieve semantic context:", error);
  }

  // Inject enabled skills content
  try {
    const skillsContent = await skillsStore.getThreadSkillsContent(
      fileTreeState.rootPath,
      conversationStore.activeConversationId,
    );
    if (skillsContent) {
      systemContent += skillsContent;
    }
  } catch (error) {
    console.warn("[Chat] Failed to retrieve skills content:", error);
  }

  // Inject memory context if enabled and authenticated
  if (settingsStore.get("memoryEnabled") && authStore.isAuthenticated) {
    try {
      const memoryContext = await bootstrapMemoryContext();
      if (memoryContext) {
        systemContent += memoryContext;
      }
    } catch (error) {
      console.warn("[Chat] Failed to retrieve memory context:", error);
    }
  }

  // Add system message to messages array
  messages.push({ role: "system", content: systemContent });

  // Add conversation history (user and assistant messages only)
  for (const msg of history) {
    if (msg.role === "user" || msg.role === "assistant") {
      messages.push({ role: msg.role, content: msg.content });
    }
  }

  // Add current user message (with images if attached)
  messages.push({ role: "user", content: buildUserContent(content, images) });

  // Get tools if enabled, with model-specific limits
  const tools = enableTools ? getAllTools(model) : undefined;

  // Get max iterations from settings (0 = unlimited)
  const maxIterations = settingsStore.get("chatMaxToolIterations");

  // Accumulated content across all iterations
  let fullContent = "";
  let yieldedContent = "";
  let hasExecutedTools = false;
  let hasNudged = false;

  for (
    let iteration = 0;
    maxIterations === 0 || iteration < maxIterations;
    iteration++
  ) {
    console.log("[streamMessageWithTools] Iteration:", iteration);
    // Send request with tools (retries on transient errors like 408 timeout)
    const response: ChatResponse = await sendWithToolsRetry(
      messages,
      model,
      tools,
      tools ? "auto" : undefined,
    );
    console.log("[streamMessageWithTools] Got response:", response);

    // Check if model wants to call tools
    if (!response.tool_calls || response.tool_calls.length === 0) {
      if (response.content) {
        fullContent += response.content;
      } else {
        console.log("[streamMessageWithTools] No content in response");
      }
      // Model returned no tool calls. If we executed tools but got no text
      // response, the model may have silently stopped mid-task. Nudge it once
      // to complete the task or explain what happened.
      if (hasExecutedTools && !fullContent.trim() && !hasNudged) {
        hasNudged = true;
        console.warn(
          "[streamMessageWithTools] Empty response after tool execution — nudging model to complete task",
        );
        messages.push({
          role: "assistant",
          content: response.content || "",
        });
        messages.push({
          role: "user",
          content:
            "You called tools but did not provide a response or complete the requested task. " +
            "Please review the tool results above and either complete the task using the appropriate tools, " +
            "or explain what happened.",
        });
        continue;
      }

      console.log(
        "[streamMessageWithTools] No tool_calls, completing with content length:",
        fullContent.length,
      );
      const finalOutputValidation = validateFinalOutput({
        finalText: fullContent,
        evidence: extractEvidenceFromToolLoopMessages(messages),
      });
      fullContent = finalOutputValidation.safeDisplayText;

      // Store conversation to memory if enabled
      if (finalOutputValidation.canStoreMemory) {
        processAssistantResponseMemory(fullContent, {
          model,
          userQuery: content,
        }).catch((err) => {
          console.warn("[streamMessageWithTools] Failed to store memory:", err);
        });
      }

      const safeDelta = fullContent.startsWith(yieldedContent)
        ? fullContent.slice(yieldedContent.length)
        : fullContent;
      if (safeDelta) {
        yieldedContent += safeDelta;
        yield { type: "content", content: safeDelta };
      }
      yield {
        type: "complete",
        finalContent: fullContent,
        finalOutputValidation,
      };
      return;
    }

    // Non-final assistant text can accompany tool calls. It is not persisted as
    // the final answer; the final turn is validated after tool execution.
    if (response.content) {
      console.log(
        "[streamMessageWithTools] Yielding content:",
        response.content.substring(0, 100),
      );
      fullContent += response.content;
      yieldedContent += response.content;
      yield { type: "content", content: response.content };
    } else {
      console.log("[streamMessageWithTools] No content in response");
    }

    // Yield tool calls for UI
    const toolNames = response.tool_calls.map((tc) => tc.function.name);
    console.log(
      `[streamMessageWithTools] Iteration ${iteration}: tool_calls =`,
      toolNames,
    );
    yield { type: "tool_calls", toolCalls: response.tool_calls };

    // Add assistant message with tool_calls to history
    messages.push({
      role: "assistant",
      content: response.content,
      tool_calls: response.tool_calls,
    });

    // Execute tools
    const results = await executeTools(response.tool_calls);
    hasExecutedTools = true;

    // Log tool execution results
    for (const result of results) {
      if (result.is_error) {
        console.warn(
          `[streamMessageWithTools] Tool error: ${result.tool_call_id}`,
          result.content.substring(0, 200),
        );
      }
    }

    // Yield results for UI
    yield { type: "tool_results", results };

    // Add tool results to messages
    for (const result of results) {
      messages.push({
        role: "tool",
        content: result.content,
        tool_call_id: result.tool_call_id,
      });
    }

    // Continue loop to get model's response to tool results
  }

  // If we hit max iterations, yield an event that allows the user to continue
  if (!fullContent.trim()) {
    console.warn(
      `[streamMessageWithTools] Hit iteration limit (${maxIterations}) with empty content`,
    );
  }
  yield {
    type: "iteration_limit",
    currentIteration: maxIterations,
    maxIterations,
    continueState: {
      messages,
      model,
      tools,
      fullContent,
      iteration: maxIterations,
    },
  };
}

/**
 * Continue a tool iteration loop from a saved state.
 * Called when user clicks "Continue" after hitting the iteration limit.
 *
 * @param state - The saved state from the iteration_limit event
 * @param additionalIterations - How many more iterations to allow (default: 10)
 */
export async function* continueToolIteration(
  state: ToolIterationState,
  additionalIterations = 10,
): AsyncGenerator<ToolStreamEvent> {
  const { messages, model, tools, fullContent: existingContent } = state;
  let fullContent = existingContent;
  let yieldedContent = existingContent;
  let hasExecutedTools = false;
  let hasNudged = false;

  for (let i = 0; i < additionalIterations; i++) {
    console.log("[continueToolIteration] Iteration:", i);

    const response: ChatResponse = await sendWithToolsRetry(
      messages,
      model,
      tools,
      tools ? "auto" : undefined,
    );

    if (response.content) {
      fullContent += response.content;
    }

    if (!response.tool_calls || response.tool_calls.length === 0) {
      if (hasExecutedTools && !fullContent.trim() && !hasNudged) {
        hasNudged = true;
        console.warn(
          "[continueToolIteration] Empty response after tool execution — nudging model to complete task",
        );
        messages.push({
          role: "assistant",
          content: response.content || "",
        });
        messages.push({
          role: "user",
          content:
            "You called tools but did not provide a response or complete the requested task. " +
            "Please review the tool results above and either complete the task using the appropriate tools, " +
            "or explain what happened.",
        });
        continue;
      }

      // Store conversation to memory if enabled
      const finalOutputValidation = validateFinalOutput({
        finalText: fullContent,
        evidence: extractEvidenceFromToolLoopMessages(messages),
      });
      fullContent = finalOutputValidation.safeDisplayText;
      if (finalOutputValidation.canStoreMemory) {
        processAssistantResponseMemory(fullContent, {
          model,
        }).catch((err) => {
          console.warn("[continueToolIteration] Failed to store memory:", err);
        });
      }

      const safeDelta = fullContent.startsWith(yieldedContent)
        ? fullContent.slice(yieldedContent.length)
        : fullContent;
      if (safeDelta) {
        yieldedContent += safeDelta;
        yield { type: "content", content: safeDelta };
      }
      yield {
        type: "complete",
        finalContent: fullContent,
        finalOutputValidation,
      };
      return;
    }

    if (response.content) {
      yieldedContent += response.content;
      yield { type: "content", content: response.content };
    }

    yield { type: "tool_calls", toolCalls: response.tool_calls };

    messages.push({
      role: "assistant",
      content: response.content,
      tool_calls: response.tool_calls,
    });

    const results = await executeTools(response.tool_calls);
    hasExecutedTools = true;
    yield { type: "tool_results", results };

    for (const result of results) {
      messages.push({
        role: "tool",
        content: result.content,
        tool_call_id: result.tool_call_id,
      });
    }
  }

  // Hit the additional limit again
  yield {
    type: "iteration_limit",
    currentIteration: state.iteration + additionalIterations,
    maxIterations: additionalIterations,
    continueState: {
      messages,
      model,
      tools,
      fullContent,
      iteration: state.iteration + additionalIterations,
    },
  };
}

/**
 * Check if tools are available for the current provider.
 * Verifies both provider support AND actual tool availability.
 */
export function areToolsAvailable(): boolean {
  // Only Seren provider supports tools
  if (providerStore.activeProvider !== "seren") {
    return false;
  }

  // Actually check if we have tools - this prevents making claims
  // about tool access when tools aren't actually available
  const tools = getAllTools();
  return tools.length > 0;
}

/**
 * Get the count of available tools (for conditional system prompts).
 */
export function getAvailableToolCount(): number {
  if (providerStore.activeProvider !== "seren") {
    return 0;
  }
  return getAllTools().length;
}
