/**
 * automation/core/llm-client.ts
 *
 * 统一的 OpenAI 兼容客户端工厂。两种构建模式：
 *
 *   - createLlmClient(options)        — 按阶段解析 Provider 配置；缺配置时抛错。
 *   - createVisionClientFromEnv(opts) — 仅读 .env；缺配置时返回 null（用于
 *                                       视觉降级路径）。
 *
 * 注：由 row-pipeline 通过 `node`（而非 tsx）拉起的 Playwright 子进程
 * 使用 automation/core/llm-env.js 中的轻量 env resolver，避免 JS 文件
 * import 本 .ts 模块。
 */

import OpenAI from "openai";

import { getEnv, type AutomationEnv, type LlmStage } from "./env.ts";

export interface ResolvedLlmRuntimeConfig {
  baseURL: string;
  apiKey: string;
  model: string;
  provider: string;
}

export interface LlmClientOptions {
  /** LLM 阶段；用于解析 provider/baseURL/apiKey/model */
  stage?: LlmStage;
  /** OpenAI SDK maxRetries (默认 0) */
  maxRetries?: number;
  /** 请求超时毫秒数（不传则使用 SDK 默认） */
  timeout?: number;
}

export const DEFAULT_STAGE_LLM_MODELS = {
  document_ocr: "mimo-v2.5",
  document_extraction: "mimo-v2.5",
  standardization: "deepseek-v4-pro",
  keyword_semantic: "deepseek-v4-flash",
  search_vision: "gemini-2.5-pro-preview-p",
  fixed_search_batch: "glm-4-plus",
  chatbot_intent: "deepseek-v4-flash",
} as const satisfies Record<LlmStage, string>;

type ResolveStageLlmModelOptions = {
  env?: AutomationEnv;
  fallbackModel?: string | null;
};

type DbLlmProviderConfig = {
  provider_key: string;
  base_url: string;
  api_key: string;
  is_active?: boolean;
};

type DbLlmStageRoute = {
  stage: string;
  provider_key: string;
  model: string;
};

function parseOptionalString(value: unknown): string | undefined {
  if (typeof value !== "string") return undefined;
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

function trimTrailingSlash(value: string): string {
  return value.replace(/\/+$/, "");
}

function normalizeKey(value: unknown): string | undefined {
  return parseOptionalString(value)?.toLowerCase();
}

export function resolveStageLlmModel(
  stage: LlmStage,
  fallbackModel?: string | null,
): string;
export function resolveStageLlmModel(
  stage: LlmStage,
  options?: ResolveStageLlmModelOptions,
): string;
export function resolveStageLlmModel(
  stage: LlmStage,
  fallbackOrOptions: string | null | ResolveStageLlmModelOptions = {},
): string {
  const options = typeof fallbackOrOptions === "object" && fallbackOrOptions !== null
    ? fallbackOrOptions
    : { fallbackModel: fallbackOrOptions };
  const env = options.env ?? getEnv();

  return parseOptionalString(env.llmStageModels[stage])
    ?? parseOptionalString(options.fallbackModel)
    ?? parseOptionalString(env.llmModel)
    ?? DEFAULT_STAGE_LLM_MODELS[stage];
}

export function resolveLlmProvider(config: {
  provider?: string | null;
  baseURL?: string | null;
  model?: string | null;
}): string {
  const baseURL = parseOptionalString(config.baseURL)?.toLowerCase() ?? "";
  if (baseURL.includes("api.deepseek.com")) {
    return "deepseek";
  }
  if (baseURL.includes("xiaomimimo.com")) {
    return "mimo";
  }

  const provider = parseOptionalString(config.provider)?.toLowerCase();
  if (provider === "deepseek" || provider?.includes("deepseek")) {
    return "deepseek";
  }
  if (provider === "glm" || provider === "bigmodel" || provider?.includes("bigmodel")) {
    return "glm";
  }
  if (provider === "mimo" || provider === "xiaomi" || provider?.includes("xiaomi")) {
    return "mimo";
  }

  const model = parseOptionalString(config.model)?.toLowerCase() ?? "";
  if (model.startsWith("deepseek")) {
    return "deepseek";
  }
  if (model.startsWith("glm-")) {
    return "glm";
  }
  if (model.startsWith("mimo-")) {
    return "mimo";
  }

  return provider ?? "env";
}

/**
 * 解析 LLM 运行配置：优先阶段 env 路由，其次 Supabase 多 Provider 配置，
 * 最后回退到 legacy 全局 env。旧单 active llm_config 不再参与运行时解析。
 */
async function loadDbMultiProviderConfig(): Promise<{
  providers: Map<string, DbLlmProviderConfig>;
  routes: Map<string, DbLlmStageRoute>;
}> {
  try {
    const db = await import("../internal/db.ts");
    const [providerRows, routeRows] = await Promise.all([
      typeof db.listActiveLlmProviderConfigs === "function"
        ? db.listActiveLlmProviderConfigs()
        : Promise.resolve([]),
      typeof db.listLlmStageRoutes === "function"
        ? db.listLlmStageRoutes()
        : Promise.resolve([]),
    ]);
    return {
      providers: new Map(
        (providerRows as DbLlmProviderConfig[])
          .map((row) => [normalizeKey(row.provider_key), row] as const)
          .filter((entry): entry is [string, DbLlmProviderConfig] => Boolean(entry[0])),
      ),
      routes: new Map(
        (routeRows as DbLlmStageRoute[])
          .map((row) => [normalizeKey(row.stage), row] as const)
          .filter((entry): entry is [string, DbLlmStageRoute] => Boolean(entry[0])),
      ),
    };
  } catch (err) {
    console.warn("[LLM] 从多 provider 数据库配置加载失败，回退到环境变量:", err);
    return { providers: new Map(), routes: new Map() };
  }
}

function buildLegacyRuntimeConfig(stage: LlmStage, env: AutomationEnv): ResolvedLlmRuntimeConfig | null {
  if (!env.llmBaseUrl || !env.llmApiKey) {
    return null;
  }
  const model = resolveStageLlmModel(stage, { env, fallbackModel: env.llmModel });
  const provider = resolveLlmProvider({
    provider: env.llmProvider ?? "env",
    baseURL: env.llmBaseUrl,
    model,
  });
  return {
    baseURL: trimTrailingSlash(env.llmBaseUrl),
    apiKey: env.llmApiKey,
    model,
    provider,
  };
}

function missingStageRuntimeError(stage: LlmStage, model: string) {
  const envName = stage.toUpperCase();
  const stageLabel: Record<LlmStage, string> = {
    document_ocr: "文档 OCR",
    document_extraction: "文档提取",
    standardization: "标准化",
    keyword_semantic: "关键词语义",
    search_vision: "搜索视觉",
    fixed_search_batch: "固定搜索 Batch",
    chatbot_intent: "客服意图",
  };
  return new Error(
    `${stageLabel[stage]}阶段缺少 LLM provider 配置。请设置 ${envName}_LLM_PROVIDER 并配置对应 *_LLM_BASE_URL/*_LLM_API_KEY，` +
      `或在管理面板配置阶段路由，或使用 legacy LLM_BASE_URL/LLM_API_KEY fallback。当前模型=${model}`,
  );
}

export async function resolveLlmRuntimeConfig(stage: LlmStage = "standardization"): Promise<ResolvedLlmRuntimeConfig> {
  const env = getEnv();
  const model = resolveStageLlmModel(stage, { env });
  const envProviderKey = normalizeKey(env.llmStageProviders[stage]);
  if (envProviderKey) {
    const credential = env.llmProviderCredentials[envProviderKey];
    if (!credential?.baseURL || !credential.apiKey) {
      throw new Error(
        `${stage} 配置了 provider=${envProviderKey}，但缺少 ${envProviderKey.toUpperCase()}_LLM_BASE_URL 或 ${envProviderKey.toUpperCase()}_LLM_API_KEY。`,
      );
    }
    return {
      provider: resolveLlmProvider({ provider: envProviderKey, baseURL: credential.baseURL, model }),
      baseURL: trimTrailingSlash(credential.baseURL),
      apiKey: credential.apiKey,
      model,
    };
  }

  const dbConfig = await loadDbMultiProviderConfig();
  const dbRoute = dbConfig.routes.get(stage);
  const dbProviderKey = normalizeKey(dbRoute?.provider_key);
  if (dbRoute && dbProviderKey) {
    const provider = dbConfig.providers.get(dbProviderKey);
    if (!provider?.base_url || !provider.api_key) {
      throw new Error(`阶段 ${stage} 的数据库 provider=${dbProviderKey} 缺少 base_url 或 api_key。`);
    }
    return {
      provider: resolveLlmProvider({ provider: dbProviderKey, baseURL: provider.base_url, model: dbRoute.model }),
      baseURL: trimTrailingSlash(provider.base_url),
      apiKey: provider.api_key,
      model: parseOptionalString(dbRoute.model) ?? model,
    };
  }

  const legacy = buildLegacyRuntimeConfig(stage, env);
  if (legacy) {
    return legacy;
  }

  throw missingStageRuntimeError(stage, model);
}

function buildClientOptions(
  config: { baseURL: string; apiKey: string },
  options: LlmClientOptions,
): ConstructorParameters<typeof OpenAI>[0] {
  const clientOptions: ConstructorParameters<typeof OpenAI>[0] = {
    baseURL: config.baseURL,
    apiKey: config.apiKey,
    maxRetries: options.maxRetries ?? 0,
  };
  if (options.timeout !== undefined) {
    clientOptions.timeout = options.timeout;
  }
  return clientOptions;
}

function createMimoChatCompletionClient(
  config: ResolvedLlmRuntimeConfig,
  options: LlmClientOptions,
): OpenAI {
  const endpoint = `${trimTrailingSlash(config.baseURL)}/chat/completions`;
  const timeoutSignal = options.timeout === undefined
    ? undefined
    : AbortSignal.timeout(options.timeout);

  return {
    chat: {
      completions: {
        create: async (request: unknown) => {
          const response = await fetch(endpoint, {
            method: "POST",
            headers: {
              "api-key": config.apiKey,
              "Content-Type": "application/json",
            },
            body: JSON.stringify(request),
            ...(timeoutSignal ? { signal: timeoutSignal } : {}),
          });
          const responseText = await response.text();
          if (!response.ok) {
            const error = new Error(`MiMo API failed with ${response.status}: ${responseText}`);
            (error as { status?: number }).status = response.status;
            throw error;
          }
          return JSON.parse(responseText);
        },
      },
    },
  } as unknown as OpenAI;
}

/**
 * 构建一个 DB-aware 的 OpenAI 兼容客户端。
 *
 * 调用方负责自己的缓存策略：本工厂每次返回新实例。需要长期复用的
 * 调用点（如 standardization-llm 的 _client 单例）请在外层做记忆化。
 */
export async function createLlmClient(
  options: LlmClientOptions = {},
): Promise<{ client: OpenAI; config: ResolvedLlmRuntimeConfig }> {
  const config = await resolveLlmRuntimeConfig(options.stage ?? "standardization");
  const client = config.provider === "mimo"
    ? createMimoChatCompletionClient(config, options)
    : new OpenAI(buildClientOptions(config, options));
  return { client, config };
}

/**
 * 仅从 .env 构建一个视觉/降级用 OpenAI 客户端。
 * LLM_BASE_URL 或 LLM_API_KEY 缺失时返回 null，方便调用方做 graceful degradation。
 */
export function createVisionClientFromEnv(
  options: LlmClientOptions = {},
): OpenAI | null {
  const env = getEnv();
  const stage = options.stage ?? "search_vision";
  const providerKey = normalizeKey(env.llmStageProviders[stage]);
  const credential = providerKey ? env.llmProviderCredentials[providerKey] : undefined;
  const baseURL = credential?.baseURL ?? env.llmBaseUrl;
  const apiKey = credential?.apiKey ?? env.llmApiKey;
  const model = resolveStageLlmModel(stage, { env, fallbackModel: env.llmModel });
  if (!baseURL || !apiKey) {
    return null;
  }
  const provider = resolveLlmProvider({ provider: providerKey ?? env.llmProvider, baseURL, model });
  if (provider === "mimo") {
    return createMimoChatCompletionClient({
      baseURL: trimTrailingSlash(baseURL),
      apiKey,
      model,
      provider,
    }, options);
  }
  return new OpenAI(
    buildClientOptions({
      baseURL: trimTrailingSlash(baseURL),
      apiKey,
    }, options),
  );
}
