/**
 * File Attachment for automatic upload on the chat container page.
 * @typedef Attachment
 * @property {string} name - the given file name
 * @property {string} mime - the given file mime
 * @property {string} contentString - full base64 encoded string of file
 */

/**
 * @typedef {Object} ResponseMetrics
 * @property {number} prompt_tokens - The number of prompt tokens used
 * @property {number} completion_tokens - The number of completion tokens used
 * @property {number} total_tokens - The total number of tokens used
 * @property {number} outputTps - The output tokens per second
 * @property {number} duration - The duration of the request in seconds
 *
 * @typedef {Object} ChatMessage
 * @property {string} role - The role of the message sender (e.g. 'user', 'assistant', 'system')
 * @property {string} content - The content of the message
 *
 * @typedef {Object} ChatCompletionResponse
 * @property {string} textResponse - The text response from the LLM
 * @property {ResponseMetrics} metrics - The response metrics
 *
 * @typedef {Object} ChatCompletionOptions
 * @property {number} temperature - The sampling temperature for the LLM response
 * @property {import("@prisma/client").users} user - The user object for the chat completion to send to the LLM provider for user tracking (optional)
 *
 * @typedef {function(Array<ChatMessage>, ChatCompletionOptions): Promise<ChatCompletionResponse>} getChatCompletionFunction
 *
 * @typedef {function(Array<ChatMessage>, ChatCompletionOptions): Promise<import("./chat/LLMPerformanceMonitor").MonitoredStream>} streamGetChatCompletionFunction
 */

/**
 * @typedef {Object} BaseLLMProvider - A basic llm provider object
 * @property {string} className - Provider identifier used in logs and response metrics.
 * @property {string} model - The active model name for this provider instance.
 * @property {number} defaultTemp - Default sampling temperature (typically 0.7).
 * @property {Function} streamingEnabled - Checks if streaming is enabled for chat completions.
 * @property {Function} promptWindowLimit - Returns the token limit for the current model.
 * @property {Function} isValidChatCompletionModel - Validates if the provided model is suitable for chat completion.
 * @property {Function} constructPrompt - Constructs a formatted prompt for the chat completion request.
 * @property {getChatCompletionFunction} getChatCompletion - Gets a chat completion response.
 * @property {streamGetChatCompletionFunction} streamGetChatCompletion - Streams a chat completion response.
 * @property {Function} handleStream - Handles the streaming response.
 * @property {Function} embedTextInput - Embeds the provided text input using the specified embedder.
 * @property {Function} embedChunks - Embeds multiple chunks of text using the specified embedder.
 * @property {Function} compressMessages - Compresses chat messages to fit within the token limit.
 */

/**
 * @typedef {Object} BaseLLMProviderClass - Class method of provider - not instantiated
 * @property {function(string): number} promptWindowLimit - Returns the token limit for the provided model.
 */

/**
 * @typedef {Object} BaseVectorDatabaseProvider
 * @property {string} name - The name of the Vector Database instance.
 * @property {Function} connect - Connects to the Vector Database client.
 * @property {Function} totalVectors - Returns the total number of vectors in the database.
 * @property {Function} namespaceCount - Returns the count of vectors in a given namespace.
 * @property {Function} similarityResponse - Performs a similarity search on a given namespace.
 * @property {Function} rerankedSimilarityResponse - Performs a similarity search on a given namespace with reranking (if supported by provider).
 * @property {Function} namespace - Retrieves the specified namespace collection.
 * @property {Function} hasNamespace - Checks if a namespace exists.
 * @property {Function} namespaceExists - Verifies if a namespace exists in the client.
 * @property {Function} deleteVectorsInNamespace - Deletes all vectors in a specified namespace.
 * @property {Function} deleteDocumentFromNamespace - Deletes a document from a specified namespace.
 * @property {Function} addDocumentToNamespace - Adds a document to a specified namespace.
 * @property {Function} performSimilaritySearch - Performs a similarity search in the namespace.
 */

/**
 * @typedef {Object} BaseEmbedderProvider
 * @property {string} model - The model used for embedding.
 * @property {number} maxConcurrentChunks - The maximum number of chunks processed concurrently.
 * @property {number} embeddingMaxChunkLength - The maximum length of each chunk for embedding.
 * @property {Function} embedTextInput - Embeds a single text input.
 * @property {Function} embedChunks - Embeds multiple chunks of text.
 */

/**
 * Gets the systems current vector database provider.
 * @param {('pinecone' | 'chroma' | 'chromacloud' | 'lancedb' | 'weaviate' | 'qdrant' | 'milvus' | 'zilliz' | 'astra') | null} getExactly - If provided, this will return an explit provider.
 * @returns { BaseVectorDatabaseProvider}
 */
function getVectorDbClass(getExactly = null) {
  const vectorSelection = getExactly ?? process.env.VECTOR_DB ?? "lancedb";
  switch (vectorSelection) {
    case "pinecone":
      const { Pinecone } = require("../vectorDbProviders/pinecone");
      return new Pinecone();
    case "chroma":
      const { Chroma } = require("../vectorDbProviders/chroma");
      return new Chroma();
    case "chromacloud":
      const { ChromaCloud } = require("../vectorDbProviders/chromacloud");
      return new ChromaCloud();
    case "lancedb":
      const { LanceDb } = require("../vectorDbProviders/lance");
      return new LanceDb();
    case "weaviate":
      const { Weaviate } = require("../vectorDbProviders/weaviate");
      return new Weaviate();
    case "qdrant":
      const { QDrant } = require("../vectorDbProviders/qdrant");
      return new QDrant();
    case "milvus":
      const { Milvus } = require("../vectorDbProviders/milvus");
      return new Milvus();
    case "zilliz":
      const { Zilliz } = require("../vectorDbProviders/zilliz");
      return new Zilliz();
    case "astra":
      const { AstraDB } = require("../vectorDbProviders/astra");
      return new AstraDB();
    case "pgvector":
      const { PGVector } = require("../vectorDbProviders/pgvector");
      return new PGVector();
    default:
      console.error(
        `\x1b[31m[ENV ERROR]\x1b[0m No VECTOR_DB value found in environment! Falling back to LanceDB`
      );
      const { LanceDb: DefaultLanceDb } = require("../vectorDbProviders/lance");
      return new DefaultLanceDb();
  }
}

/**
 * Returns the LLMProvider with its embedder attached via system or via defined provider.
 * @notice Use resolveProviderConnector instead as this function DOES NOT handle the anythingllm-router provider.
 * You should only use this function if you are absolutely sure you are not using the anythingllm-router provider ever in your code.
 * @param {{provider: string | null, model: string | null} | null} params - Initialize params for LLMs provider
 * @returns {BaseLLMProvider}
 */
function getLLMProvider({ provider = null, model = null } = {}) {
  const LLMSelection = provider ?? process.env.LLM_PROVIDER ?? "openai";
  const embedder = getEmbeddingEngineSelection();

  switch (LLMSelection) {
    case "openai":
      const { OpenAiLLM } = require("../AiProviders/openAi");
      return new OpenAiLLM(embedder, model);
    case "azure":
      const { AzureOpenAiLLM } = require("../AiProviders/azureOpenAi");
      return new AzureOpenAiLLM(embedder, model);
    case "anthropic":
      const { AnthropicLLM } = require("../AiProviders/anthropic");
      return new AnthropicLLM(embedder, model);
    case "gemini":
      const { GeminiLLM } = require("../AiProviders/gemini");
      return new GeminiLLM(embedder, model);
    case "lmstudio":
      const { LMStudioLLM } = require("../AiProviders/lmStudio");
      return new LMStudioLLM(embedder, model);
    case "localai":
      const { LocalAiLLM } = require("../AiProviders/localAi");
      return new LocalAiLLM(embedder, model);
    case "ollama":
      const { OllamaAILLM } = require("../AiProviders/ollama");
      return new OllamaAILLM(embedder, model);
    case "togetherai":
      const { TogetherAiLLM } = require("../AiProviders/togetherAi");
      return new TogetherAiLLM(embedder, model);
    case "fireworksai":
      const { FireworksAiLLM } = require("../AiProviders/fireworksAi");
      return new FireworksAiLLM(embedder, model);
    case "perplexity":
      const { PerplexityLLM } = require("../AiProviders/perplexity");
      return new PerplexityLLM(embedder, model);
    case "openrouter":
      const { OpenRouterLLM } = require("../AiProviders/openRouter");
      return new OpenRouterLLM(embedder, model);
    case "mistral":
      const { MistralLLM } = require("../AiProviders/mistral");
      return new MistralLLM(embedder, model);
    case "groq":
      const { GroqLLM } = require("../AiProviders/groq");
      return new GroqLLM(embedder, model);
    case "koboldcpp":
      const { KoboldCPPLLM } = require("../AiProviders/koboldCPP");
      return new KoboldCPPLLM(embedder, model);
    case "textgenwebui":
      const { TextGenWebUILLM } = require("../AiProviders/textGenWebUI");
      return new TextGenWebUILLM(embedder, model);
    case "cohere":
      const { CohereLLM } = require("../AiProviders/cohere");
      return new CohereLLM(embedder, model);
    case "litellm":
      const { LiteLLM } = require("../AiProviders/liteLLM");
      return new LiteLLM(embedder, model);
    case "generic-openai":
      const { GenericOpenAiLLM } = require("../AiProviders/genericOpenAi");
      return new GenericOpenAiLLM(embedder, model);
    case "bedrock":
      const { AWSBedrockLLM } = require("../AiProviders/bedrock");
      return new AWSBedrockLLM(embedder, model);
    case "deepseek":
      const { DeepSeekLLM } = require("../AiProviders/deepseek");
      return new DeepSeekLLM(embedder, model);
    case "apipie":
      const { ApiPieLLM } = require("../AiProviders/apipie");
      return new ApiPieLLM(embedder, model);
    case "novita":
      const { NovitaLLM } = require("../AiProviders/novita");
      return new NovitaLLM(embedder, model);
    case "xai":
      const { XAiLLM } = require("../AiProviders/xai");
      return new XAiLLM(embedder, model);
    case "nvidia-nim":
      const { NvidiaNimLLM } = require("../AiProviders/nvidiaNim");
      return new NvidiaNimLLM(embedder, model);
    case "ppio":
      const { PPIOLLM } = require("../AiProviders/ppio");
      return new PPIOLLM(embedder, model);
    case "moonshotai":
      const { MoonshotAiLLM } = require("../AiProviders/moonshotAi");
      return new MoonshotAiLLM(embedder, model);
    case "cometapi":
      const { CometApiLLM } = require("../AiProviders/cometapi");
      return new CometApiLLM(embedder, model);
    case "foundry":
      const { FoundryLLM } = require("../AiProviders/foundry");
      return new FoundryLLM(embedder, model);
    case "zai":
      const { ZAiLLM } = require("../AiProviders/zai");
      return new ZAiLLM(embedder, model);
    case "giteeai":
      const { GiteeAILLM } = require("../AiProviders/giteeai");
      return new GiteeAILLM(embedder, model);
    case "docker-model-runner":
      const {
        DockerModelRunnerLLM,
      } = require("../AiProviders/dockerModelRunner");
      return new DockerModelRunnerLLM(embedder, model);
    case "privatemode":
      const { PrivatemodeLLM } = require("../AiProviders/privatemode");
      return new PrivatemodeLLM(embedder, model);
    case "sambanova":
      const { SambaNovaLLM } = require("../AiProviders/sambanova");
      return new SambaNovaLLM(embedder, model);
    case "lemonade":
      const { LemonadeLLM } = require("../AiProviders/lemonade");
      return new LemonadeLLM(embedder, model);
    case "minimax":
      const { MinimaxLLM } = require("../AiProviders/minimax");
      return new MinimaxLLM(embedder, model);
    case "cerebras":
      const { CerebrasLLM } = require("../AiProviders/cerebras");
      return new CerebrasLLM(embedder, model);
    case "anythingllm-router":
      // Model router is handled separately in stream.js via AnythingLLMModelRouter.
      // This case should not be hit directly - if it is, throw a descriptive error.
      throw new Error(
        "anythingllm-router provider must be resolved via AnythingLLMModelRouter class, not getLLMProvider directly."
      );
    default:
      throw new Error(
        `ENV: No valid LLM_PROVIDER value found in environment! Using ${process.env.LLM_PROVIDER}`
      );
  }
}

/**
 * Returns the EmbedderProvider by itself to whatever is currently in the system settings.
 * @returns {BaseEmbedderProvider}
 */
function getEmbeddingEngineSelection() {
  const { NativeEmbedder } = require("../EmbeddingEngines/native");
  const engineSelection = process.env.EMBEDDING_ENGINE;
  switch (engineSelection) {
    case "openai":
      const { OpenAiEmbedder } = require("../EmbeddingEngines/openAi");
      return new OpenAiEmbedder();
    case "azure":
      const {
        AzureOpenAiEmbedder,
      } = require("../EmbeddingEngines/azureOpenAi");
      return new AzureOpenAiEmbedder();
    case "localai":
      const { LocalAiEmbedder } = require("../EmbeddingEngines/localAi");
      return new LocalAiEmbedder();
    case "ollama":
      const { OllamaEmbedder } = require("../EmbeddingEngines/ollama");
      return new OllamaEmbedder();
    case "native":
      return new NativeEmbedder();
    case "lmstudio":
      const { LMStudioEmbedder } = require("../EmbeddingEngines/lmstudio");
      return new LMStudioEmbedder();
    case "cohere":
      const { CohereEmbedder } = require("../EmbeddingEngines/cohere");
      return new CohereEmbedder();
    case "voyageai":
      const { VoyageAiEmbedder } = require("../EmbeddingEngines/voyageAi");
      return new VoyageAiEmbedder();
    case "litellm":
      const { LiteLLMEmbedder } = require("../EmbeddingEngines/liteLLM");
      return new LiteLLMEmbedder();
    case "mistral":
      const { MistralEmbedder } = require("../EmbeddingEngines/mistral");
      return new MistralEmbedder();
    case "generic-openai":
      const {
        GenericOpenAiEmbedder,
      } = require("../EmbeddingEngines/genericOpenAi");
      return new GenericOpenAiEmbedder();
    case "gemini":
      const { GeminiEmbedder } = require("../EmbeddingEngines/gemini");
      return new GeminiEmbedder();
    case "openrouter":
      const { OpenRouterEmbedder } = require("../EmbeddingEngines/openRouter");
      return new OpenRouterEmbedder();
    case "lemonade":
      const { LemonadeEmbedder } = require("../EmbeddingEngines/lemonade");
      return new LemonadeEmbedder();
    default:
      return new NativeEmbedder();
  }
}

/**
 * Returns the LLMProviderClass - this is a helper method to access static methods on a class
 * @param {{provider: string | null} | null} params - Initialize params for LLMs provider
 * @returns {BaseLLMProviderClass}
 */
function getLLMProviderClass({ provider = null } = {}) {
  switch (provider) {
    case "openai":
      const { OpenAiLLM } = require("../AiProviders/openAi");
      return OpenAiLLM;
    case "azure":
      const { AzureOpenAiLLM } = require("../AiProviders/azureOpenAi");
      return AzureOpenAiLLM;
    case "anthropic":
      const { AnthropicLLM } = require("../AiProviders/anthropic");
      return AnthropicLLM;
    case "gemini":
      const { GeminiLLM } = require("../AiProviders/gemini");
      return GeminiLLM;
    case "lmstudio":
      const { LMStudioLLM } = require("../AiProviders/lmStudio");
      return LMStudioLLM;
    case "localai":
      const { LocalAiLLM } = require("../AiProviders/localAi");
      return LocalAiLLM;
    case "ollama":
      const { OllamaAILLM } = require("../AiProviders/ollama");
      return OllamaAILLM;
    case "togetherai":
      const { TogetherAiLLM } = require("../AiProviders/togetherAi");
      return TogetherAiLLM;
    case "fireworksai":
      const { FireworksAiLLM } = require("../AiProviders/fireworksAi");
      return FireworksAiLLM;
    case "perplexity":
      const { PerplexityLLM } = require("../AiProviders/perplexity");
      return PerplexityLLM;
    case "openrouter":
      const { OpenRouterLLM } = require("../AiProviders/openRouter");
      return OpenRouterLLM;
    case "mistral":
      const { MistralLLM } = require("../AiProviders/mistral");
      return MistralLLM;
    case "groq":
      const { GroqLLM } = require("../AiProviders/groq");
      return GroqLLM;
    case "koboldcpp":
      const { KoboldCPPLLM } = require("../AiProviders/koboldCPP");
      return KoboldCPPLLM;
    case "textgenwebui":
      const { TextGenWebUILLM } = require("../AiProviders/textGenWebUI");
      return TextGenWebUILLM;
    case "cohere":
      const { CohereLLM } = require("../AiProviders/cohere");
      return CohereLLM;
    case "litellm":
      const { LiteLLM } = require("../AiProviders/liteLLM");
      return LiteLLM;
    case "generic-openai":
      const { GenericOpenAiLLM } = require("../AiProviders/genericOpenAi");
      return GenericOpenAiLLM;
    case "bedrock":
      const { AWSBedrockLLM } = require("../AiProviders/bedrock");
      return AWSBedrockLLM;
    case "deepseek":
      const { DeepSeekLLM } = require("../AiProviders/deepseek");
      return DeepSeekLLM;
    case "apipie":
      const { ApiPieLLM } = require("../AiProviders/apipie");
      return ApiPieLLM;
    case "novita":
      const { NovitaLLM } = require("../AiProviders/novita");
      return NovitaLLM;
    case "xai":
      const { XAiLLM } = require("../AiProviders/xai");
      return XAiLLM;
    case "nvidia-nim":
      const { NvidiaNimLLM } = require("../AiProviders/nvidiaNim");
      return NvidiaNimLLM;
    case "ppio":
      const { PPIOLLM } = require("../AiProviders/ppio");
      return PPIOLLM;
    case "moonshotai":
      const { MoonshotAiLLM } = require("../AiProviders/moonshotAi");
      return MoonshotAiLLM;
    case "cometapi":
      const { CometApiLLM } = require("../AiProviders/cometapi");
      return CometApiLLM;
    case "foundry":
      const { FoundryLLM } = require("../AiProviders/foundry");
      return FoundryLLM;
    case "zai":
      const { ZAiLLM } = require("../AiProviders/zai");
      return ZAiLLM;
    case "giteeai":
      const { GiteeAILLM } = require("../AiProviders/giteeai");
      return GiteeAILLM;
    case "docker-model-runner":
      const {
        DockerModelRunnerLLM,
      } = require("../AiProviders/dockerModelRunner");
      return DockerModelRunnerLLM;
    case "privatemode":
      const { PrivateModeLLM } = require("../AiProviders/privatemode");
      return PrivateModeLLM;
    case "sambanova":
      const { SambaNovaLLM } = require("../AiProviders/sambanova");
      return SambaNovaLLM;
    case "lemonade":
      const { LemonadeLLM } = require("../AiProviders/lemonade");
      return LemonadeLLM;
    case "minimax":
      const { MinimaxLLM } = require("../AiProviders/minimax");
      return MinimaxLLM;
    case "cerebras":
      const { CerebrasLLM } = require("../AiProviders/cerebras");
      return CerebrasLLM;
    case "anythingllm-router":
      const { AnythingLLMModelRouter } = require("../AiProviders/modelRouter");
      return AnythingLLMModelRouter;
    default:
      return null;
  }
}

/**
 * Returns the defined model (if available) for the given provider.
 * @param {{provider: string | null} | null} params - Initialize params for LLMs provider
 * @returns {string | null}
 */
function getBaseLLMProviderModel({ provider = null } = {}) {
  switch (provider) {
    case "openai":
      return process.env.OPEN_MODEL_PREF;
    case "azure":
      return process.env.AZURE_OPENAI_MODEL_PREF || process.env.OPEN_MODEL_PREF;
    case "anthropic":
      return process.env.ANTHROPIC_MODEL_PREF;
    case "gemini":
      return process.env.GEMINI_LLM_MODEL_PREF;
    case "lmstudio":
      return process.env.LMSTUDIO_MODEL_PREF;
    case "localai":
      return process.env.LOCAL_AI_MODEL_PREF;
    case "ollama":
      return process.env.OLLAMA_MODEL_PREF;
    case "togetherai":
      return process.env.TOGETHER_AI_MODEL_PREF;
    case "fireworksai":
      return process.env.FIREWORKS_AI_LLM_MODEL_PREF;
    case "perplexity":
      return process.env.PERPLEXITY_MODEL_PREF;
    case "openrouter":
      return process.env.OPENROUTER_MODEL_PREF;
    case "mistral":
      return process.env.MISTRAL_MODEL_PREF;
    case "groq":
      return process.env.GROQ_MODEL_PREF;
    case "koboldcpp":
      return process.env.KOBOLD_CPP_MODEL_PREF;
    case "textgenwebui":
      return null;
    case "cohere":
      return process.env.COHERE_MODEL_PREF;
    case "litellm":
      return process.env.LITE_LLM_MODEL_PREF;
    case "generic-openai":
      return process.env.GENERIC_OPEN_AI_MODEL_PREF;
    case "bedrock":
      return process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE;
    case "deepseek":
      return process.env.DEEPSEEK_MODEL_PREF;
    case "apipie":
      return process.env.APIPIE_LLM_MODEL_PREF;
    case "novita":
      return process.env.NOVITA_LLM_MODEL_PREF;
    case "xai":
      return process.env.XAI_LLM_MODEL_PREF;
    case "nvidia-nim":
      return process.env.NVIDIA_NIM_LLM_MODEL_PREF;
    case "ppio":
      return process.env.PPIO_MODEL_PREF;
    case "moonshotai":
      return process.env.MOONSHOT_AI_MODEL_PREF;
    case "cometapi":
      return process.env.COMETAPI_LLM_MODEL_PREF;
    case "foundry":
      return process.env.FOUNDRY_MODEL_PREF;
    case "zai":
      return process.env.ZAI_MODEL_PREF;
    case "giteeai":
      return process.env.GITEE_AI_MODEL_PREF;
    case "docker-model-runner":
      return process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_PREF;
    case "privatemode":
      return process.env.PRIVATEMODE_LLM_MODEL_PREF;
    case "sambanova":
      return process.env.SAMBANOVA_LLM_MODEL_PREF;
    case "lemonade":
      return process.env.LEMONADE_LLM_MODEL_PREF;
    case "minimax":
      return process.env.MINIMAX_MODEL_PREF;
    case "cerebras":
      return process.env.CEREBRAS_MODEL_PREF;
    default:
      return null;
  }
}

// Some models have lower restrictions on chars that can be encoded in a single pass
// and by default we assume it can handle 1,000 chars, but some models use work with smaller
// chars so here we can override that value when embedding information.
function maximumChunkLength() {
  if (
    !!process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH &&
    !isNaN(process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH) &&
    Number(process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH) > 1
  )
    return Number(process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH);

  return 1_000;
}

function toChunks(arr, size) {
  return Array.from({ length: Math.ceil(arr.length / size) }, (_v, i) =>
    arr.slice(i * size, i * size + size)
  );
}

/**
 * Report chunk-level embedding progress from any embedder.
 * Works in both the child worker process (IPC via process.send) and the
 * main server process (direct SSE emit via EmbeddingWorkerManager).
 *
 * Requires `global.__embeddingProgress` to be set by the caller with
 * { workspaceSlug, filename, userId }.
 *
 * @param {number} chunksProcessed
 * @param {number} totalChunks
 */
function reportEmbeddingProgress(chunksProcessed, totalChunks) {
  if (!global.__embeddingProgress) return;
  const ctx = global.__embeddingProgress;
  const event = {
    type: "chunk_progress",
    workspaceSlug: ctx.workspaceSlug,
    filename: ctx.filename,
    userId: ctx.userId,
    chunksProcessed,
    totalChunks,
    silent: true,
  };

  if (typeof process.send === "function") {
    try {
      process.send(event);
    } catch {}
    return;
  }

  const { emitProgress } = require("../EmbeddingWorkerManager");
  emitProgress(ctx.workspaceSlug, event);
}

function humanFileSize(bytes, si = false, dp = 1) {
  const thresh = si ? 1000 : 1024;

  if (Math.abs(bytes) < thresh) {
    return bytes + " B";
  }

  const units = si
    ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
    : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
  let u = -1;
  const r = 10 ** dp;

  do {
    bytes /= thresh;
    ++u;
  } while (
    Math.round(Math.abs(bytes) * r) / r >= thresh &&
    u < units.length - 1
  );

  return bytes.toFixed(dp) + " " + units[u];
}

/**
 * Async wrapper that resolves the correct LLM connector for a workspace,
 * handling the anythingllm-router provider transparently. Callers get back
 * a ready-to-use connector without needing to know about routing internals.
 *
 * @param {Object} opts
 * @param {Object} opts.workspace - The workspace record (required)
 * @param {string} [opts.prompt] - The current user prompt
 * @param {Object|null} [opts.user] - The user object
 * @param {Object|null} [opts.thread] - The thread object
 * @param {Object[]} [opts.attachments] - Attachments array
 * @param {Object|null} [opts.chatHistoryOverride] - Pre-fetched chat history
 * @param {number|null} [opts.messageCountOverride] - Override for message count
 * @param {string|null} [opts.apiSessionId] - API session scope
 * @returns {Promise<{connector: BaseLLMProvider, routingMetadata: Object|null, prefetchedContext: Object|null}>}
 */
async function resolveProviderConnector({
  workspace,
  prompt = "",
  user = null,
  thread = null,
  attachments = [],
  chatHistoryOverride = null,
  messageCountOverride = null,
  apiSessionId = null,
}) {
  const effectiveProvider = workspace?.chatProvider || process.env.LLM_PROVIDER;

  if (effectiveProvider !== "anythingllm-router") {
    return {
      connector: getLLMProvider({
        provider: workspace?.chatProvider,
        model: workspace?.chatModel,
      }),
      routingMetadata: null,
      prefetchedContext: null,
    };
  }

  const { AnythingLLMModelRouter } = require("../AiProviders/modelRouter");
  const { ModelRouterService } = require("../router");

  const routerWorkspace = workspace?.router_id
    ? workspace
    : {
        ...workspace,
        router_id: process.env.MODEL_ROUTER_ID
          ? Number(process.env.MODEL_ROUTER_ID)
          : null,
      };

  const router = new AnythingLLMModelRouter(routerWorkspace);
  const ctx = await ModelRouterService.gatherRoutingContext({
    workspace,
    user,
    thread,
    message: prompt,
    chatHistoryOverride,
    messageCountOverride,
    apiSessionId,
  });

  await router.resolve(
    {
      prompt,
      conversationTokenCount: ctx.conversationTokenCount,
      conversationMessageCount: ctx.conversationMessageCount,
      attachments,
    },
    { user, thread }
  );

  return {
    connector: router.delegateProvider,
    routingMetadata: router.routingMetadata,
    prefetchedContext: ctx,
  };
}

/**
 * Strips thought/thinking tags from text (e.g., <thinking>...</thinking>)
 * Useful for cleaning LLM responses before sending notifications.
 * @param {string} text - The text to strip thoughts from.
 * @returns {string} - The text with thought tags and their content removed.
 */
const THOUGHT_KEYWORDS = ["thought", "thinking", "think", "thought_chain"];
const THOUGHT_REGEX_COMPLETE = new RegExp(
  THOUGHT_KEYWORDS.map(
    (keyword) =>
      `<${keyword}\\s*(?:[^>]*?)?\\s*>[\\s\\S]*?<\\/${keyword}\\s*(?:[^>]*?)?>`
  ).join("|"),
  "gi"
);

function stripThinkingFromText(text = "") {
  return text.replace(THOUGHT_REGEX_COMPLETE, "").trim();
}

module.exports = {
  getEmbeddingEngineSelection,
  maximumChunkLength,
  getVectorDbClass,
  getLLMProviderClass,
  getBaseLLMProviderModel,
  getLLMProvider,
  resolveProviderConnector,
  toChunks,
  humanFileSize,
  reportEmbeddingProgress,
  stripThinkingFromText,
};
