import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createDeepSeek, type DeepSeekProvider } from '@ai-sdk/deepseek';
import { createGoogleGenerativeAI, type GoogleGenerativeAIProvider } from '@ai-sdk/google';
import { createOpenAICompatible, type OpenAICompatibleProvider } from '@ai-sdk/openai-compatible';
import type { LanguageModel } from 'ai';
import type { AppConfig } from '../../../config/configuration';

/**
 * AiProviderService — 模型工厂。
 *
 * harness 原则:AiCall 只声明 modelId 字符串,provider 决定怎么实例化。
 * 这里集中处理 vendor 路由(deepseek / gemini;以后加 qwen 时本类内部扩 switch)。
 *
 * 设计选择:
 *   - DeepSeek 走官方 SDK @ai-sdk/deepseek(国内 endpoint,合规)
 *   - Gemini 走 @ai-sdk/google(generativelanguage.googleapis.com;海外需可达,可设 baseURL 走代理)
 *   - 不走 Vercel AI Gateway(美国基建,数据出境)
 *   - modelId 字符串直传,不硬编码 enum;前端可只传逻辑键 "deepseek" / "gemini",
 *     由本类解析到 config 配的具体型号(单一真源在后端 env)
 */
@Injectable()
export class AiProviderService {
  private readonly logger = new Logger(AiProviderService.name);
  private readonly deepseek: DeepSeekProvider;
  private readonly google: GoogleGenerativeAIProvider;
  private readonly qwen: OpenAICompatibleProvider;
  private readonly defaultModel: string;
  private readonly geminiDefaultModel: string;
  private readonly qwenDefaultModel: string;

  constructor(private readonly config: ConfigService<AppConfig, true>) {
    const ai = this.config.get('ai', { infer: true });
    this.defaultModel = ai.defaultModel;
    this.geminiDefaultModel = ai.geminiDefaultModel;
    this.qwenDefaultModel = ai.qwenDefaultModel;

    if (!ai.deepseekApiKey) {
      this.logger.warn(
        'DEEPSEEK_API_KEY 未设置 — DeepSeek 调用会失败。开发环境到 https://platform.deepseek.com/api_keys 申请',
      );
    }
    this.deepseek = createDeepSeek({ apiKey: ai.deepseekApiKey, baseURL: ai.deepseekBaseUrl });

    if (!ai.geminiApiKey) {
      this.logger.warn('GEMINI_API_KEY 未设置 — Gemini 调用会失败。到 https://aistudio.google.com/apikey 申请');
    }
    this.google = createGoogleGenerativeAI({
      apiKey: ai.geminiApiKey,
      // baseURL 留空 → SDK 默认 endpoint;设了走代理 / 镜像
      ...(ai.geminiBaseUrl ? { baseURL: ai.geminiBaseUrl } : {}),
    });

    if (!ai.qwenApiKey) {
      this.logger.warn('QWEN_API_KEY(或 DASHSCOPE_API_KEY)未设置 — Qwen 调用会失败。到阿里云百炼 dashscope 申请');
    }
    // 通义千问走 DashScope OpenAI-兼容端点(compatible-mode)。
    // ⚠️ 实测结论(2026-06,qwen3.7-max):
    //   - DashScope 的 response_format=json_schema **不被强制执行** —— 即便 strict:true 也返 200 但 key 全自己编
    //     (拿 .describe() 中文当 key)→ AI SDK 用 Zod 一校验必 fail。所以 supportsStructuredOutputs 关掉。
    //   - 但 response_format=json_object **是认的**(保证纯 JSON、不裹 markdown);配合把"英文 key 骨架"
    //     注入到 prompt(见 ai-call-runner.ts withQwenStructuredHint),qwen 才会用正确的 key。
    //   - DashScope 用 json_object 时硬性要求 messages 含 "json" 字样(key 骨架文案已含"JSON")。
    //   - qwen3.7-max 是推理模型:流式下 thinking 会污染 content → enable_thinking:false 关掉。
    //   - 这两个参数 SDK 都不直接支持,统一用 fetch 中间件改 body 注入。
    this.qwen = createOpenAICompatible({
      name: 'qwen',
      apiKey: ai.qwenApiKey,
      baseURL: ai.qwenBaseUrl,
      supportsStructuredOutputs: false,
      fetch: (async (url: string | URL | Request, options?: RequestInit) => {
        if (options?.body && typeof options.body === 'string') {
          try {
            const b = JSON.parse(options.body) as Record<string, unknown>;
            // ⚠️ 仅"非工具调用"才强制 json_object(结构化输出框架 generateObject 用 json mode、无 tools)。
            //   tool-calling / agent(streamText + tools)不能用 JSON mode:① 与工具调用互斥
            //   ② DashScope 还会硬性要求 messages 含 "json" 字样 → 400。带 tools 时跳过注入。
            const hasTools = Array.isArray(b.tools) && b.tools.length > 0;
            /**
             * 🔴 2026-08-14：`enable_thinking = false` 由**无条件**改成**只在 json mode 那条路**。
             *
             * 上面那条实测（2026-06，qwen3.7-max：流式下 thinking 污染 content）跑的是
             *   **结构化输出**那条路 —— 而 agent 这条路从来没重新评估过。
             * ⚠️ 证据就在旁边三行：`response_format` 的注入判了 `hasTools`，`enable_thinking` **没判**。
             *   于是这几天拿 qwen 跟 DeepSeek 比"要不要边写边调工具"，比的其实是
             *   **「便宜档 + 会思考」对「最高档 + 不许思考」**，比较本身不成立。
             * ⚠️ agent 路开思考更慢、也更贵（reasoning 计入 output token），
             *   而 `qwen3.8-max` 在 configuration 里**还没有单价** —— 账目会更不准，见那边的 🔴。
             * ⛔ 别改回无条件关：json mode 那条路仍然要关（那个污染是真的）。
             */
            if (!hasTools) b.enable_thinking = false;
            if (!hasTools && b.response_format === undefined) {
              b.response_format = { type: 'json_object' }; // DashScope 认 json_object(json_schema 不强制)
            }
            options = { ...options, body: JSON.stringify(b) };
          } catch {
            /* 非 JSON body 不动 */
          }
        }
        return fetch(url, options);
      }) as typeof fetch,
    });
  }

  /**
   * 解析 modelId → vendor 实例 + 规范化具体 modelId(落账 modelName / 计费 priceTable 用规范化后的)。
   * 命名约定:
   *   "deepseek-v4-pro" / "deepseek-v4-flash" / "deepseek"(裸键)→ deepseek
   *   "gemini-2.5-flash" / "gemini"(裸键)                      → gemini
   *   "qwen-max"(将来)                                         → qwen
   * 裸键(无 "-" 版本)→ 解析到 config 配的默认型号;带版本 → 原样直传 SDK。
   */
  resolve(modelId: string): { model: LanguageModel; provider: string; modelId: string } {
    if (modelId === 'deepseek' || modelId.startsWith('deepseek-')) {
      const canonical = modelId === 'deepseek' ? this.defaultModel : modelId;
      return { model: this.deepseek(canonical), provider: 'deepseek', modelId: canonical };
    }
    if (modelId === 'gemini' || modelId.startsWith('gemini-')) {
      const canonical = modelId === 'gemini' ? this.geminiDefaultModel : modelId;
      return { model: this.google(canonical), provider: 'gemini', modelId: canonical };
    }
    // qwen 前缀(含 qwen3.8-max / qwen-max / 裸键 qwen)→ DashScope 兼容端点
    if (modelId.startsWith('qwen')) {
      const canonical = modelId === 'qwen' ? this.qwenDefaultModel : modelId;
      return { model: this.qwen(canonical), provider: 'qwen', modelId: canonical };
    }
    throw new Error(`Unsupported model id: ${modelId}(支持 deepseek-* / gemini-* / qwen* 前缀或裸键)`);
  }

  getDefaultModelId(): string {
    return this.defaultModel;
  }
}
