import { BadRequestException, Injectable, Logger, Optional } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { AiUsageService } from 'src/ai-usage/ai-usage.service';

export interface CompletarParams {
  proveedor: string;
  modelo: string;
  apiKey?: string | null;
  baseUrl?: string | null;
  system?: string;
  prompt: string;
  historial?: Array<{ rol: 'user' | 'assistant'; contenido: string }>;
  max_tokens?: number;
  temperatura?: number;
  signal?: AbortSignal;
  // ── Medición de consumo (opcional). Si se pasa empresa_id, la llamada se
  //    registra en ai_consumo con estos metadatos. ──────────────────────────
  empresa_id?: string;
  feature?: string;
  usuario_id?: string | null;
  referencia_id?: string | null;
  credencial_propia?: boolean;
}

export interface CompletarResult {
  texto: string;
  tokens_in: number;
  tokens_out: number;
  modelo: string;
  latency_ms: number;
}

export interface EmbedParams {
  proveedor: string;
  modelo: string;
  apiKey?: string | null;
  baseUrl?: string | null;
  textos: string[];
  signal?: AbortSignal;
}

/**
 * Servicio independiente del Dashboard IA. Encapsula los 3 proveedores
 * soportados por Ayuda IA: anthropic / openai / ollama.
 *
 * - completar: chat completion
 * - embed:     vectores de embedding (1536 dims)
 */
@Injectable()
export class AyudaProviderService {
  private readonly logger = new Logger(AyudaProviderService.name);

  // Opcional para no romper usos standalone/tests que instancian el provider suelto.
  constructor(@Optional() private readonly usage?: AiUsageService) {}

  async completar(p: CompletarParams): Promise<CompletarResult> {
    // Gate de cuota (si tenemos empresa y servicio de medición): corta antes de gastar.
    if (this.usage && p.empresa_id) {
      await this.usage.verificarCuota(p.empresa_id);
    }
    const t0 = Date.now();
    try {
      let result: CompletarResult;
      switch (p.proveedor) {
        case 'anthropic':
          result = this.attachLatency(await this.anthropicCompletar(p), t0);
          break;
        case 'openai':
          result = this.attachLatency(await this.openaiCompletar(p), t0);
          break;
        case 'deepseek': {
          // DeepSeek = API compatible con OpenAI (base URL propia). El campo base_url
          // sirve para Ollama; si quedó uno viejo (p.ej. el localhost de Ollama) NO debe
          // secuestrar la llamada a DeepSeek. Solo se honra si apunta a DeepSeek.
          const dsBase = p.baseUrl && /deepseek\.com/i.test(p.baseUrl) ? p.baseUrl : 'https://api.deepseek.com';
          result = this.attachLatency(await this.openaiCompletar({ ...p, baseUrl: dsBase, modelo: p.modelo || 'deepseek-chat' }), t0);
          break;
        }
        case 'ollama':
          result = this.attachLatency(await this.ollamaCompletar(p), t0);
          break;
        default:
          throw new BadRequestException(`Proveedor desconocido: ${p.proveedor}`);
      }

      if (this.usage && p.empresa_id) {
        await this.usage.registrar({
          empresa_id: p.empresa_id,
          feature: p.feature ?? 'AYUDA',
          proveedor: p.proveedor,
          modelo: result.modelo,
          // Respaldo para encontrar la tarifa si la API responde con otro alias.
          modelo_configurado: p.modelo,
          tokens_in: result.tokens_in,
          tokens_out: result.tokens_out,
          usuario_id: p.usuario_id ?? null,
          referencia_id: p.referencia_id ?? null,
          latencia_ms: result.latency_ms,
          credencial_propia: p.credencial_propia ?? true,
          ok: true,
        });
      }

      return result;
    } catch (err) {
      this.logger.error(`completar(${p.proveedor}) falló: ${(err as Error).message}`);
      throw err;
    }
  }

  async embed(p: EmbedParams): Promise<number[][]> {
    switch (p.proveedor) {
      case 'openai':
        return this.openaiEmbed(p);
      case 'ollama':
        return this.ollamaEmbed(p);
      case 'anthropic':
        throw new BadRequestException(
          'Anthropic no soporta embeddings. Configurá proveedor_embeddings=openai u ollama.',
        );
      default:
        throw new BadRequestException(`Proveedor de embeddings desconocido: ${p.proveedor}`);
    }
  }

  // ---------- Anthropic ----------
  private async anthropicCompletar(p: CompletarParams): Promise<CompletarResult> {
    if (!p.apiKey) throw new BadRequestException('API key de Anthropic no configurada');
    const client = new Anthropic({ apiKey: p.apiKey });

    const messages: Anthropic.MessageParam[] = [];
    for (const h of p.historial ?? []) {
      messages.push({ role: h.rol === 'user' ? 'user' : 'assistant', content: h.contenido });
    }
    messages.push({ role: 'user', content: p.prompt });

    const resp = await client.messages.create(
      {
        model: p.modelo || 'claude-haiku-4-5-20251001',
        max_tokens: p.max_tokens ?? 800,
        temperature: p.temperatura ?? 0.2,
        system: p.system,
        messages,
      },
      { signal: p.signal },
    );

    const texto = resp.content[0]?.type === 'text' ? resp.content[0].text : '';
    return {
      texto,
      tokens_in: resp.usage?.input_tokens ?? 0,
      tokens_out: resp.usage?.output_tokens ?? 0,
      modelo: resp.model,
      latency_ms: 0,
    };
  }

  // ---------- OpenAI ----------
  private async openaiCompletar(p: CompletarParams): Promise<CompletarResult> {
    if (!p.apiKey) throw new BadRequestException('API key de OpenAI no configurada');
    const client = new OpenAI(p.baseUrl ? { apiKey: p.apiKey, baseURL: p.baseUrl } : { apiKey: p.apiKey });

    const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [];
    if (p.system) messages.push({ role: 'system', content: p.system });
    for (const h of p.historial ?? []) {
      messages.push({ role: h.rol === 'user' ? 'user' : 'assistant', content: h.contenido });
    }
    messages.push({ role: 'user', content: p.prompt });

    // DeepSeek v4 razona por defecto (modo "thinking"): multiplica la latencia y puede
    // agotar el presupuesto de tokens en razonamiento. Para el chat de ayuda lo desactivamos
    // (respuesta ~4x más rápida, sin truncar). Solo aplica a DeepSeek.
    const esDeepSeek = !!p.baseUrl && /deepseek/i.test(p.baseUrl);
    const extra: Record<string, unknown> = esDeepSeek ? { thinking: { type: 'disabled' } } : {};

    const resp = await client.chat.completions.create(
      {
        model: p.modelo || 'gpt-4o-mini',
        max_tokens: p.max_tokens ?? 800,
        temperature: p.temperatura ?? 0.2,
        messages,
        ...extra,
      } as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming,
      { signal: p.signal },
    );

    return {
      texto: resp.choices[0]?.message?.content ?? '',
      tokens_in: resp.usage?.prompt_tokens ?? 0,
      tokens_out: resp.usage?.completion_tokens ?? 0,
      modelo: resp.model,
      latency_ms: 0,
    };
  }

  private async openaiEmbed(p: EmbedParams): Promise<number[][]> {
    if (!p.apiKey) throw new BadRequestException('API key de OpenAI no configurada (embeddings)');
    const client = new OpenAI({ apiKey: p.apiKey });
    const resp = await client.embeddings.create(
      { model: p.modelo || 'text-embedding-3-small', input: p.textos },
      { signal: p.signal },
    );
    return resp.data.map((d) => d.embedding as number[]);
  }

  // ---------- Ollama ----------
  private async ollamaCompletar(p: CompletarParams): Promise<CompletarResult> {
    const baseUrl = this.validateOllamaBaseUrl(p.baseUrl);
    const messages: Array<{ role: string; content: string }> = [];
    if (p.system) messages.push({ role: 'system', content: p.system });
    for (const h of p.historial ?? []) {
      messages.push({ role: h.rol === 'user' ? 'user' : 'assistant', content: h.contenido });
    }
    messages.push({ role: 'user', content: p.prompt });

    const resp = await fetch(`${baseUrl}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        model: p.modelo || 'llama3.1',
        messages,
        stream: false,
        options: {
          num_predict: p.max_tokens ?? 800,
          temperature: p.temperatura ?? 0.2,
        },
      }),
      signal: p.signal,
    });

    if (!resp.ok) {
      const body = await resp.text();
      throw new BadRequestException(`Ollama respondió ${resp.status}: ${body}`);
    }

    const data = (await resp.json()) as {
      message?: { content?: string };
      model?: string;
      prompt_eval_count?: number;
      eval_count?: number;
    };

    return {
      texto: data.message?.content ?? '',
      tokens_in: data.prompt_eval_count ?? 0,
      tokens_out: data.eval_count ?? 0,
      modelo: data.model ?? p.modelo,
      latency_ms: 0,
    };
  }

  private async ollamaEmbed(p: EmbedParams): Promise<number[][]> {
    const baseUrl = this.validateOllamaBaseUrl(p.baseUrl);
    const vectores: number[][] = [];
    // Ollama /api/embeddings acepta uno por request
    for (const texto of p.textos) {
      const resp = await fetch(`${baseUrl}/api/embeddings`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ model: p.modelo || 'mxbai-embed-large', prompt: texto }),
        signal: p.signal,
      });
      if (!resp.ok) {
        const body = await resp.text();
        throw new BadRequestException(`Ollama embeddings respondió ${resp.status}: ${body}`);
      }
      const data = (await resp.json()) as { embedding?: number[] };
      if (!Array.isArray(data.embedding)) {
        throw new BadRequestException('Respuesta de Ollama no incluye embedding');
      }
      vectores.push(data.embedding);
    }
    return vectores;
  }

  /**
   * SSRF protection: solo permitir localhost / IPs privadas / dominios que
   * resuelvan a una IP privada. Para v1 hacemos una validación textual
   * estricta (sin DNS), suficiente para el caso de uso (cliente expone Ollama
   * en su LAN/localhost).
   */
  private validateOllamaBaseUrl(baseUrl?: string | null): string {
    if (!baseUrl) return 'http://127.0.0.1:11434';
    let u: URL;
    try {
      u = new URL(baseUrl);
    } catch {
      throw new BadRequestException('base_url inválida');
    }
    if (!['http:', 'https:'].includes(u.protocol)) {
      throw new BadRequestException('base_url debe ser http(s)');
    }
    const host = u.hostname.toLowerCase();
    const allowedLiterals = ['localhost', '127.0.0.1', '::1', '0.0.0.0'];
    const isPrivate =
      allowedLiterals.includes(host) ||
      /^10\./.test(host) ||
      /^192\.168\./.test(host) ||
      /^172\.(1[6-9]|2[0-9]|3[01])\./.test(host) ||
      /\.local$/.test(host) ||
      /\.lan$/.test(host) ||
      /\.internal$/.test(host);
    if (!isPrivate) {
      throw new BadRequestException(
        'base_url debe apuntar a un host privado/local (localhost, 10.x, 192.168.x, 172.16-31.x, *.local, *.lan, *.internal)',
      );
    }
    return baseUrl.replace(/\/$/, '');
  }

  private attachLatency(r: CompletarResult, t0: number): CompletarResult {
    r.latency_ms = Date.now() - t0;
    return r;
  }
}
