import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { RrhhContextService } from '../shared/rrhh-context.service';

export interface ProyeccionCobranza {
  esperado: number;       // suma de cuotas vencidas próximo mes
  probable: number;       // esperado × tasa histórica de cobro del cobrador
  optimista: number;      // esperado × 0.90
  pesimista: number;      // esperado × 0.55
  cantCuotas: number;     // cantidad de cuotas que vencen
  porCobrador: { nombre: string; esperado: number; tasa: number }[];
}

export interface ClienteRiesgo {
  clienteId: string;
  nombre: string;
  saldoPendiente: number;
  diasSinPagar: number;
  promesasIncumplidas: number;
  scoreRiesgo: number;   // 0-100
  nivel: 'alto' | 'medio' | 'bajo';
}

export interface ProyeccionVentas {
  periodos: { periodo: string; real?: number; proyectado?: number }[];
  tendencia: 'creciente' | 'estable' | 'decreciente';
  proximoMesEstimado: number;
  variacionPct: number; // vs mes anterior
}

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly rrhhContext: RrhhContextService,
  ) {}

  // ── P1: Proyección de cobranza próximo mes ────────────────────

  async proyeccionCobranza(empresaId: string): Promise<ProyeccionCobranza> {
    const hoy = new Date();
    const en30dias = new Date(hoy);
    en30dias.setDate(en30dias.getDate() + 30);

    // Cuotas que vencen en los próximos 30 días
    const cuotasProximas = await this.prisma.$queryRaw<{
      cobrador: string;
      cobrador_id: string;
      esperado: Prisma.Decimal;
      cant_cuotas: bigint;
    }[]>`
      SELECT
        COALESCE(vc.nombre, 'Sin cobrador') AS cobrador,
        COALESCE(cab.cobrador_id::text, 'sin-cobrador') AS cobrador_id,
        COALESCE(SUM(fc.saldo_pendiente), 0) AS esperado,
        COUNT(*) AS cant_cuotas
      FROM factura_cuotas fc
      JOIN factura_cab cab ON cab.id = fc.factura_cab_id
      LEFT JOIN vendedores_cobradores vc ON vc.id = cab.cobrador_id
      WHERE cab.empresa_id = ${empresaId}::uuid
        AND fc.estado = 'pendiente'
        AND fc.dvenccuo >= ${hoy}
        AND fc.dvenccuo <= ${en30dias}
      GROUP BY vc.nombre, cab.cobrador_id
      ORDER BY esperado DESC
    `;

    // Tasa histórica de cobro por cobrador (últimos 3 meses)
    const hace90dias = new Date(hoy);
    hace90dias.setDate(hace90dias.getDate() - 90);

    const tasasCobrador = await this.prisma.$queryRaw<{
      cobrador_id: string;
      total_esperado: Prisma.Decimal;
      total_cobrado: Prisma.Decimal;
    }[]>`
      SELECT
        COALESCE(cab.cobrador_id::text, 'sin-cobrador') AS cobrador_id,
        COALESCE(SUM(fc.dmoncuota), 0) AS total_esperado,
        COALESCE(SUM(
          CASE WHEN fc.estado = 'pagada' THEN fc.dmoncuota ELSE 0 END
        ), 0) AS total_cobrado
      FROM factura_cuotas fc
      JOIN factura_cab cab ON cab.id = fc.factura_cab_id
      WHERE cab.empresa_id = ${empresaId}::uuid
        AND fc.dvenccuo >= ${hace90dias}
        AND fc.dvenccuo < ${hoy}
      GROUP BY cab.cobrador_id
    `;

    const tasaMap = new Map<string, number>();
    for (const t of tasasCobrador) {
      const esperado = Number(t.total_esperado);
      const cobrado = Number(t.total_cobrado);
      tasaMap.set(t.cobrador_id, esperado > 0 ? Math.min(cobrado / esperado, 1) : 0.7);
    }

    const porCobrador = cuotasProximas.map((c) => {
      const tasa = tasaMap.get(c.cobrador_id) ?? 0.7;
      return {
        nombre: c.cobrador,
        esperado: Number(c.esperado),
        tasa: Math.round(tasa * 100),
      };
    });

    const esperadoTotal = porCobrador.reduce((s, c) => s + c.esperado, 0);
    const tasaGlobal = porCobrador.length > 0
      ? porCobrador.reduce((s, c) => s + c.tasa / 100 * c.esperado, 0) / (esperadoTotal || 1)
      : 0.7;

    const cantCuotas = cuotasProximas.reduce((s, c) => s + Number(c.cant_cuotas), 0);

    return {
      esperado: Math.round(esperadoTotal),
      probable: Math.round(esperadoTotal * tasaGlobal),
      optimista: Math.round(esperadoTotal * 0.90),
      pesimista: Math.round(esperadoTotal * 0.55),
      cantCuotas,
      porCobrador,
    };
  }

  // ── P2: Clientes en riesgo de mora ────────────────────────────

  async clientesEnRiesgo(empresaId: string): Promise<ClienteRiesgo[]> {
    // Score de riesgo basado en:
    // - Días desde último pago (40%)
    // - Cuotas vencidas actuales (35%)
    // - Promesas incumplidas (25%)
    const clientes = await this.prisma.$queryRaw<{
      cliente_id: string;
      nombre: string;
      saldo_pendiente: Prisma.Decimal;
      dias_sin_pagar: number;
      cuotas_vencidas: bigint;
      monto_vencido: Prisma.Decimal;
    }[]>`
      SELECT
        c.id AS cliente_id,
        p.razon_social AS nombre,
        COALESCE(c.saldo_pendiente, 0) AS saldo_pendiente,
        COALESCE(
          EXTRACT(DAY FROM NOW() - c.fecha_ultimo_pago)::int,
          90
        ) AS dias_sin_pagar,
        COUNT(fc.id) FILTER (WHERE fc.estado = 'pendiente' AND fc.dvenccuo < CURRENT_DATE) AS cuotas_vencidas,
        COALESCE(SUM(fc.saldo_pendiente) FILTER (WHERE fc.estado = 'pendiente' AND fc.dvenccuo < CURRENT_DATE), 0) AS monto_vencido
      FROM clientes c
      JOIN personas p ON p.id = c.persona_id
      LEFT JOIN factura_cab cab ON cab.id = (
        SELECT id FROM factura_cab WHERE cliente_id = c.id AND empresa_id = ${empresaId}::uuid LIMIT 1
      )
      LEFT JOIN factura_cuotas fc ON fc.factura_cab_id = cab.id
      WHERE p.empresa_id = ${empresaId}::uuid
        AND c.deleted = false
        AND c.saldo_pendiente > 0
      GROUP BY c.id, p.razon_social, c.saldo_pendiente, c.fecha_ultimo_pago
      HAVING COUNT(fc.id) FILTER (WHERE fc.estado = 'pendiente' AND fc.dvenccuo < CURRENT_DATE) > 0
         OR COALESCE(EXTRACT(DAY FROM NOW() - c.fecha_ultimo_pago), 90) > 30
      ORDER BY saldo_pendiente DESC
      LIMIT 30
    `;

    // Promesas incumplidas por cliente
    const promesasMap = new Map<string, number>();
    try {
      const promesas = await this.prisma.$queryRaw<{ cliente_id: string; total: bigint }[]>`
        SELECT fc.cliente_id, COUNT(*) AS total
        FROM promesas_pago pp
        JOIN factura_cab fc ON fc.id = pp.factura_id
        WHERE fc.empresa_id = ${empresaId}::uuid
          AND pp.fecha_prometida < CURRENT_DATE
          AND pp.estado = 'pendiente'
        GROUP BY fc.cliente_id
      `;
      for (const p of promesas) promesasMap.set(p.cliente_id, Number(p.total));
    } catch { /* tabla puede no existir */ }

    return clientes.map((c) => {
      const diasSinPagar = c.dias_sin_pagar ?? 90;
      const cuotasVencidas = Number(c.cuotas_vencidas ?? 0);
      const promesasIncumplidas = promesasMap.get(c.cliente_id) ?? 0;

      // Score 0-100
      const scoreDias = Math.min(diasSinPagar / 90, 1) * 40;
      const scoreCuotas = Math.min(cuotasVencidas / 5, 1) * 35;
      const scorePromesas = Math.min(promesasIncumplidas / 3, 1) * 25;
      const score = Math.round(scoreDias + scoreCuotas + scorePromesas);

      return {
        clienteId: c.cliente_id,
        nombre: c.nombre,
        saldoPendiente: Number(c.saldo_pendiente),
        diasSinPagar,
        promesasIncumplidas,
        scoreRiesgo: score,
        nivel: (score >= 70 ? 'alto' : score >= 40 ? 'medio' : 'bajo') as 'alto' | 'medio' | 'bajo',
      };
    }).sort((a, b) => b.scoreRiesgo - a.scoreRiesgo);
  }

  // ── P3: Proyección de ventas ──────────────────────────────────

  async proyeccionVentas(empresaId: string): Promise<ProyeccionVentas> {
    // Serie de los últimos 6 meses
    const hace6meses = new Date();
    hace6meses.setMonth(hace6meses.getMonth() - 6);

    const serie = await this.prisma.$queryRaw<{
      periodo: string;
      total: Prisma.Decimal;
    }[]>`
      SELECT
        TO_CHAR(DATE_TRUNC('month', dfeemide), 'YYYY-MM') AS periodo,
        COALESCE(SUM(total_factura), 0) AS total
      FROM factura_cab
      WHERE empresa_id = ${empresaId}::uuid
        AND dfeemide >= ${hace6meses}
        AND estado != 'Anulada'
      GROUP BY DATE_TRUNC('month', dfeemide)
      ORDER BY periodo ASC
    `;

    const valores = serie.map((s) => Number(s.total));
    const n = valores.length;

    if (n === 0) {
      return {
        periodos: [],
        tendencia: 'estable',
        proximoMesEstimado: 0,
        variacionPct: 0,
      };
    }

    // Promedio móvil de 3 meses como proyección
    const ultimos3 = valores.slice(-3);
    const promedio3 = ultimos3.reduce((a, b) => a + b, 0) / ultimos3.length;

    // Tendencia: comparar primera y segunda mitad
    const mitad = Math.floor(n / 2);
    const promPrimera = valores.slice(0, mitad).reduce((a, b) => a + b, 0) / (mitad || 1);
    const promSegunda = valores.slice(mitad).reduce((a, b) => a + b, 0) / ((n - mitad) || 1);
    const ratio = promPrimera > 0 ? promSegunda / promPrimera : 1;
    const tendencia: 'creciente' | 'estable' | 'decreciente' =
      ratio > 1.05 ? 'creciente' : ratio < 0.95 ? 'decreciente' : 'estable';

    // Variación último vs penúltimo mes
    const ultimoMes = valores[n - 1] ?? 0;
    const penultimoMes = valores[n - 2] ?? 0;
    const variacionPct = penultimoMes > 0
      ? Math.round(((ultimoMes - penultimoMes) / penultimoMes) * 100 * 10) / 10
      : 0;

    // Próximo mes: promedio móvil + factor tendencia
    const proximoMesEstimado = Math.round(promedio3 * (tendencia === 'creciente' ? 1.05 : tendencia === 'decreciente' ? 0.95 : 1));

    // Agregar período proyectado
    const hoy = new Date();
    const proximoPeriodo = `${hoy.getFullYear()}-${String(hoy.getMonth() + 2 > 12 ? 1 : hoy.getMonth() + 2).padStart(2, '0')}`;

    const periodos = [
      ...serie.map((s) => ({ periodo: s.periodo, real: Number(s.total) })),
      { periodo: proximoPeriodo, proyectado: proximoMesEstimado },
    ];

    return { periodos, tendencia, proximoMesEstimado, variacionPct };
  }

  // ── P4: Proyección de compras próximo mes ─────────────────────

  async proyeccionCompras(empresaId: string): Promise<ProyeccionVentas> {
    const hace6meses = new Date();
    hace6meses.setMonth(hace6meses.getMonth() - 6);

    const serie = await this.prisma.$queryRaw<{ periodo: string; total: Prisma.Decimal }[]>`
      SELECT
        TO_CHAR(DATE_TRUNC('month', fecha_emision), 'YYYY-MM') AS periodo,
        COALESCE(SUM(total * COALESCE(cotizacion, 1)), 0) AS total
      FROM compra_cab
      WHERE empresa_id = ${empresaId}::uuid
        AND fecha_emision >= ${hace6meses}
        AND estado != 'anulada'
      GROUP BY DATE_TRUNC('month', fecha_emision)
      ORDER BY periodo ASC
    `;

    const valores = serie.map((s) => Number(s.total));
    const n = valores.length;

    if (n === 0) return { periodos: [], tendencia: 'estable', proximoMesEstimado: 0, variacionPct: 0 };

    const ultimos3 = valores.slice(-3);
    const promedio3 = ultimos3.reduce((a, b) => a + b, 0) / ultimos3.length;
    const mitad = Math.floor(n / 2);
    const promPrimera = valores.slice(0, mitad).reduce((a, b) => a + b, 0) / (mitad || 1);
    const promSegunda = valores.slice(mitad).reduce((a, b) => a + b, 0) / ((n - mitad) || 1);
    const ratio = promPrimera > 0 ? promSegunda / promPrimera : 1;
    const tendencia: 'creciente' | 'estable' | 'decreciente' =
      ratio > 1.05 ? 'creciente' : ratio < 0.95 ? 'decreciente' : 'estable';

    const ultimoMes = valores[n - 1] ?? 0;
    const penultimoMes = valores[n - 2] ?? 0;
    const variacionPct = penultimoMes > 0
      ? Math.round(((ultimoMes - penultimoMes) / penultimoMes) * 100 * 10) / 10
      : 0;

    const proximoMesEstimado = Math.round(promedio3 * (tendencia === 'creciente' ? 1.05 : tendencia === 'decreciente' ? 0.95 : 1));

    const hoy = new Date();
    const proximoPeriodo = `${hoy.getFullYear()}-${String(hoy.getMonth() + 2 > 12 ? 1 : hoy.getMonth() + 2).padStart(2, '0')}`;

    return {
      periodos: [
        ...serie.map((s) => ({ periodo: s.periodo, real: Number(s.total) })),
        { periodo: proximoPeriodo, proyectado: proximoMesEstimado },
      ],
      tendencia,
      proximoMesEstimado,
      variacionPct,
    };
  }

  // ── P5: Productos para reordenar (stock bajo vs velocidad de ventas) ──

  async productosParaReordenar(empresaId: string): Promise<{ productoId: string; descripcion: string; stockActual: number; ventasMensuales: number; diasCobertura: number; urgencia: 'alta' | 'media' | 'baja' }[]> {
    const hace3meses = new Date();
    hace3meses.setMonth(hace3meses.getMonth() - 3);

    const productos = await this.prisma.$queryRaw<{
      producto_id: string;
      descripcion: string;
      stock_actual: Prisma.Decimal;
      ventas_3meses: Prisma.Decimal;
    }[]>`
      SELECT
        pr.id AS producto_id,
        pr.descripcion,
        COALESCE(SUM(sd.cantidad_disponible), 0) AS stock_actual,
        COALESCE(SUM(fd.dcantproser) FILTER (WHERE fc.dfeemide >= ${hace3meses}), 0) AS ventas_3meses
      FROM productos pr
      LEFT JOIN stock_deposito sd ON sd.producto_id = pr.id
      LEFT JOIN factura_det fd ON fd.producto_id = pr.id
      LEFT JOIN factura_cab fc ON fc.id = fd.factura_cab_id AND fc.empresa_id = ${empresaId}::uuid AND fc.estado != 'Anulada'
      WHERE pr.empresa_id = ${empresaId}::uuid
        AND pr.deleted = false
        AND pr.active = true
      GROUP BY pr.id, pr.descripcion
      HAVING COALESCE(SUM(sd.cantidad_disponible), 0) <= 20
        AND COALESCE(SUM(fd.dcantproser) FILTER (WHERE fc.dfeemide >= ${hace3meses}), 0) > 0
      ORDER BY stock_actual ASC
      LIMIT 15
    `;

    return productos.map((p) => {
      const stockActual = Number(p.stock_actual);
      const ventas3m = Number(p.ventas_3meses);
      const ventasMensuales = ventas3m / 3;
      const diasCobertura = ventasMensuales > 0 ? Math.round((stockActual / ventasMensuales) * 30) : 999;
      return {
        productoId: p.producto_id,
        descripcion: p.descripcion,
        stockActual,
        ventasMensuales: Math.round(ventasMensuales * 10) / 10,
        diasCobertura,
        urgencia: (diasCobertura <= 7 ? 'alta' : diasCobertura <= 15 ? 'media' : 'baja') as 'alta' | 'media' | 'baja',
      };
    }).sort((a, b) => a.diasCobertura - b.diasCobertura);
  }

  // ── P6: Proyección de flujo de caja (tesorería) ──────────────

  async proyeccionFlujoCaja(empresaId: string): Promise<{
    posicionActual: number;
    porCobrar30d: number;
    porPagar30d: number;
    flujoPrevisto: number;
    saldoEstimado: number;
    chequesEntrada: { numeroCheque: string; banco: string; monto: number; vencimiento: string }[];
    chequesSalida: { numeroCheque: string; banco: string; monto: number; vencimiento: string }[];
  }> {
    try {
      const [posicion, chequesRecibidos, chequesEmitidos] = await Promise.all([
        this.prisma.$queryRaw<{ total: Prisma.Decimal }[]>`
          SELECT COALESCE(SUM(saldo_actual), 0) AS total
          FROM tes_cuentas
          WHERE empresa_id = ${empresaId}::uuid AND activo = true AND moneda = 'PYG'
        `,
        this.prisma.$queryRaw<{ numero_cheque: string; banco_emisor: string; monto: Prisma.Decimal; fecha_vencimiento: Date }[]>`
          SELECT numero_cheque, banco_emisor, monto, fecha_vencimiento
          FROM tes_cheques
          WHERE empresa_id = ${empresaId}::uuid
            AND tipo = 'RECIBIDO' AND estado = 'EN_CARTERA'
            AND fecha_vencimiento <= CURRENT_DATE + INTERVAL '30 days'
          ORDER BY fecha_vencimiento ASC LIMIT 20
        `,
        this.prisma.$queryRaw<{ numero_cheque: string; banco_emisor: string; monto: Prisma.Decimal; fecha_vencimiento: Date }[]>`
          SELECT numero_cheque, banco_emisor, monto, fecha_vencimiento
          FROM tes_cheques
          WHERE empresa_id = ${empresaId}::uuid
            AND tipo = 'EMITIDO' AND estado = 'EN_CARTERA'
            AND fecha_vencimiento <= CURRENT_DATE + INTERVAL '30 days'
          ORDER BY fecha_vencimiento ASC LIMIT 20
        `,
      ]);

      const posicionActual = Number(posicion[0]?.total ?? 0);
      const porCobrar30d = chequesRecibidos.reduce((s, c) => s + Number(c.monto), 0);
      const porPagar30d = chequesEmitidos.reduce((s, c) => s + Number(c.monto), 0);
      const flujoPrevisto = porCobrar30d - porPagar30d;

      return {
        posicionActual,
        porCobrar30d,
        porPagar30d,
        flujoPrevisto,
        saldoEstimado: posicionActual + flujoPrevisto,
        chequesEntrada: chequesRecibidos.map((c) => ({
          numeroCheque: c.numero_cheque,
          banco: c.banco_emisor,
          monto: Number(c.monto),
          vencimiento: c.fecha_vencimiento instanceof Date ? c.fecha_vencimiento.toISOString().slice(0, 10) : String(c.fecha_vencimiento),
        })),
        chequesSalida: chequesEmitidos.map((c) => ({
          numeroCheque: c.numero_cheque,
          banco: c.banco_emisor,
          monto: Number(c.monto),
          vencimiento: c.fecha_vencimiento instanceof Date ? c.fecha_vencimiento.toISOString().slice(0, 10) : String(c.fecha_vencimiento),
        })),
      };
    } catch {
      return { posicionActual: 0, porCobrar30d: 0, porPagar30d: 0, flujoPrevisto: 0, saldoEstimado: 0, chequesEntrada: [], chequesSalida: [] };
    }
  }

  // ── P7: Proyección de nómina RRHH ─────────────────────────────

  async proyeccionNominaRRHH(
    empresaId: string,
    usuarioId?: string,
  ): Promise<{
    periodos: { periodo: string; real?: number; proyectado?: number }[];
    tendencia: 'creciente' | 'estable' | 'decreciente';
    proximoMesEstimado: number;
    variacionPct: number;
    ipsEstimado: number;
    empleadosActivos: number;
  } | null> {
    if (!(await this.rrhhContext.tieneModuloRRHH(empresaId))) return null;
    if (usuarioId && !(await this.rrhhContext.puedeVerRRHH(usuarioId, empresaId))) return null;

    try {
      const serie = await this.prisma.$queryRaw<{
        periodo: string; neto: Prisma.Decimal; ips: Prisma.Decimal;
      }[]>`
        SELECT
          TO_CHAR(MAKE_DATE(periodo_anio, periodo_mes, 1), 'YYYY-MM') AS periodo,
          COALESCE(SUM(total_neto), 0) AS neto,
          COALESCE(SUM(total_ips_obrero + total_ips_patronal + total_ips_admin), 0) AS ips
        FROM rrhh_liquidaciones_cabecera
        WHERE empresa_id = ${empresaId}::uuid
          AND tipo = 'MENSUAL'
          AND estado IN ('CERRADA','CALCULADA')
          AND MAKE_DATE(periodo_anio, periodo_mes, 1) >= CURRENT_DATE - INTERVAL '6 months'
        GROUP BY periodo_anio, periodo_mes
        ORDER BY periodo ASC
      `;

      const activos = await this.prisma.$queryRaw<{ cant: number }[]>`
        SELECT COUNT(*)::int AS cant FROM rrhh_empleados
        WHERE empresa_id = ${empresaId}::uuid AND estado = 'ACTIVO'
      `;
      const empleadosActivos = activos[0]?.cant ?? 0;

      const valores = serie.map((s) => Number(s.neto));
      const ipsValores = serie.map((s) => Number(s.ips));
      const n = valores.length;

      if (n === 0) {
        return {
          periodos: [],
          tendencia: 'estable',
          proximoMesEstimado: 0,
          variacionPct: 0,
          ipsEstimado: 0,
          empleadosActivos,
        };
      }

      const ultimos3 = valores.slice(-3);
      const promedio3 = ultimos3.reduce((a, b) => a + b, 0) / ultimos3.length;
      const promIps = ipsValores.slice(-3).reduce((a, b) => a + b, 0) / Math.max(ipsValores.slice(-3).length, 1);

      const mitad = Math.floor(n / 2);
      const promPrimera = valores.slice(0, mitad).reduce((a, b) => a + b, 0) / (mitad || 1);
      const promSegunda = valores.slice(mitad).reduce((a, b) => a + b, 0) / ((n - mitad) || 1);
      const ratio = promPrimera > 0 ? promSegunda / promPrimera : 1;
      const tendencia: 'creciente' | 'estable' | 'decreciente' =
        ratio > 1.05 ? 'creciente' : ratio < 0.95 ? 'decreciente' : 'estable';

      const ultimoMes = valores[n - 1] ?? 0;
      const penultimoMes = valores[n - 2] ?? 0;
      const variacionPct = penultimoMes > 0
        ? Math.round(((ultimoMes - penultimoMes) / penultimoMes) * 100 * 10) / 10
        : 0;

      const factor = tendencia === 'creciente' ? 1.05 : tendencia === 'decreciente' ? 0.95 : 1;
      const proximoMesEstimado = Math.round(promedio3 * factor);
      const ipsEstimado = Math.round(promIps * factor);

      const hoy = new Date();
      const proximoPeriodo = `${hoy.getFullYear()}-${String(hoy.getMonth() + 2 > 12 ? 1 : hoy.getMonth() + 2).padStart(2, '0')}`;

      return {
        periodos: [
          ...serie.map((s) => ({ periodo: s.periodo, real: Number(s.neto) })),
          { periodo: proximoPeriodo, proyectado: proximoMesEstimado },
        ],
        tendencia,
        proximoMesEstimado,
        variacionPct,
        ipsEstimado,
        empleadosActivos,
      };
    } catch (e) {
      this.logger.warn(`proyección nómina RRHH: ${(e as Error).message}`);
      return null;
    }
  }

  async empleadosRiesgoDesvinculacion(
    empresaId: string,
    usuarioId?: string,
  ): Promise<{ empleadoId: string; nombre: string; faltas3m: number; anticipos3m: number; score: number }[] | null> {
    if (!(await this.rrhhContext.tieneModuloRRHH(empresaId))) return null;
    if (usuarioId && !(await this.rrhhContext.puedeVerRRHH(usuarioId, empresaId))) return null;

    try {
      const rows = await this.prisma.$queryRaw<{
        empleado_id: string; nombre: string; faltas: number; anticipos: number;
      }[]>`
        WITH base AS (
          SELECT e.id AS empleado_id, e.nombres || ' ' || e.apellidos AS nombre
          FROM rrhh_empleados e
          WHERE e.empresa_id = ${empresaId}::uuid AND e.estado = 'ACTIVO'
        ),
        f AS (
          SELECT empleado_id, COUNT(*)::int AS faltas
          FROM rrhh_asistencia_novedades
          WHERE empresa_id = ${empresaId}::uuid
            AND tipo_novedad IN ('FALTA','TARDANZA')
            AND COALESCE(justificado, false) = false
            AND fecha >= CURRENT_DATE - INTERVAL '3 months'
          GROUP BY empleado_id
        ),
        a AS (
          SELECT empleado_id, COUNT(*)::int AS anticipos
          FROM rrhh_anticipos_salario
          WHERE empresa_id = ${empresaId}::uuid
            AND fecha_solicitud >= CURRENT_DATE - INTERVAL '3 months'
          GROUP BY empleado_id
        )
        SELECT b.empleado_id, b.nombre,
               COALESCE(f.faltas, 0) AS faltas,
               COALESCE(a.anticipos, 0) AS anticipos
        FROM base b
        LEFT JOIN f ON f.empleado_id = b.empleado_id
        LEFT JOIN a ON a.empleado_id = b.empleado_id
        WHERE COALESCE(f.faltas, 0) >= 3 OR COALESCE(a.anticipos, 0) >= 3
        LIMIT 20
      `;

      return rows
        .map((r) => {
          const faltas = Number(r.faltas);
          const anticipos = Number(r.anticipos);
          const score = Math.min(100, Math.round(faltas * 12 + anticipos * 8));
          return {
            empleadoId: r.empleado_id,
            nombre: r.nombre,
            faltas3m: faltas,
            anticipos3m: anticipos,
            score,
          };
        })
        .sort((a, b) => b.score - a.score);
    } catch (e) {
      this.logger.warn(`riesgo desvinculación: ${(e as Error).message}`);
      return [];
    }
  }

  // ── Todas las predicciones juntas ─────────────────────────────

  async todasLasPredicciones(empresaId: string, usuarioId?: string) {
    const [cobranza, riesgo, ventas, compras, reorden, flujoCaja, nominaRRHH, empleadosRiesgo] = await Promise.all([
      this.proyeccionCobranza(empresaId).catch((e) => {
        this.logger.error('Error proyección cobranza', e);
        return null;
      }),
      this.clientesEnRiesgo(empresaId).catch((e) => {
        this.logger.error('Error clientes riesgo', e);
        return null;
      }),
      this.proyeccionVentas(empresaId).catch((e) => {
        this.logger.error('Error proyección ventas', e);
        return null;
      }),
      this.proyeccionCompras(empresaId).catch((e) => {
        this.logger.error('Error proyección compras', e);
        return null;
      }),
      this.productosParaReordenar(empresaId).catch((e) => {
        this.logger.error('Error productos para reordenar', e);
        return null;
      }),
      this.proyeccionFlujoCaja(empresaId).catch((e) => {
        this.logger.error('Error proyección flujo caja', e);
        return null;
      }),
      this.proyeccionNominaRRHH(empresaId, usuarioId).catch((e) => {
        this.logger.error('Error proyección nómina RRHH', e);
        return null;
      }),
      this.empleadosRiesgoDesvinculacion(empresaId, usuarioId).catch((e) => {
        this.logger.error('Error empleados riesgo desvinculación', e);
        return null;
      }),
    ]);

    return { cobranza, clientesEnRiesgo: riesgo, ventas, compras, reorden, flujoCaja, nominaRRHH, empleadosRiesgo };
  }
}
