import {
  BadRequestException,
  Inject,
  Injectable,
  Logger,
  NotFoundException,
  Optional,
  ServiceUnavailableException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AiConfigService } from '../../ai-dashboard/ai-config/ai-config.service';
import { AyudaProviderService } from '../../ayuda-ia/services/ayuda-provider.service';
import { ClientesCarteraService } from '../clientes/clientes-cartera.service';
import { CarteraImportadaFuente } from '../fuentes/cartera-importada-fuente.service';
import { ErpNovasisFuente } from '../fuentes/erp-novasis-fuente.service';
import { CarteraConfigService } from '../config/cartera-config.service';
import { PrismaService } from '../../prisma/prisma.service';

const PROMPT_VERSION_RECOMENDACION = 'rec-contacto-v1';
const PROMPT_VERSION_INFORME = 'informe-cli-v1';
const PROMPT_VERSION_RESUMEN_DIA = 'resumen-dia-v1';
const CACHE_HORAS = 24;

export interface RecomendacionContacto {
  canal_sugerido: 'WHATSAPP' | 'LLAMADA' | 'VISITA' | 'EMAIL' | 'SMS';
  mensaje_sugerido: string;
  prioridad: 'alta' | 'media' | 'baja';
  razonamiento: string;
  generado_en: Date;
  cacheado: boolean;
  modelo?: string;
}

export interface InformeEjecutivoCliente {
  perfil_cumplimiento: string;
  evolucion_mora: string;
  nivel_riesgo: 'alto' | 'medio' | 'bajo';
  factores_riesgo: string[];
  acciones_correctivas: string[];
  oportunidades: string[];
  resumen_ejecutivo: string;
  generado_en: Date;
  cacheado: boolean;
  modelo?: string;
}

export interface ResumenDiaPrioridad {
  cliente_id: string;
  cliente_nombre: string;
  cuenta_id: string | null;
  razon: string;
  accion_sugerida: string;
}

export interface ResumenDiaIA {
  prioridades_dia: ResumenDiaPrioridad[];
  alertas: string[];
  salud_cartera: 'sana' | 'tensionada' | 'critica';
  resumen_general: string;
  cuentas_analizadas: number;
  generado_en: Date;
  cacheado: boolean;
  modelo?: string;
}

interface CacheHit {
  output: string;
  modelo: string;
  creado_en: Date;
}

/**
 * Servicio de recomendaciones IA sobre cartera. Usa la configuración
 * **AI Dashboard por empresa** (`ai_empresa_config`) para que cada empresa
 * pueda elegir su proveedor / modelo / key independientemente.
 *
 * La ejecución del LLM se delega en `AyudaProviderService` que ya abstrae
 * los providers (Anthropic / OpenAI / Ollama). Toda llamada se audita en
 * `ai_calls` y se cachea por 24h por cliente.
 */
@Injectable()
export class CarteraIaService {
  private readonly logger = new Logger(CarteraIaService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly clientesService: ClientesCarteraService,
    private readonly carteraConfig: CarteraConfigService,
    private readonly erpNovasisFuente: ErpNovasisFuente,
    private readonly carteraImportadaFuente: CarteraImportadaFuente,
    // Opcionales: si el módulo AI Dashboard no está cargado o la empresa
    // no configuró su IA, devolvemos 503 controlado. El resto del módulo
    // sigue funcionando idéntico.
    @Optional() @Inject(AiConfigService) private readonly configIa?: AiConfigService,
    @Optional() @Inject(AyudaProviderService) private readonly provider?: AyudaProviderService,
  ) {}

  /**
   * Resuelve la config IA EFECTIVA para la empresa respetando el modo de credencial
   * (PROPIA = key de la empresa; PLATAFORMA = key del holding). Valida activo + api_key.
   */
  private async resolverConfigIa(empresa_id: string) {
    let propia: any;
    try {
      propia = await this.configIa!.getConfigConKey(empresa_id);
    } catch (err: any) {
      this.logger.warn(`Sin config IA para empresa ${empresa_id}: ${err?.message ?? err}`);
      throw new ServiceUnavailableException(
        'IA no configurada: configurá el proveedor IA en Dashboard IA → Configuración.',
      );
    }
    if (!propia.activo) {
      throw new ServiceUnavailableException('IA desactivada para esta empresa. Activala en Dashboard IA → Configuración.');
    }
    const eff = await this.configIa!.getConfigEfectiva(empresa_id);
    if (!eff.api_key) {
      throw new ServiceUnavailableException(
        'IA sin API key. Configurá tu propia key o usá los tokens de la plataforma en Dashboard IA → Configuración.',
      );
    }
    return eff;
  }

  async recomendarContacto(
    empresa_id: string,
    cliente_id: string,
    options: { forceRefresh?: boolean } = {},
  ): Promise<RecomendacionContacto> {
    if (!this.configIa || !this.provider) {
      throw new ServiceUnavailableException(
        'IA no disponible: el módulo AI Dashboard no está cargado en este backend.',
      );
    }

    // 1) Buscar cache (último call de este tipo para este cliente dentro de 24h).
    if (!options.forceRefresh) {
      const cache = await this.buscarCache(empresa_id, cliente_id, PROMPT_VERSION_RECOMENDACION);
      if (cache) {
        return this.parsearOutput(cache.output, cache.modelo, cache.creado_en, true);
      }
    }

    // 2) Obtener datos del cliente (cuentas + historial de gestiones).
    const detalle = await this.clientesService.detalle(empresa_id, cliente_id);
    if (!detalle.cliente) throw new NotFoundException('Cliente no encontrado');

    // 3) Cargar config IA EFECTIVA (respeta modo credencial propia/plataforma).
    const config = await this.resolverConfigIa(empresa_id);

    // 4) Construir prompt y llamar al LLM.
    const t0 = Date.now();
    const { system, prompt } = this.armarPrompt(detalle);
    let texto: string;
    let modelo = config.modelo;
    let tokensIn = 0;
    let tokensOut = 0;
    try {
      const resp = await this.provider.completar({
        proveedor: config.proveedor,
        modelo: config.modelo,
        apiKey: config.api_key,
        baseUrl: null,
        system,
        prompt,
        max_tokens: 500,
        temperatura: 0.3,
        empresa_id,
        feature: 'CARTERA',
        referencia_id: cliente_id,
        credencial_propia: config.credencial_propia,
      });
      texto = resp.texto;
      modelo = resp.modelo;
      tokensIn = resp.tokens_in;
      tokensOut = resp.tokens_out;
    } catch (err: any) {
      this.logger.error(`LLM falló: ${err?.message ?? err}`);
      throw new ServiceUnavailableException(
        `IA falló al generar la recomendación: ${err?.message ?? err}`,
      );
    }
    const latency = Date.now() - t0;

    // 5) Persistir en ai_calls para auditoría + cache.
    const ahora = new Date();
    await this.prisma.ai_calls.create({
      data: {
        empresa_id,
        prompt_version: PROMPT_VERSION_RECOMENDACION.slice(0, 20),
        modelo: modelo.slice(0, 50),
        input: {
          tipo: 'recomendar_contacto',
          cliente_id,
          contexto: {
            cuentas_pendientes: detalle.totales.cuentas_pendientes,
            saldo_total: detalle.totales.saldo_total,
            gestiones_total: detalle.totales.gestiones_total,
          },
          tokens_in: tokensIn,
          tokens_out: tokensOut,
        } as Prisma.InputJsonValue,
        output: texto,
        latencia_ms: latency,
        fallback_usado: false,
      },
    });

    return this.parsearOutput(texto, modelo, ahora, false);
  }

  // ─── IA-2: Informe ejecutivo del cliente ─────────────────────────────────

  async informeEjecutivo(
    empresa_id: string,
    cliente_id: string,
    options: { forceRefresh?: boolean } = {},
  ): Promise<InformeEjecutivoCliente> {
    if (!this.configIa || !this.provider) {
      throw new ServiceUnavailableException(
        'IA no disponible: el módulo AI Dashboard no está cargado en este backend.',
      );
    }

    if (!options.forceRefresh) {
      const cache = await this.buscarCache(empresa_id, cliente_id, PROMPT_VERSION_INFORME);
      if (cache) {
        return this.parsearInforme(cache.output, cache.modelo, cache.creado_en, true);
      }
    }

    const detalle = await this.clientesService.detalle(empresa_id, cliente_id);
    if (!detalle.cliente) throw new NotFoundException('Cliente no encontrado');

    const config = await this.resolverConfigIa(empresa_id);

    const t0 = Date.now();
    const { system, prompt } = this.armarPromptInforme(detalle);
    let texto: string;
    let modelo = config.modelo;
    let tokensIn = 0;
    let tokensOut = 0;
    try {
      const resp = await this.provider.completar({
        proveedor: config.proveedor,
        modelo: config.modelo,
        apiKey: config.api_key,
        baseUrl: null,
        system,
        prompt,
        max_tokens: 1200,
        temperatura: 0.4,
        empresa_id,
        feature: 'CARTERA',
        referencia_id: cliente_id,
        credencial_propia: config.credencial_propia,
      });
      texto = resp.texto;
      modelo = resp.modelo;
      tokensIn = resp.tokens_in;
      tokensOut = resp.tokens_out;
    } catch (err: any) {
      this.logger.error(`LLM falló (informe): ${err?.message ?? err}`);
      throw new ServiceUnavailableException(
        `IA falló al generar el informe: ${err?.message ?? err}`,
      );
    }
    const latency = Date.now() - t0;
    const ahora = new Date();

    await this.prisma.ai_calls.create({
      data: {
        empresa_id,
        prompt_version: PROMPT_VERSION_INFORME.slice(0, 20),
        modelo: modelo.slice(0, 50),
        input: {
          tipo: 'informe_ejecutivo',
          cliente_id,
          contexto: {
            cuentas_pendientes: detalle.totales.cuentas_pendientes,
            saldo_total: detalle.totales.saldo_total,
            gestiones_total: detalle.totales.gestiones_total,
          },
          tokens_in: tokensIn,
          tokens_out: tokensOut,
        } as Prisma.InputJsonValue,
        output: texto,
        latencia_ms: latency,
        fallback_usado: false,
      },
    });

    return this.parsearInforme(texto, modelo, ahora, false);
  }

  // ─── Helpers ─────────────────────────────────────────────────────────────

  private async buscarCache(
    empresa_id: string,
    cliente_id: string,
    promptVersion: string,
  ): Promise<CacheHit | null> {
    const since = new Date(Date.now() - CACHE_HORAS * 60 * 60 * 1000);
    const row = await this.prisma.ai_calls.findFirst({
      where: {
        empresa_id,
        prompt_version: promptVersion.slice(0, 20),
        creado_en: { gte: since },
        input: { path: ['cliente_id'], equals: cliente_id },
      },
      orderBy: { creado_en: 'desc' },
    });
    if (!row || !row.output) return null;
    return { output: row.output, modelo: row.modelo, creado_en: row.creado_en };
  }

  private armarPrompt(detalle: any): { system: string; prompt: string } {
    const totales = detalle.totales;
    const monedaCodigo = detalle.cuentas[0]?.moneda_codigo ?? 'PYG';

    const cuentas = (detalle.cuentas as any[]).slice(0, 10).map((c) => ({
      documento: c.documento_numero,
      saldo: Number(c.saldo_pendiente),
      vencimiento: c.fecha_vencimiento
        ? new Date(c.fecha_vencimiento).toISOString().slice(0, 10)
        : null,
      dias_mora: c.dias_mora,
      moneda: c.moneda_codigo,
    }));

    const gestiones = (detalle.gestiones as any[]).slice(0, 10).map((g) => ({
      fecha: new Date(g.fecha).toISOString().slice(0, 10),
      tipo: g.tipo_gestion,
      resultado: g.resultado,
      observacion: g.observacion?.slice(0, 200) ?? null,
    }));

    const contexto = {
      cliente: {
        nombre: detalle.cliente.nombre,
        documento: detalle.cliente.documento ?? null,
        tiene_telefono: !!detalle.cliente.telefono,
        tiene_email: !!detalle.cliente.email,
      },
      totales: {
        cuentas_pendientes: totales.cuentas_pendientes,
        saldo_total: totales.saldo_total,
        moneda: monedaCodigo,
        gestiones_total: totales.gestiones_total,
      },
      cuentas_top10: cuentas,
      gestiones_ultimas: gestiones,
    };

    const system = [
      'Sos un analista de cobranzas senior. Tu tarea es recomendar la mejor acción para contactar a un cliente con mora.',
      'Respondés SIEMPRE en JSON estricto con este shape exacto:',
      '{',
      '  "canal_sugerido": "WHATSAPP" | "LLAMADA" | "VISITA" | "EMAIL" | "SMS",',
      '  "mensaje_sugerido": "<texto del mensaje en español paraguayo, máximo 280 caracteres, tuteando, cordial>",',
      '  "prioridad": "alta" | "media" | "baja",',
      '  "razonamiento": "<1-2 oraciones explicando por qué este cliente es prioritario y por qué este canal>"',
      '}',
      'Reglas:',
      '- Si dias_mora > 60: prioridad alta y canal LLAMADA o VISITA.',
      '- Si dias_mora 30-60: prioridad media, WHATSAPP suele funcionar mejor.',
      '- Si dias_mora < 30: prioridad baja, WHATSAPP/SMS amable.',
      '- Si el cliente NO tiene_telefono: NO recomendar LLAMADA/SMS/WHATSAPP, ofrecer EMAIL o VISITA.',
      '- Si el cliente NO tiene_email: NO recomendar EMAIL.',
      '- Si en gestiones recientes resultado=NO_ATIENDE repetido: subí a VISITA.',
      '- Si resultado=PROMESA_PAGO reciente: tono de seguimiento, no agresivo.',
      '- En mensaje_sugerido NUNCA inventes montos exactos — usá "tu saldo pendiente" o redondeá.',
      '- NO incluyas texto fuera del JSON. NO uses markdown. NO uses backticks.',
    ].join('\n');

    const prompt = `Datos del cliente:\n${JSON.stringify(contexto, null, 2)}\n\nGenerá la recomendación en JSON.`;

    return { system, prompt };
  }

  private parsearOutput(
    output: string,
    modelo: string,
    generadoEn: Date,
    cacheado: boolean,
  ): RecomendacionContacto {
    let limpio = output.trim();
    // Tolerar envoltorios markdown que algunos modelos meten.
    if (limpio.startsWith('```')) {
      limpio = limpio.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');
    }
    let parsed: any;
    try {
      parsed = JSON.parse(limpio);
    } catch {
      throw new BadRequestException(
        `La IA devolvió un formato inválido. Probá regenerar. Respuesta cruda: ${limpio.slice(0, 200)}`,
      );
    }

    const canalesValidos = ['WHATSAPP', 'LLAMADA', 'VISITA', 'EMAIL', 'SMS'];
    if (!canalesValidos.includes(parsed.canal_sugerido)) {
      parsed.canal_sugerido = 'WHATSAPP';
    }
    if (!['alta', 'media', 'baja'].includes(parsed.prioridad)) {
      parsed.prioridad = 'media';
    }

    return {
      canal_sugerido: parsed.canal_sugerido,
      mensaje_sugerido: String(parsed.mensaje_sugerido ?? '').slice(0, 500),
      prioridad: parsed.prioridad,
      razonamiento: String(parsed.razonamiento ?? '').slice(0, 1000),
      generado_en: generadoEn,
      cacheado,
      modelo,
    };
  }

  // ─── Prompt + parser para Informe ejecutivo (IA-2) ───────────────────────

  private armarPromptInforme(detalle: any): { system: string; prompt: string } {
    const totales = detalle.totales;
    const monedaCodigo = detalle.cuentas[0]?.moneda_codigo ?? 'PYG';

    const cuentas = (detalle.cuentas as any[]).slice(0, 15).map((c) => ({
      documento: c.documento_numero,
      saldo: Number(c.saldo_pendiente),
      monto_total: Number(c.monto_total),
      vencimiento: c.fecha_vencimiento
        ? new Date(c.fecha_vencimiento).toISOString().slice(0, 10)
        : null,
      dias_mora: c.dias_mora,
      moneda: c.moneda_codigo,
    }));

    const gestiones = (detalle.gestiones as any[]).slice(0, 20).map((g) => ({
      fecha: new Date(g.fecha).toISOString().slice(0, 10),
      tipo: g.tipo_gestion,
      resultado: g.resultado,
      observacion: g.observacion?.slice(0, 200) ?? null,
    }));

    const moraPromedio = cuentas.length > 0
      ? Math.round(cuentas.reduce((s, c) => s + c.dias_mora, 0) / cuentas.length)
      : 0;
    const moraMaxima = cuentas.length > 0
      ? Math.max(...cuentas.map((c) => c.dias_mora))
      : 0;
    const conMora = cuentas.filter((c) => c.dias_mora > 0).length;

    const contexto = {
      cliente: {
        nombre: detalle.cliente.nombre,
        documento: detalle.cliente.documento ?? null,
        tiene_telefono: !!detalle.cliente.telefono,
        tiene_email: !!detalle.cliente.email,
      },
      totales: {
        cuentas_pendientes: totales.cuentas_pendientes,
        saldo_total: totales.saldo_total,
        moneda: monedaCodigo,
        gestiones_total: totales.gestiones_total,
      },
      indicadores: {
        mora_promedio_dias: moraPromedio,
        mora_maxima_dias: moraMaxima,
        cuentas_con_mora: conMora,
        cuentas_total: cuentas.length,
      },
      cuentas_top15: cuentas,
      gestiones_ultimas20: gestiones,
    };

    const system = [
      'Sos un analista senior de riesgo crediticio y cobranzas.',
      'Tu tarea es generar un INFORME EJECUTIVO en JSON estricto sobre un cliente con cartera pendiente.',
      'Respondés SIEMPRE en JSON estricto con este shape exacto:',
      '{',
      '  "perfil_cumplimiento": "<2-3 oraciones describiendo cómo cumple históricamente (puntual, intermitente, moroso crónico, etc.) basándote en patrones del historial de gestiones>",',
      '  "evolucion_mora": "<2-3 oraciones describiendo la evolución de la mora: si está empeorando, mejorando o estable. Cita datos concretos (mora promedio, máxima, cuántas cuentas)>",',
      '  "nivel_riesgo": "alto" | "medio" | "bajo",',
      '  "factores_riesgo": ["<factor 1>", "<factor 2>", "..."],',
      '  "acciones_correctivas": ["<acción concreta 1>", "<acción 2>", "..."],',
      '  "oportunidades": ["<oportunidad 1 (refinanciación, descuento por pago contado, ajuste de plan de pagos, etc.)>", "..."],',
      '  "resumen_ejecutivo": "<1-2 oraciones de cierre: qué hacer con este cliente esta semana>"',
      '}',
      'Reglas:',
      '- Los arrays deben tener entre 2 y 5 items cada uno.',
      '- Sé concreto y accionable. Evitá frases genéricas tipo "monitorear de cerca" — decí qué exactamente.',
      '- Tono profesional, en español paraguayo, sin tutear (estás escribiendo para gerencia).',
      '- nivel_riesgo se basa en mora_promedio_dias + cuentas_con_mora / cuentas_total + resultados de gestiones recientes (CLIENTE_FALLECIDO / RECHAZO_PAGAR / DIRECCION_INCORRECTA aumentan el riesgo).',
      '- NO inventes datos que no estén en el contexto. Si falta información (ej. ingresos del cliente), señalalo en factores_riesgo.',
      '- NO incluyas markdown ni backticks. Sólo JSON puro.',
    ].join('\n');

    const prompt = `Datos del cliente:\n${JSON.stringify(contexto, null, 2)}\n\nGenerá el informe ejecutivo en JSON.`;

    return { system, prompt };
  }

  private parsearInforme(
    output: string,
    modelo: string,
    generadoEn: Date,
    cacheado: boolean,
  ): InformeEjecutivoCliente {
    let limpio = output.trim();
    if (limpio.startsWith('```')) {
      limpio = limpio.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');
    }
    let parsed: any;
    try {
      parsed = JSON.parse(limpio);
    } catch {
      throw new BadRequestException(
        `La IA devolvió un formato inválido para el informe. Probá regenerar. Crudo: ${limpio.slice(0, 200)}`,
      );
    }

    const asArray = (v: unknown): string[] => {
      if (!Array.isArray(v)) return [];
      return v.map((x) => String(x ?? '').slice(0, 400)).filter((s) => s.trim().length > 0).slice(0, 8);
    };

    const nivel = ['alto', 'medio', 'bajo'].includes(parsed.nivel_riesgo)
      ? parsed.nivel_riesgo
      : 'medio';

    return {
      perfil_cumplimiento: String(parsed.perfil_cumplimiento ?? '').slice(0, 1500),
      evolucion_mora: String(parsed.evolucion_mora ?? '').slice(0, 1500),
      nivel_riesgo: nivel,
      factores_riesgo: asArray(parsed.factores_riesgo),
      acciones_correctivas: asArray(parsed.acciones_correctivas),
      oportunidades: asArray(parsed.oportunidades),
      resumen_ejecutivo: String(parsed.resumen_ejecutivo ?? '').slice(0, 1000),
      generado_en: generadoEn,
      cacheado,
      modelo,
    };
  }

  // ─── IA-3: Resumen IA del día (portfolio-level) ──────────────────────────

  async resumenDia(
    empresa_id: string,
    options: { forceRefresh?: boolean } = {},
  ): Promise<ResumenDiaIA> {
    if (!this.configIa || !this.provider) {
      throw new ServiceUnavailableException(
        'IA no disponible: el módulo AI Dashboard no está cargado en este backend.',
      );
    }

    // Cache 24h por empresa (no por cliente, este es portfolio-level).
    if (!options.forceRefresh) {
      const cache = await this.buscarCachePortfolio(empresa_id, PROMPT_VERSION_RESUMEN_DIA);
      if (cache) {
        return this.parsearResumen(cache.output, cache.modelo, cache.creado_en, true, 0);
      }
    }

    // Cargar las top 20 cuentas críticas como contexto para la IA.
    const config = await this.carteraConfig.getOrCreate(empresa_id);
    const fuente =
      config.fuente_default === 'cartera-importada'
        ? this.carteraImportadaFuente
        : this.erpNovasisFuente;
    const cuentas = await fuente.listarCuentasPendientes({
      empresa_id,
      page: 1,
      page_size: 30,
    });
    const criticas = cuentas
      .filter((c) => c.dias_mora > 0)
      .sort((a, b) => b.dias_mora - a.dias_mora || b.saldo_pendiente - a.saldo_pendiente)
      .slice(0, 20);

    if (criticas.length === 0) {
      // Sin mora — devolver respuesta directa sin llamar al LLM (ahorra tokens).
      return {
        prioridades_dia: [],
        alertas: [],
        salud_cartera: 'sana',
        resumen_general:
          'Sin cuentas con mora actualmente. La cartera está en buen estado — buen momento para enfocarse en prevención y consolidar relaciones con clientes pagadores.',
        cuentas_analizadas: 0,
        generado_en: new Date(),
        cacheado: false,
        modelo: 'sin-llm',
      };
    }

    const iaConfig = await this.resolverConfigIa(empresa_id);

    const t0 = Date.now();
    const { system, prompt } = this.armarPromptResumen(criticas);
    let texto: string;
    let modelo = iaConfig.modelo;
    let tokensIn = 0;
    let tokensOut = 0;
    try {
      const resp = await this.provider.completar({
        proveedor: iaConfig.proveedor,
        modelo: iaConfig.modelo,
        apiKey: iaConfig.api_key,
        baseUrl: null,
        system,
        prompt,
        max_tokens: 3000,
        temperatura: 0.3,
        empresa_id,
        feature: 'CARTERA',
        credencial_propia: iaConfig.credencial_propia,
      });
      texto = resp.texto;
      modelo = resp.modelo;
      tokensIn = resp.tokens_in;
      tokensOut = resp.tokens_out;
    } catch (err: any) {
      this.logger.error(`LLM falló (resumen-dia): ${err?.message ?? err}`);
      throw new ServiceUnavailableException(
        `IA falló al generar el resumen del día: ${err?.message ?? err}`,
      );
    }
    const latency = Date.now() - t0;
    const ahora = new Date();

    await this.prisma.ai_calls.create({
      data: {
        empresa_id,
        prompt_version: PROMPT_VERSION_RESUMEN_DIA.slice(0, 20),
        modelo: modelo.slice(0, 50),
        input: {
          tipo: 'resumen_dia',
          cuentas_analizadas: criticas.length,
          mora_maxima: Math.max(...criticas.map((c) => c.dias_mora)),
          tokens_in: tokensIn,
          tokens_out: tokensOut,
        } as Prisma.InputJsonValue,
        output: texto,
        latencia_ms: latency,
        fallback_usado: false,
      },
    });

    return this.parsearResumen(texto, modelo, ahora, false, criticas.length);
  }

  private async buscarCachePortfolio(
    empresa_id: string,
    promptVersion: string,
  ): Promise<CacheHit | null> {
    const since = new Date(Date.now() - CACHE_HORAS * 60 * 60 * 1000);
    const row = await this.prisma.ai_calls.findFirst({
      where: {
        empresa_id,
        prompt_version: promptVersion.slice(0, 20),
        creado_en: { gte: since },
      },
      orderBy: { creado_en: 'desc' },
    });
    if (!row || !row.output) return null;
    return { output: row.output, modelo: row.modelo, creado_en: row.creado_en };
  }

  private armarPromptResumen(cuentas: any[]): { system: string; prompt: string } {
    const moneda = cuentas[0]?.moneda_codigo ?? 'PYG';
    const saldoTotal = cuentas.reduce((s, c) => s + Number(c.saldo_pendiente), 0);
    const moraPromedio = Math.round(
      cuentas.reduce((s, c) => s + c.dias_mora, 0) / cuentas.length,
    );
    const moraMaxima = Math.max(...cuentas.map((c) => c.dias_mora));

    const contexto = {
      empresa: {
        cuentas_criticas: cuentas.length,
        saldo_total_pendiente: saldoTotal,
        moneda,
        mora_promedio_dias: moraPromedio,
        mora_maxima_dias: moraMaxima,
      },
      cuentas: cuentas.map((c) => ({
        cliente_id: c.cliente_id,
        cliente_nombre: c.cliente_nombre,
        documento: c.documento_numero,
        saldo: Number(c.saldo_pendiente),
        dias_mora: c.dias_mora,
        moneda: c.moneda_codigo,
      })),
    };

    const system = [
      'Sos un gerente de cobranzas senior. Tu tarea es generar un RESUMEN EJECUTIVO DEL DÍA para el equipo de cobranzas, mirando el portfolio completo de cuentas críticas.',
      'Respondés SIEMPRE en JSON estricto con este shape exacto:',
      '{',
      '  "salud_cartera": "sana" | "tensionada" | "critica",',
      '  "resumen_general": "<2-3 oraciones que describan el estado general de la cartera>",',
      '  "prioridades_dia": [',
      '    {',
      '      "cliente_id": "<id del contexto>",',
      '      "cliente_nombre": "<nombre del contexto>",',
      '      "cuenta_id": null,',
      '      "razon": "<por qué este cliente es prioridad hoy>",',
      '      "accion_sugerida": "<acción específica: llamar, visitar, escalar a gestor X, etc.>"',
      '    }',
      '  ],',
      '  "alertas": ["<alerta operativa 1>", "<alerta 2>", "..."]',
      '}',
      'Reglas:',
      '- prioridades_dia debe tener entre 3 y 5 clientes (los más prioritarios para atacar hoy). Para cada uno usá el cliente_id y nombre EXACTOS del contexto.',
      '- alertas debe tener entre 1 y 5 items: patrones que el equipo debe vigilar (concentración de riesgo, clientes nuevos con mora, deterioro acelerado, etc.).',
      '- salud_cartera: "sana" si <30% cuentas con mora>30d, "tensionada" si 30-50%, "critica" si >50% o mora máxima >120d.',
      '- Tono profesional, escribiendo PARA el equipo (usá "el equipo" o tuteo formal). Sin tutear personalmente.',
      '- NO inventes datos. Si falta info, mencionalo en alertas.',
      '- NO incluyas markdown ni backticks. Sólo JSON puro.',
    ].join('\n');

    const prompt = `Datos del portfolio crítico de hoy:\n${JSON.stringify(contexto, null, 2)}\n\nGenerá el resumen ejecutivo del día en JSON.`;

    return { system, prompt };
  }

  private parsearResumen(
    output: string,
    modelo: string,
    generadoEn: Date,
    cacheado: boolean,
    cuentasAnalizadas: number,
  ): ResumenDiaIA {
    let limpio = output.trim();
    if (limpio.startsWith('```')) {
      limpio = limpio.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');
    }
    let parsed: any;
    try {
      parsed = JSON.parse(limpio);
    } catch {
      // El modelo puede cortar la respuesta (max_tokens). Intentamos reparar:
      // cerramos arrays/objetos abiertos contando llaves/corchetes.
      const reparado = this.repararJsonTruncado(limpio);
      if (reparado) {
        try {
          parsed = JSON.parse(reparado);
          this.logger.warn(
            `Resumen del día: JSON truncado reparado heurísticamente (output ${limpio.length} chars).`,
          );
        } catch {
          throw new BadRequestException(
            `La IA devolvió un JSON truncado e irreparable. Probá con un modelo más grande o regenerá. ` +
              `Crudo: ${limpio.slice(0, 200)}`,
          );
        }
      } else {
        throw new BadRequestException(
          `La IA devolvió un formato inválido para el resumen. Probá regenerar. ` +
            `Crudo: ${limpio.slice(0, 200)}`,
        );
      }
    }

    const salud = ['sana', 'tensionada', 'critica'].includes(parsed.salud_cartera)
      ? parsed.salud_cartera
      : 'tensionada';

    const prioridades: ResumenDiaPrioridad[] = Array.isArray(parsed.prioridades_dia)
      ? parsed.prioridades_dia
          .slice(0, 6)
          .map((p: any) => ({
            cliente_id: String(p.cliente_id ?? ''),
            cliente_nombre: String(p.cliente_nombre ?? '').slice(0, 200),
            cuenta_id: p.cuenta_id ? String(p.cuenta_id) : null,
            razon: String(p.razon ?? '').slice(0, 400),
            accion_sugerida: String(p.accion_sugerida ?? '').slice(0, 400),
          }))
          .filter((p: ResumenDiaPrioridad) => p.cliente_id && p.cliente_nombre)
      : [];

    const alertas: string[] = Array.isArray(parsed.alertas)
      ? parsed.alertas
          .map((a: any) => String(a ?? '').slice(0, 300))
          .filter((s: string) => s.trim().length > 0)
          .slice(0, 6)
      : [];

    return {
      prioridades_dia: prioridades,
      alertas,
      salud_cartera: salud,
      resumen_general: String(parsed.resumen_general ?? '').slice(0, 1000),
      cuentas_analizadas: cuentasAnalizadas,
      generado_en: generadoEn,
      cacheado,
      modelo,
    };
  }

  /**
   * Heurística simple para cerrar JSON truncado por límite de tokens.
   * Recorta hasta el último carácter "seguro" (fin de string completo o último
   * elemento de array que termina en } o ]) y cierra llaves/corchetes pendientes.
   * Devuelve `null` si no se puede salvar.
   */
  private repararJsonTruncado(raw: string): string | null {
    if (!raw.startsWith('{')) return null;

    // Encontrar el último carácter "fin de valor" (} o ]) que NO esté dentro de un string.
    let inString = false;
    let escaped = false;
    let stack: string[] = [];
    let lastSafe = -1;
    for (let i = 0; i < raw.length; i++) {
      const ch = raw[i];
      if (escaped) {
        escaped = false;
        continue;
      }
      if (inString) {
        if (ch === '\\') escaped = true;
        else if (ch === '"') inString = false;
        continue;
      }
      if (ch === '"') {
        inString = true;
      } else if (ch === '{' || ch === '[') {
        stack.push(ch);
      } else if (ch === '}' || ch === ']') {
        stack.pop();
        if (stack.length > 0) lastSafe = i;
      } else if (ch === ',' && stack.length === 1) {
        lastSafe = i - 1; // antes de la coma, dentro del objeto raíz
      }
    }

    if (lastSafe < 0) return null;

    let candidato = raw.slice(0, lastSafe + 1);

    // Reconstruir stack para el candidato truncado.
    inString = false;
    escaped = false;
    stack = [];
    for (let i = 0; i < candidato.length; i++) {
      const ch = candidato[i];
      if (escaped) { escaped = false; continue; }
      if (inString) {
        if (ch === '\\') escaped = true;
        else if (ch === '"') inString = false;
        continue;
      }
      if (ch === '"') inString = true;
      else if (ch === '{' || ch === '[') stack.push(ch);
      else if (ch === '}' || ch === ']') stack.pop();
    }
    if (inString) return null;

    // Cerrar todas las llaves/corchetes pendientes.
    while (stack.length > 0) {
      const open = stack.pop();
      candidato += open === '{' ? '}' : ']';
    }
    return candidato;
  }
}
