import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { UpsertAyudaConfigDto } from '../dto/upsert-ayuda-config.dto';
import { AyudaProviderService } from './ayuda-provider.service';
import { decryptKey, encryptKey } from './ayuda-encryption.helper';

export interface ConfigConKey {
  proveedor: string;
  modelo: string;
  modelo_embeddings: string;
  proveedor_embeddings: string;
  base_url: string | null;
  api_key: string | null;
  api_key_embeddings: string | null;
  max_tokens: number;
  temperatura: number;
  activo: boolean;
  // ── Re-ranker (v1.1) ────────────────────────────────────────────
  reranker_activo: boolean;
  reranker_proveedor: string | null;
  reranker_modelo: string | null;
  reranker_api_key: string | null;
  reranker_base_url: string | null;
  reranker_top_k: number;
  reranker_min_score: number;
}

const MODELOS_POR_PROVEEDOR: Record<string, { chat: string[]; embeddings: string[] }> = {
  anthropic: {
    chat: ['claude-opus-4-7', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'],
    embeddings: [],
  },
  openai: {
    chat: ['gpt-4o', 'gpt-4o-mini', 'gpt-4.1-mini'],
    embeddings: ['text-embedding-3-small', 'text-embedding-3-large'],
  },
  deepseek: {
    chat: ['deepseek-chat', 'deepseek-reasoner', 'deepseek-v4-flash', 'deepseek-v4-pro'],
    embeddings: [], // DeepSeek no ofrece embeddings — usar OpenAI/Ollama para esa función
  },
  ollama: {
    chat: ['llama3.1', 'llama3.2', 'qwen2.5', 'mistral'],
    embeddings: ['mxbai-embed-large', 'nomic-embed-text'],
  },
};

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly provider: AyudaProviderService,
  ) {}

  /**
   * Ayuda IA es global al holding: la config vive en una única fila ligada a la empresa holding.
   * Resuelve el empresa_id del holding (tabla empresas.type='holding').
   */
  private async resolverHoldingId(): Promise<string> {
    const holding = await this.prisma.empresas.findFirst({ where: { type: 'holding' } });
    if (!holding) {
      throw new NotFoundException(
        'No existe empresa de tipo "holding". La configuración de Ayuda IA es global y la gestiona el holding.',
      );
    }
    return holding.id;
  }

  /**
   * Verifica que el caller pertenezca a una empresa de tipo 'holding'.
   * Sólo el holding puede leer/escribir la config y disparar ingestas.
   */
  async assertHoldingCaller(callerEmpresaId: string): Promise<void> {
    const e = await this.prisma.empresas.findUnique({ where: { id: callerEmpresaId } });
    if (!e || e.type !== 'holding') {
      throw new ForbiddenException('Sólo el holding puede gestionar la configuración de Ayuda IA.');
    }
  }

  async upsertConfig(callerEmpresaId: string, dto: UpsertAyudaConfigDto) {
    await this.assertHoldingCaller(callerEmpresaId);
    const holdingId = await this.resolverHoldingId();

    const existing = await this.prisma.ayuda_ia_config.findUnique({ where: { empresa_id: holdingId } });

    let apiKeyEnc = existing?.api_key_encrypted ?? null;
    if (dto.api_key && dto.api_key.trim().length > 0) {
      apiKeyEnc = encryptKey(dto.api_key.trim());
    }

    let apiKeyEmbEnc = existing?.api_key_embeddings_encrypted ?? null;
    if (dto.api_key_embeddings && dto.api_key_embeddings.trim().length > 0) {
      apiKeyEmbEnc = encryptKey(dto.api_key_embeddings.trim());
    }

    let rerankerApiKeyEnc = existing?.reranker_api_key_encrypted ?? null;
    if (dto.reranker_api_key && dto.reranker_api_key.trim().length > 0) {
      rerankerApiKeyEnc = encryptKey(dto.reranker_api_key.trim());
    }

    // base_url solo es relevante para Ollama (LLM o embeddings). Con otros
    // proveedores (deepseek/openai/anthropic) no debe persistir un valor viejo
    // —p.ej. el localhost de Ollama— que después secuestre las llamadas.
    const usaOllama = dto.proveedor === 'ollama' || dto.proveedor_embeddings === 'ollama';
    const baseUrlNormalizado = usaOllama ? (dto.base_url ?? null) : null;

    const data = {
      empresa_id: holdingId,
      proveedor: dto.proveedor,
      modelo: dto.modelo,
      modelo_embeddings: dto.modelo_embeddings ?? null,
      proveedor_embeddings: dto.proveedor_embeddings ?? null,
      base_url: baseUrlNormalizado,
      api_key_encrypted: apiKeyEnc,
      api_key_embeddings_encrypted: apiKeyEmbEnc,
      activo: dto.activo ?? existing?.activo ?? false,
      max_tokens: dto.max_tokens ?? existing?.max_tokens ?? 800,
      temperatura: dto.temperatura ?? existing?.temperatura ?? 0.2,
      // ── Reranker ────────────────────────────────────────────
      reranker_proveedor: dto.reranker_proveedor ?? existing?.reranker_proveedor ?? null,
      reranker_modelo: dto.reranker_modelo ?? existing?.reranker_modelo ?? null,
      reranker_api_key_encrypted: rerankerApiKeyEnc,
      reranker_base_url: dto.reranker_base_url ?? existing?.reranker_base_url ?? null,
      reranker_top_k: dto.reranker_top_k ?? existing?.reranker_top_k ?? 5,
      reranker_min_score: dto.reranker_min_score ?? existing?.reranker_min_score ?? 0.30,
      reranker_activo: dto.reranker_activo ?? existing?.reranker_activo ?? false,
      updated_at: new Date(),
    };

    const saved = await this.prisma.ayuda_ia_config.upsert({
      where: { empresa_id: holdingId },
      update: data,
      create: data,
    });

    return this.sanitize(saved);
  }

  /**
   * Lectura de la config global. Cualquier empresa autenticada puede leerla
   * (sin la API key) para saber si el módulo está activo.
   */
  async getConfig() {
    const holdingId = await this.resolverHoldingId().catch(() => null);
    if (!holdingId) return null;
    const c = await this.prisma.ayuda_ia_config.findUnique({ where: { empresa_id: holdingId } });
    if (!c) return null;
    return this.sanitize(c);
  }

  /**
   * Devuelve la config con API key descifrada — uso interno (preguntar / embed).
   * No depende del empresaId del caller; la config es global del holding.
   */
  async getConfigConKey(): Promise<ConfigConKey> {
    const holdingId = await this.resolverHoldingId();
    const c = await this.prisma.ayuda_ia_config.findUnique({ where: { empresa_id: holdingId } });
    if (!c) throw new NotFoundException('Ayuda IA no está configurada. El holding debe configurarla.');
    if (!c.activo) throw new BadRequestException('Ayuda IA está desactivada por el holding.');

    const apiKey = c.api_key_encrypted ? decryptKey(c.api_key_encrypted) : null;
    const apiKeyEmb = c.api_key_embeddings_encrypted ? decryptKey(c.api_key_embeddings_encrypted) : null;
    const rerankerApiKey = c.reranker_api_key_encrypted ? decryptKey(c.reranker_api_key_encrypted) : null;

    return {
      proveedor: c.proveedor,
      modelo: c.modelo,
      modelo_embeddings: c.modelo_embeddings || this.defaultEmbeddingModel(c.proveedor_embeddings || c.proveedor),
      proveedor_embeddings: c.proveedor_embeddings || (c.proveedor === 'anthropic' ? 'openai' : c.proveedor),
      base_url: c.base_url,
      api_key: apiKey,
      api_key_embeddings: apiKeyEmb || apiKey,
      max_tokens: c.max_tokens,
      temperatura: c.temperatura,
      activo: c.activo,
      reranker_activo: c.reranker_activo,
      reranker_proveedor: c.reranker_proveedor,
      reranker_modelo: c.reranker_modelo,
      reranker_api_key: rerankerApiKey,
      reranker_base_url: c.reranker_base_url,
      reranker_top_k: c.reranker_top_k,
      reranker_min_score: c.reranker_min_score,
    };
  }

  async listarModelos(body: { proveedor: string; api_key?: string; base_url?: string }) {
    const proveedor = body.proveedor;
    if (!MODELOS_POR_PROVEEDOR[proveedor]) {
      throw new BadRequestException(`Proveedor desconocido: ${proveedor}`);
    }

    // Si no llega api_key/base_url, intentar usar la guardada
    let apiKey = body.api_key && body.api_key.trim().length > 0 ? body.api_key.trim() : null;
    let baseUrl = body.base_url || null;
    if (!apiKey || !baseUrl) {
      try {
        const holdingId = await this.resolverHoldingId();
        const c = await this.prisma.ayuda_ia_config.findUnique({ where: { empresa_id: holdingId } });
        if (c) {
          if (!apiKey) {
            if (proveedor === c.proveedor && c.api_key_encrypted) apiKey = decryptKey(c.api_key_encrypted);
            else if (proveedor === c.proveedor_embeddings && c.api_key_embeddings_encrypted)
              apiKey = decryptKey(c.api_key_embeddings_encrypted);
          }
          if (!baseUrl) baseUrl = c.base_url;
        }
      } catch {
        // sin holding aún — usar estatica
      }
    }

    try {
      if (proveedor === 'ollama') {
        const url = (baseUrl || 'http://localhost:11434').replace(/\/$/, '');
        const r = await fetch(`${url}/api/tags`);
        if (!r.ok) throw new Error(`Ollama /api/tags respondió ${r.status}`);
        const data = (await r.json()) as { models?: Array<{ name?: string; model?: string }> };
        const todos = (data.models || []).map((m) => m.name || m.model || '').filter(Boolean);
        const embeddings = todos.filter((n) => /embed|bge|nomic|minilm|gte/i.test(n));
        const chat = todos.filter((n) => !embeddings.includes(n));
        return {
          chat: chat.length ? chat : MODELOS_POR_PROVEEDOR.ollama.chat,
          embeddings: embeddings.length ? embeddings : MODELOS_POR_PROVEEDOR.ollama.embeddings,
          fuente: todos.length ? 'api' : 'estatica',
        };
      }
      if (proveedor === 'anthropic') {
        if (!apiKey) return { ...MODELOS_POR_PROVEEDOR.anthropic, fuente: 'estatica' };
        const r = await fetch('https://api.anthropic.com/v1/models', {
          headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
        });
        if (!r.ok) {
          const txt = await r.text();
          throw new Error(`Anthropic ${r.status}: ${txt.slice(0, 200)}`);
        }
        const json = (await r.json()) as { data?: Array<{ id: string }> };
        const chat = (json.data || [])
          .map((m) => m.id)
          .filter((id) => id.startsWith('claude'));
        return {
          chat: chat.length ? chat : MODELOS_POR_PROVEEDOR.anthropic.chat,
          embeddings: [],
          fuente: chat.length ? 'api' : 'estatica',
        };
      }
      if (proveedor === 'openai') {
        if (!apiKey) return { ...MODELOS_POR_PROVEEDOR.openai, fuente: 'estatica' };
        const r = await fetch('https://api.openai.com/v1/models', {
          headers: { Authorization: `Bearer ${apiKey}` },
        });
        if (!r.ok) {
          const txt = await r.text();
          throw new Error(`OpenAI ${r.status}: ${txt.slice(0, 200)}`);
        }
        const json = (await r.json()) as { data?: Array<{ id: string }> };
        const ids = (json.data || []).map((m) => m.id);
        const chat = ids.filter((id) => /^gpt-/i.test(id)).sort();
        const embeddings = ids.filter((id) => /^text-embedding-/i.test(id)).sort();
        return {
          chat: chat.length ? chat : MODELOS_POR_PROVEEDOR.openai.chat,
          embeddings: embeddings.length ? embeddings : MODELOS_POR_PROVEEDOR.openai.embeddings,
          fuente: ids.length ? 'api' : 'estatica',
        };
      }
      if (proveedor === 'deepseek') {
        if (!apiKey) return { ...MODELOS_POR_PROVEEDOR.deepseek, fuente: 'estatica' };
        const r = await fetch('https://api.deepseek.com/models', {
          headers: { Authorization: `Bearer ${apiKey}` },
        });
        if (!r.ok) {
          const txt = await r.text();
          throw new Error(`DeepSeek ${r.status}: ${txt.slice(0, 200)}`);
        }
        const json = (await r.json()) as { data?: Array<{ id: string }> };
        const chat = (json.data || []).map((m) => m.id).filter(Boolean).sort();
        return {
          chat: chat.length ? chat : MODELOS_POR_PROVEEDOR.deepseek.chat,
          embeddings: [],
          fuente: chat.length ? 'api' : 'estatica',
        };
      }
    } catch (err) {
      const e = err as Error & { cause?: { code?: string } };
      const causa = e.cause?.code ? ` [${e.cause.code}]` : '';
      this.logger.warn(`listarModelos(${proveedor}) fallback estática: ${e.message}${causa}`);
      return { ...MODELOS_POR_PROVEEDOR[proveedor], fuente: 'estatica', error: `${e.message}${causa}` };
    }
    return { ...MODELOS_POR_PROVEEDOR[proveedor], fuente: 'estatica' };
  }

  async testConexion(callerEmpresaId: string, body: Partial<UpsertAyudaConfigDto> & { api_key?: string }) {
    await this.assertHoldingCaller(callerEmpresaId);
    const holdingId = await this.resolverHoldingId();
    const existing = await this.prisma.ayuda_ia_config.findUnique({ where: { empresa_id: holdingId } });

    const proveedor = body.proveedor ?? existing?.proveedor ?? 'openai';
    const modelo = body.modelo ?? existing?.modelo ?? 'gpt-4o-mini';
    const apiKey =
      body.api_key && body.api_key.trim().length > 0
        ? body.api_key.trim()
        : existing?.api_key_encrypted
          ? decryptKey(existing.api_key_encrypted)
          : null;
    const baseUrl = body.base_url ?? existing?.base_url ?? null;

    const proveedorEmb =
      body.proveedor_embeddings ?? existing?.proveedor_embeddings ?? (proveedor === 'anthropic' ? 'openai' : proveedor);
    const modeloEmb =
      body.modelo_embeddings ?? existing?.modelo_embeddings ?? this.defaultEmbeddingModel(proveedorEmb);
    const apiKeyEmb =
      body.api_key_embeddings && body.api_key_embeddings.trim().length > 0
        ? body.api_key_embeddings.trim()
        : existing?.api_key_embeddings_encrypted
          ? decryptKey(existing.api_key_embeddings_encrypted)
          : proveedorEmb === proveedor
            ? apiKey
            : null;

    // 1) Chat
    const chat = await this.tryCompletar({ proveedor, modelo, apiKey, baseUrl });
    // 2) Embeddings (con 1 texto corto)
    const embed = await this.tryEmbed({ proveedor: proveedorEmb, modelo: modeloEmb, apiKey: apiKeyEmb, baseUrl });

    // 3) Reranker (solo si está activo o si vienen credenciales nuevas)
    const rerankerActivo =
      body.reranker_activo ?? existing?.reranker_activo ?? false;
    const rerankerProveedor =
      body.reranker_proveedor ?? existing?.reranker_proveedor ?? null;
    const rerankerModelo =
      body.reranker_modelo ?? existing?.reranker_modelo ?? null;
    const rerankerApiKey =
      body.reranker_api_key && body.reranker_api_key.trim().length > 0
        ? body.reranker_api_key.trim()
        : existing?.reranker_api_key_encrypted
          ? decryptKey(existing.reranker_api_key_encrypted)
          : null;
    const rerankerBaseUrl =
      body.reranker_base_url ?? existing?.reranker_base_url ?? null;

    let reranker: any = null;
    if (rerankerActivo && rerankerProveedor && rerankerModelo) {
      reranker = await this.tryRerank({
        proveedor: rerankerProveedor,
        modelo: rerankerModelo,
        apiKey: rerankerApiKey,
        baseUrl: rerankerBaseUrl,
      });
      reranker = { ...reranker, proveedor: rerankerProveedor, modelo: rerankerModelo };
    }

    return {
      ok: chat.ok && embed.ok && (reranker ? reranker.ok : true),
      chat: { ...chat, proveedor, modelo },
      embeddings: { ...embed, proveedor: proveedorEmb, modelo: modeloEmb },
      reranker,
    };
  }

  private async tryRerank(p: {
    proveedor: string;
    modelo: string;
    apiKey: string | null;
    baseUrl: string | null;
  }) {
    try {
      const t0 = Date.now();
      const url =
        p.proveedor === 'cohere'
          ? (p.baseUrl ?? 'https://api.cohere.com').replace(/\/$/, '') + '/v2/rerank'
          : p.proveedor === 'jina'
            ? (p.baseUrl ?? 'https://api.jina.ai').replace(/\/$/, '') + '/v1/rerank'
            : p.baseUrl
              ? p.baseUrl.replace(/\/$/, '') + '/v1/rerank'
              : null;
      if (!url) throw new Error('Reranker local requiere base_url configurada.');
      if ((p.proveedor === 'cohere' || p.proveedor === 'jina') && !p.apiKey) {
        throw new Error(`${p.proveedor} requiere API key configurada.`);
      }
      const headers: Record<string, string> = { 'Content-Type': 'application/json' };
      if (p.apiKey) headers['Authorization'] = `Bearer ${p.apiKey}`;
      const resp = await fetch(url, {
        method: 'POST',
        headers,
        body: JSON.stringify({
          model: p.modelo,
          query: '¿cómo registro un pago?',
          documents: [
            'Para registrar un pago, abrí el módulo de Cobranzas.',
            'La luna es el satélite natural de la Tierra.',
          ],
          top_n: 2,
        }),
      });
      if (!resp.ok) {
        const text = await resp.text().catch(() => '');
        throw new Error(`${p.proveedor} ${resp.status}: ${text.slice(0, 200)}`);
      }
      const data = (await resp.json()) as {
        results?: Array<{ index: number; relevance_score: number }>;
      };
      const topScore = data.results?.[0]?.relevance_score ?? null;
      return { ok: true, latency_ms: Date.now() - t0, top_score: topScore };
    } catch (err) {
      return this.formatTestError(err);
    }
  }

  private async tryCompletar(p: { proveedor: string; modelo: string; apiKey: string | null; baseUrl: string | null }) {
    try {
      const r = await this.provider.completar({
        proveedor: p.proveedor,
        modelo: p.modelo,
        apiKey: p.apiKey,
        baseUrl: p.baseUrl,
        prompt: 'Respondé solo "OK".',
        system: 'Sos un asistente de prueba.',
        max_tokens: 10,
        temperatura: 0,
      });
      return { ok: true, latency_ms: r.latency_ms, sample: r.texto };
    } catch (err) {
      return this.formatTestError(err);
    }
  }

  private async tryEmbed(p: { proveedor: string; modelo: string; apiKey: string | null; baseUrl: string | null }) {
    try {
      const t0 = Date.now();
      const vecs = await this.provider.embed({
        proveedor: p.proveedor,
        modelo: p.modelo,
        apiKey: p.apiKey,
        baseUrl: p.baseUrl,
        textos: ['ping'],
      });
      return { ok: true, latency_ms: Date.now() - t0, dims: vecs[0]?.length ?? 0 };
    } catch (err) {
      return this.formatTestError(err);
    }
  }

  private formatTestError(err: unknown) {
    const e = err as Error & { cause?: { code?: string; message?: string }; status?: number };
    const detalle = e.cause?.code || e.status ? ` [${e.cause?.code || `HTTP ${e.status}`}]` : '';
    return {
      ok: false,
      error: `${e.message}${detalle}`,
      codigo: e.cause?.code || (e.status ? `HTTP ${e.status}` : null),
    };
  }

  private sanitize<T extends {
    api_key_encrypted: string | null;
    api_key_embeddings_encrypted: string | null;
    reranker_api_key_encrypted?: string | null;
  }>(c: T) {
    const { api_key_encrypted, api_key_embeddings_encrypted, reranker_api_key_encrypted, ...rest } = c;
    return {
      ...rest,
      tiene_api_key: !!api_key_encrypted,
      tiene_api_key_embeddings: !!api_key_embeddings_encrypted,
      tiene_reranker_api_key: !!reranker_api_key_encrypted,
    };
  }

  private defaultEmbeddingModel(proveedor: string): string {
    if (proveedor === 'openai') return 'text-embedding-3-small';
    if (proveedor === 'ollama') return 'mxbai-embed-large';
    return 'text-embedding-3-small';
  }
}
