import type {
  EmbeddingModel,
  ModelInputModality,
  SupportedProvider,
} from "@archestra/shared";
import { providerRequiresPerUserCredential } from "@archestra/shared";
import { createDirectLLMModel, type LLMModel } from "@/clients/llm-client";
import { getProviderConfiguredBaseUrl } from "@/config";
import logger from "@/logging";
import {
  LlmProviderApiKeyModel,
  ModelModel,
  OrganizationModel,
} from "@/models";
import { getSecretValueForLlmProviderApiKey } from "@/secrets-manager";

export interface EmbeddingConfig {
  apiKey: string;
  baseUrl: string | null;
  model: EmbeddingModel;
  dimensions: number;
  provider: SupportedProvider;
  /** Input modalities supported by this embedding model (e.g. ["text", "image"]).
   * Null when no matching record exists in the models table (e.g. the model name
   * hasn't been synced from models.dev yet, or no model is configured). */
  inputModalities: ModelInputModality[] | null;
}

interface RerankerConfig {
  llmModel: LLMModel;
  modelName: string;
  provider: SupportedProvider;
}

/**
 * Resolve the embedding configuration for an organization.
 * Returns null if the organization doesn't have an embedding API key configured.
 */
export async function resolveEmbeddingConfig(
  organizationId: string,
): Promise<EmbeddingConfig | null> {
  const org = await OrganizationModel.getById(organizationId);
  if (!org?.embeddingChatApiKeyId || !org.embeddingModel) {
    return null;
  }

  const resolved = await resolveApiKeyFromChatApiKey(org.embeddingChatApiKeyId);
  if (!resolved) {
    logger.warn(
      { organizationId, chatApiKeyId: org.embeddingChatApiKeyId },
      "[KB] Embedding API key configured but secret could not be resolved",
    );
    return null;
  }

  const model = await ModelModel.findByProviderAndModelId(
    resolved.provider,
    org.embeddingModel,
  );

  return {
    apiKey: resolved.apiKey,
    baseUrl: resolved.baseUrl,
    model: org.embeddingModel,
    /**
     * TODO: Temporary transition. Prefer per-model dimensions. Fall back to the deprecated org-level
     * setting during the rollout, then to the historical 1536 default.
     */
    dimensions: model?.embeddingDimensions ?? org.embeddingDimensions ?? 1536,
    provider: resolved.provider,
    inputModalities: model?.inputModalities ?? null,
  };
}

/**
 * Resolve the reranker configuration for an organization.
 * Returns null if the organization doesn't have a reranker API key configured.
 */
export async function resolveRerankerConfig(
  organizationId: string,
): Promise<RerankerConfig | null> {
  const org = await OrganizationModel.getById(organizationId);
  if (!org?.rerankerChatApiKeyId || !org.rerankerModel) {
    return null;
  }

  const resolved = await resolveApiKeyFromChatApiKey(org.rerankerChatApiKeyId);
  if (!resolved) {
    logger.warn(
      { organizationId, chatApiKeyId: org.rerankerChatApiKeyId },
      "[KB] Reranker API key configured but secret could not be resolved",
    );
    return null;
  }

  const modelName = org.rerankerModel;

  return {
    llmModel: createDirectLLMModel({
      provider: resolved.provider,
      apiKey: resolved.apiKey,
      modelName,
      baseUrl: resolved.baseUrl,
    }),
    modelName,
    provider: resolved.provider,
  };
}

/**
 * Get the default organization and check if it has embedding configured.
 * Used by the embedding cron which runs without request context.
 */
export async function getDefaultOrgEmbeddingConfig(): Promise<{
  organizationId: string;
  config: EmbeddingConfig;
} | null> {
  const org = await OrganizationModel.getFirst();
  if (!org) return null;

  const embeddingConfig = await resolveEmbeddingConfig(org.id);
  if (!embeddingConfig) return null;

  return { organizationId: org.id, config: embeddingConfig };
}

/**
 * Resolve the actual API key, base URL, and provider from a chat API key ID.
 * Used by embedding config resolution and test-embedding endpoint.
 */
export async function resolveApiKeyFromChatApiKey(
  chatApiKeyId: string,
): Promise<{
  apiKey: string;
  baseUrl: string | null;
  provider: SupportedProvider;
} | null> {
  const chatApiKey = await LlmProviderApiKeyModel.findById(chatApiKeyId);
  if (!chatApiKey) return null;

  // Knowledge-base embedding/reranking is a system operation with no acting
  // user, so a per-user provider (GitHub Copilot) can't be used here — its
  // token belongs to one person. (Copilot also exposes no embeddings.)
  if (providerRequiresPerUserCredential(chatApiKey.provider)) return null;

  // Fall back to the provider's configured (env-aware) base URL when none is set
  // on the key — the same source chat and model-sync use, so self-hosted
  // providers (Ollama/vLLM) resolve the deployment's host, not a hardcoded default.
  const baseUrl =
    chatApiKey.inferenceBaseUrl ||
    chatApiKey.baseUrl ||
    getProviderConfiguredBaseUrl(chatApiKey.provider) ||
    null;

  // Providers like Ollama don't require an API key — use a placeholder
  // since the OpenAI SDK requires a non-empty apiKey string
  if (!chatApiKey.secretId) {
    return {
      apiKey: "unused",
      baseUrl,
      provider: chatApiKey.provider,
    };
  }

  const apiKey = await getSecretValueForLlmProviderApiKey(chatApiKey.secretId);
  if (!apiKey) return null;

  return { apiKey, baseUrl, provider: chatApiKey.provider };
}
