import { Injectable, OnModuleInit } from '@nestjs/common';
import { createLogger } from '@core/observability/logging/config/winston.config';
import { randomUUID } from 'crypto';
import type {
  ModelProvider,
  ProviderRequest,
  ProviderResponse,
  ProviderStreamChunk,
  ProviderToolCall,
} from './provider.types';

const logger = createLogger('QwenProvider');

/**
 * 通义千问 ModelProvider（PR3 真实接入，PRD §11.1 plan B 国内 LLM 适配）。
 *
 * 国内服务（阿里云百炼） → **不涉及数据出境合规**，可以 day-1 启用，
 * 无需等 Anthropic/OpenAI 的 PIPL/CAC 法务结论。
 *
 * 走 OpenAI SDK 兼容模式连 dashscope；环境变量：
 *   - DASHSCOPE_API_KEY    阿里云百炼 API Key（必须）
 *   - QWEN_MODEL           默认模型，缺省 qwen-plus
 *   - QWEN_BASE_URL        端点，缺省 https://dashscope.aliyuncs.com/compatible-mode/v1
 */
@Injectable()
export class QwenProvider implements ModelProvider, OnModuleInit {
  readonly name = 'qwen';
  // 阿里云百炼支持的常见 model alias；按"小→大"排，cost-budget 时 Registry resolver 取首位
  readonly supportedModels = [
    'qwen-turbo',
    'qwen-plus',
    'qwen-max',
    'qwen-max-longcontext',
  ] as const;

  private client: any = null;
  private defaultModel = 'qwen-plus';

  async onModuleInit() {
    const apiKey = process.env.DASHSCOPE_API_KEY;
    if (!apiKey) {
      logger.warn('DASHSCOPE_API_KEY 未配置 — Qwen provider unavailable');
      return;
    }
    this.defaultModel = process.env.QWEN_MODEL ?? 'qwen-plus';
    const baseURL =
      process.env.QWEN_BASE_URL ?? 'https://dashscope.aliyuncs.com/compatible-mode/v1';
    try {
      // eslint-disable-next-line @typescript-eslint/no-var-requires
      const OpenAI = require('openai').default;
      this.client = new OpenAI({ apiKey, baseURL });
      logger.log(`Qwen provider initialized (default model: ${this.defaultModel})`);
    } catch (err) {
      logger.error(`Failed to init Qwen provider: ${(err as Error).message}`);
    }
  }

  isAvailable(): boolean {
    return this.client !== null;
  }

  async invoke(request: ProviderRequest): Promise<ProviderResponse> {
    if (!this.client) {
      throw new Error('Qwen provider not initialized');
    }
    const model = request.model || this.defaultModel;
    const t0 = Date.now();
    try {
      const apiMessages = request.messages.map((m) => {
        // role='tool' 必须带 tool_call_id（OpenAI 协议）
        if (m.role === 'tool') {
          return {
            role: 'tool' as const,
            content: m.content,
            tool_call_id: m.toolCallId ?? 'unknown',
          };
        }
        // role='assistant' 发起 tool_calls 时把它带上
        if (m.role === 'assistant' && m.toolCalls?.length) {
          return {
            role: 'assistant' as const,
            content: m.content || '',
            tool_calls: m.toolCalls,
          };
        }
        return { role: m.role, content: m.content };
      });

      const response = await this.client.chat.completions.create({
        model,
        messages: apiMessages,
        temperature: request.temperature ?? 0.7,
        max_tokens: request.maxTokens ?? 2048,
        ...(request.tools && request.tools.length > 0 ? { tools: request.tools } : {}),
      });
      const choice = response.choices?.[0];
      const text = choice?.message?.content ?? '';
      const finishReason = choice?.finish_reason as string | undefined;
      const toolCalls = choice?.message?.tool_calls as
        | Array<{ id: string; type: 'function'; function: { name: string; arguments: string } }>
        | undefined;
      logger.log(
        `Qwen invoke ok: model=${model} finish=${finishReason} latency=${Date.now() - t0}ms tokens=${response.usage?.total_tokens ?? '?'}${toolCalls ? ` tool_calls=${toolCalls.length}` : ''}`,
      );
      return {
        id: response.id ?? randomUUID(),
        model: request.model,
        resolvedModel: response.model ?? model,
        text,
        stopReason: mapStopReason(finishReason),
        usage: {
          inputTokens: response.usage?.prompt_tokens ?? 0,
          outputTokens: response.usage?.completion_tokens ?? 0,
        },
        ...(toolCalls && toolCalls.length > 0 ? { toolCalls } : {}),
      };
    } catch (err) {
      logger.error(`Qwen invoke failed: ${(err as Error).message}`);
      throw err;
    }
  }

  async *invokeStream(request: ProviderRequest): AsyncGenerator<ProviderStreamChunk, void, void> {
    if (!this.client) throw new Error('Qwen provider not initialized');
    const model = request.model || this.defaultModel;
    const apiMessages = request.messages.map((m) => {
      if (m.role === 'tool') {
        return { role: 'tool' as const, content: m.content, tool_call_id: m.toolCallId ?? 'unknown' };
      }
      if (m.role === 'assistant' && m.toolCalls?.length) {
        return { role: 'assistant' as const, content: m.content || '', tool_calls: m.toolCalls };
      }
      return { role: m.role, content: m.content };
    });

    const stream = await this.client.chat.completions.create({
      model,
      messages: apiMessages,
      temperature: request.temperature ?? 0.7,
      max_tokens: request.maxTokens ?? 2048,
      stream: true,
      stream_options: { include_usage: true },
      ...(request.tools && request.tools.length > 0 ? { tools: request.tools } : {}),
    });

    let fullText = '';
    const toolCallBuilders = new Map<
      number,
      { id?: string; name?: string; args: string }
    >();
    let finalUsage = { inputTokens: 0, outputTokens: 0 };
    let finishReason: string | undefined;
    let resolvedModel: string | undefined;

    for await (const chunk of stream) {
      resolvedModel = resolvedModel ?? chunk.model;
      if (chunk.usage) {
        finalUsage = {
          inputTokens: chunk.usage.prompt_tokens ?? 0,
          outputTokens: chunk.usage.completion_tokens ?? 0,
        };
      }
      const choice = chunk.choices?.[0];
      if (!choice) continue;
      const delta = choice.delta;
      finishReason = choice.finish_reason ?? finishReason;

      if (delta?.content) {
        fullText += delta.content;
        yield { type: 'text_delta', text: delta.content };
      }
      if (delta?.tool_calls?.length) {
        for (const tcDelta of delta.tool_calls) {
          const idx = tcDelta.index ?? 0;
          const builder = toolCallBuilders.get(idx) ?? { args: '' };
          if (tcDelta.id) builder.id = tcDelta.id;
          if (tcDelta.function?.name) builder.name = tcDelta.function.name;
          if (tcDelta.function?.arguments) builder.args += tcDelta.function.arguments;
          toolCallBuilders.set(idx, builder);
          yield {
            type: 'tool_call_delta',
            index: idx,
            id: tcDelta.id,
            name: tcDelta.function?.name,
            argumentsDelta: tcDelta.function?.arguments,
          };
        }
      }
    }

    const toolCalls: ProviderToolCall[] | undefined =
      toolCallBuilders.size > 0
        ? [...toolCallBuilders.entries()]
            .sort(([a], [b]) => a - b)
            .map(([, b]) => ({
              id: b.id ?? '',
              type: 'function' as const,
              function: { name: b.name ?? '', arguments: b.args || '{}' },
            }))
        : undefined;

    yield {
      type: 'stop',
      stopReason: mapStopReason(finishReason),
      usage: finalUsage,
      toolCalls,
      text: fullText,
      resolvedModel,
    };
    logger.log(
      `Qwen stream done: model=${model} finish=${finishReason} chars=${fullText.length}${toolCalls ? ` tool_calls=${toolCalls.length}` : ''}`,
    );
  }
}

function mapStopReason(finish?: string): ProviderResponse['stopReason'] {
  switch (finish) {
    case 'stop':
      return 'end_turn';
    case 'length':
      return 'max_tokens';
    case 'tool_calls':
      return 'tool_use';
    default:
      return 'end_turn';
  }
}
