import { Injectable, Logger } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { AiConfigService } from '../ai-config/ai-config.service';
import { AiUsageService } from 'src/ai-usage/ai-usage.service';
import { esErrorParametroTokens, extraerMensajeError, mensajeAmigableIA } from '../shared/ai-error.util';
import type { ParteMensaje } from 'src/imagenes-ia/pipeline/tipos';
import { partesAAnthropic, partesAOpenAI } from './ai-provider.partes';

export interface AiCompletionResult {
  content: string;
  /** Tokens de entrada (prompt). */
  tokensIn: number;
  /** Tokens de salida (respuesta). */
  tokensOut: number;
  /** Suma in+out. Se mantiene por compatibilidad con callers existentes. */
  tokensUsados: number;
  modelo: string;
}

/** Metadatos para la medición de consumo. Opcional: si no se pasa, se registra igual con feature DESCONOCIDO. */
export interface AiCallMeta {
  feature?: string;
  usuarioId?: string | null;
  referenciaId?: string | null;
}

@Injectable()
export class AiProviderService {
  private readonly logger = new Logger(AiProviderService.name);

  constructor(
    private readonly aiConfigService: AiConfigService,
    private readonly usage: AiUsageService,
  ) {}

  async completar(
    empresaId: string,
    prompt: string,
    systemPrompt?: string,
    meta?: AiCallMeta,
  ): Promise<AiCompletionResult> {
    const config = await this.aiConfigService.getConfigEfectiva(empresaId);

    if (!config.api_key) {
      return {
        content: '[Sin API key configurada]',
        tokensIn: 0,
        tokensOut: 0,
        tokensUsados: 0,
        modelo: config.modelo ?? '',
      };
    }

    // Gate de cuota: corta antes de gastar si la empresa superó su tope mensual.
    await this.usage.verificarCuota(empresaId);

    const t0 = Date.now();
    try {
      let result: AiCompletionResult;
      switch (config.proveedor) {
        case 'anthropic':
          result = await this._completarAnthropic(config.api_key, config.modelo, prompt, systemPrompt);
          break;
        case 'openai':
          result = await this._completarOpenAI(config.api_key, config.modelo, prompt, systemPrompt);
          break;
        case 'deepseek':
          // DeepSeek es compatible con la API de OpenAI (base URL propia).
          result = await this._completarOpenAI(config.api_key, config.modelo || 'deepseek-chat', prompt, systemPrompt, 'https://api.deepseek.com');
          break;
        default:
          this.logger.warn(`Proveedor desconocido: ${config.proveedor}`);
          return {
            content: `[Proveedor ${config.proveedor} no soportado aún]`,
            tokensIn: 0,
            tokensOut: 0,
            tokensUsados: 0,
            modelo: config.modelo ?? '',
          };
      }

      await this.usage.registrar({
        empresa_id: empresaId,
        feature: meta?.feature ?? 'DESCONOCIDO',
        proveedor: config.proveedor,
        modelo: result.modelo,
        modelo_configurado: config.modelo,
        tokens_in: result.tokensIn,
        tokens_out: result.tokensOut,
        usuario_id: meta?.usuarioId ?? null,
        referencia_id: meta?.referenciaId ?? null,
        latencia_ms: Date.now() - t0,
        credencial_propia: config.credencial_propia,
        ok: true,
      });

      return result;
    } catch (error) {
      this.logger.error(`Error llamando al proveedor ${config.proveedor} para empresa ${empresaId}`, error);
      throw error;
    }
  }

  /**
   * Igual que completar() pero con un mensaje multimodal (texto + imágenes JPEG base64).
   * Soporta anthropic, openai y deepseek (deepseek-flash tiene visión; se desactiva el
   * modo thinking porque agota max_tokens razonando). Registra tokens en ai_consumo.
   */
  async completarConImagenes(
    empresaId: string,
    systemPrompt: string,
    partes: ParteMensaje[],
    meta?: AiCallMeta,
    maxTokens = 2000,
  ): Promise<AiCompletionResult> {
    const config = await this.aiConfigService.getConfigEfectiva(empresaId);
    if (!config.api_key) {
      return { content: '[Sin API key configurada]', tokensIn: 0, tokensOut: 0, tokensUsados: 0, modelo: config.modelo ?? '' };
    }
    await this.usage.verificarCuota(empresaId);

    const t0 = Date.now();
    let result: AiCompletionResult;
    if (config.proveedor === 'anthropic') {
      const client = new Anthropic({ apiKey: config.api_key });
      const response = await client.messages.create({
        model: config.modelo || 'claude-haiku-4-5-20251001',
        max_tokens: maxTokens,
        system: systemPrompt,
        messages: [{ role: 'user', content: partesAAnthropic(partes) }],
      });
      const content = response.content.map((b) => (b.type === 'text' ? b.text : '')).join('');
      const tokensIn = response.usage?.input_tokens ?? 0;
      const tokensOut = response.usage?.output_tokens ?? 0;
      result = { content, tokensIn, tokensOut, tokensUsados: tokensIn + tokensOut, modelo: response.model };
    } else if (config.proveedor === 'openai' || config.proveedor === 'deepseek') {
      const esDeepSeek = config.proveedor === 'deepseek';
      const client = new OpenAI(esDeepSeek ? { apiKey: config.api_key, baseURL: 'https://api.deepseek.com' } : { apiKey: config.api_key });
      const extra: Record<string, unknown> = esDeepSeek ? { thinking: { type: 'disabled' } } : {};
      const messages = [
        { role: 'system' as const, content: systemPrompt },
        { role: 'user' as const, content: partesAOpenAI(partes) },
      ];
      const params = (tokens: Record<string, number>) =>
        ({ model: config.modelo || (esDeepSeek ? 'deepseek-flash' : 'gpt-4o-mini'), messages, ...tokens, ...extra }) as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming;
      let response;
      try {
        response = await client.chat.completions.create(params({ max_completion_tokens: maxTokens }));
      } catch (err) {
        if (!esErrorParametroTokens(err)) throw err;
        response = await client.chat.completions.create(params({ max_tokens: maxTokens }));
      }
      const content = response.choices[0]?.message?.content ?? '';
      const tokensIn = response.usage?.prompt_tokens ?? 0;
      const tokensOut = response.usage?.completion_tokens ?? 0;
      result = { content, tokensIn, tokensOut, tokensUsados: tokensIn + tokensOut, modelo: response.model };
    } else {
      throw new Error(`El proveedor ${config.proveedor} no soporta imágenes`);
    }

    await this.usage.registrar({
      empresa_id: empresaId,
      feature: meta?.feature ?? 'DESCONOCIDO',
      proveedor: config.proveedor,
      modelo: result.modelo,
      modelo_configurado: config.modelo,
      tokens_in: result.tokensIn,
      tokens_out: result.tokensOut,
      usuario_id: meta?.usuarioId ?? null,
      referencia_id: meta?.referenciaId ?? null,
      latencia_ms: Date.now() - t0,
      credencial_propia: config.credencial_propia,
      ok: true,
    });
    return result;
  }

  private async _completarAnthropic(
    apiKey: string,
    modelo: string,
    prompt: string,
    systemPrompt?: string,
  ): Promise<AiCompletionResult> {
    const client = new Anthropic({ apiKey });

    const response = await client.messages.create({
      model: modelo || 'claude-haiku-4-5-20251001',
      max_tokens: 2048,
      system: systemPrompt || 'Sos un analista financiero experto en ERP. Respondé en español, de forma clara y directa.',
      messages: [{ role: 'user', content: prompt }],
    });

    const content = response.content[0].type === 'text' ? response.content[0].text : '';
    const tokensIn = response.usage?.input_tokens ?? 0;
    const tokensOut = response.usage?.output_tokens ?? 0;

    return { content, tokensIn, tokensOut, tokensUsados: tokensIn + tokensOut, modelo: response.model };
  }

  private async _completarOpenAI(
    apiKey: string,
    modelo: string,
    prompt: string,
    systemPrompt?: string,
    baseURL?: string,
  ): Promise<AiCompletionResult> {
    const client = new OpenAI(baseURL ? { apiKey, baseURL } : { apiKey });
    const modelName = modelo || 'gpt-4o-mini';
    const messages = [
      {
        role: 'system' as const,
        content: systemPrompt || 'Sos un analista financiero experto en ERP. Respondé en español, de forma clara y directa.',
      },
      { role: 'user' as const, content: prompt },
    ];

    // DeepSeek v4 razona por defecto (modo "thinking"): para una tarea mecánica como
    // extraer movimientos eso multiplica la latencia (~4x) y puede agotar el presupuesto
    // de tokens en razonamiento, truncando la respuesta. Lo desactivamos.
    const esDeepSeek = !!baseURL && /deepseek/i.test(baseURL);
    const extra: Record<string, unknown> = esDeepSeek ? { thinking: { type: 'disabled' } } : {};

    // Los modelos nuevos de OpenAI (gpt-5, o1, o3, …) RECHAZAN `max_tokens` y exigen
    // `max_completion_tokens`. Usamos el parámetro vigente y, solo si un modelo viejo no lo
    // acepta, reintentamos con `max_tokens`. Así funciona con modelos nuevos y viejos.
    const params = (tokens: Record<string, number>) =>
      ({ model: modelName, messages, ...tokens, ...extra }) as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming;
    let response;
    try {
      response = await client.chat.completions.create(params({ max_completion_tokens: 2048 }));
    } catch (err) {
      if (esErrorParametroTokens(err)) {
        response = await client.chat.completions.create(params({ max_tokens: 2048 }));
      } else {
        throw err;
      }
    }

    const content = response.choices[0]?.message?.content ?? '';
    const tokensIn = response.usage?.prompt_tokens ?? 0;
    const tokensOut = response.usage?.completion_tokens ?? 0;

    return { content, tokensIn, tokensOut, tokensUsados: tokensIn + tokensOut, modelo: response.model };
  }

  async testConexion(empresaId: string): Promise<{ ok: boolean; modelo?: string; error?: string }> {
    try {
      const result = await this.completar(
        empresaId,
        'Responde solo con la palabra: OK',
        'Eres un asistente de prueba.',
        { feature: 'TEST' },
      );
      return { ok: result.content.trim().includes('OK'), modelo: result.content };
    } catch (error) {
      return { ok: false, error: mensajeAmigableIA(extraerMensajeError(error)) };
    }
  }
}
