import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';

export type TipoEventoTimeline =
  | 'actividad' | 'oportunidad' | 'presupuesto' | 'factura' | 'nota_credito' | 'cobro' | 'lead';

export interface EventoTimeline {
  tipo: TipoEventoTimeline;
  id: string;
  fecha: Date;
  titulo: string;
  detalle?: string | null;
  monto?: number | null;
  estado?: string | null;
  /** Para que el front sepa a dónde navegar sin inventar rutas. */
  ref?: Record<string, unknown>;
}

/**
 * Ficha 360 del cliente (M92). Ver docs/spec-crm-360.md §6.3.
 *
 * Es sólo lectura: junta en una línea de tiempo lo que ya vive en sus módulos.
 * El CRM no copia nada — si duplicáramos facturas o cobros acá, el día que se
 * anule una factura el timeline seguiría mostrándola como válida.
 *
 * Paginación por cursor de FECHA, no por offset: las fuentes son varias y cada
 * una crece por su lado, así que un `skip` numérico se desalinea en cuanto
 * aparece un evento nuevo mientras el usuario scrollea.
 */
@Injectable()
export class TimelineService {
  constructor(private readonly prisma: PrismaService) {}

  private async clienteDeEmpresa(cliente_id: string, empresa_id: string) {
    const cliente = await this.prisma.clientes.findFirst({
      where: { id: cliente_id, deleted: false, personas: { empresa_id } },
      include: {
        personas: { select: { razon_social: true, ruc: true, email: true, celular: true } },
      },
    });
    if (!cliente) throw new NotFoundException('Cliente no encontrado');
    return cliente;
  }

  /**
   * Cabecera de la ficha: lo que el vendedor necesita ver ANTES de ofrecer algo.
   * El saldo y el límite salen de `clientes`, que es donde cobranzas los mantiene.
   */
  /**
   * @param verSaldo El bloque de crédito es dato sensible y la spec le da un
   * permiso propio (`CRM_CLI_SALDO_VER`). Sin él la cabecera se arma igual, pero
   * sin saldo ni límite: un vendedor externo puede ver el historial del cliente
   * sin enterarse de cuánto debe.
   */
  async resumen(cliente_id: string, empresa_id: string, verSaldo = true) {
    const cliente = await this.clienteDeEmpresa(cliente_id, empresa_id);

    const hace12Meses = new Date();
    hace12Meses.setMonth(hace12Meses.getMonth() - 12);

    const [facturado12m, historico, abiertas, ultimaActividad] = await Promise.all([
      this.facturadoDesde(empresa_id, cliente_id, hace12Meses),
      this.facturadoDesde(empresa_id, cliente_id, null),
      this.prisma.crm_oportunidades.aggregate({
        where: { empresa_id, cliente_id, estado: 'abierta' },
        _sum: { valor_unico: true, valor_mrr: true },
        _count: { _all: true },
      }),
      this.prisma.crm_actividades.findFirst({
        where: { empresa_id, cliente_id, estado: 'realizada' },
        orderBy: { fecha_realizada: 'desc' },
        select: { fecha_realizada: true, tipo: true, asunto: true },
      }),
    ]);

    const saldo = Number(cliente.saldo_pendiente ?? 0);
    const limite = Number(cliente.limite_credito ?? 0);

    return {
      cliente: {
        id: cliente.id,
        razon_social: cliente.personas?.razon_social ?? '',
        ruc: cliente.personas?.ruc ?? null,
        email: cliente.personas?.email ?? null,
        telefono: cliente.telefono_e164 ?? cliente.personas?.celular ?? null,
        etiquetas: cliente.etiquetas ?? [],
        segmento: cliente.segmento ?? null,
      },
      // LTV: todo lo facturado no anulado. Es la métrica más honesta que se
      // puede dar hoy; afinar por cobrado real queda para cuando F3 traiga MRR.
      ltv: historico.total,
      facturas_emitidas: historico.cantidad,
      facturado_12m: facturado12m.total,
      credito: verSaldo
        ? {
            limite,
            saldo_pendiente: saldo,
            disponible: limite > 0 ? limite - saldo : null,
            habilitado: cliente.credito_habilitado !== false,
            bloqueado: cliente.bloqueado_credito === true,
          }
        : null,
      pipeline: {
        oportunidades_abiertas: abiertas._count._all,
        valor_abierto: Number(abiertas._sum.valor_unico ?? 0),
        mrr_en_juego: Number(abiertas._sum.valor_mrr ?? 0),
      },
      ultimo_contacto: ultimaActividad
        ? { fecha: ultimaActividad.fecha_realizada, tipo: ultimaActividad.tipo, asunto: ultimaActividad.asunto }
        : null,
    };
  }

  /**
   * Lo facturado al cliente, desde una fecha o desde siempre.
   *
   * **`factura_cab.total_factura` está siempre en NULL en este sistema** y así lo
   * advierte `src/ai-dashboard/chat/schema-context.ts`: "NO USAR". La primera
   * versión de este servicio la usó igual, así que el LTV y lo facturado en 12
   * meses mostraban 0 para todos los clientes. Se vio cargando datos de prueba.
   *
   * La fuente real es `factura_subtotales.dtotgralope`, en la moneda de la
   * factura, y se lleva a guaraníes con `COALESCE(dticam, 1)` — la cotización
   * congelada al emitir. Es raro que haya moneda extranjera (7 facturas de 12.233
   * en producción), pero sin eso esas siete quedarían mal por un factor de 7.000.
   *
   * Va en SQL porque un `aggregate` de Prisma no puede multiplicar dos columnas.
   * Queda fuera del alcance de `crm-multitenancy.spec.ts`, que sólo mira los
   * `where` de las llamadas del cliente: el filtro por empresa está en el SQL.
   */
  private async facturadoDesde(empresa_id: string, cliente_id: string, desde: Date | null) {
    const filas = await this.prisma.$queryRaw<{ total: number | null; cantidad: bigint }[]>`
      SELECT COALESCE(SUM(fs.dtotgralope * COALESCE(fc.dticam, 1)), 0)::float8 AS total,
             COUNT(*)::bigint AS cantidad
        FROM factura_cab fc
        JOIN factura_subtotales fs ON fs.factura_cab_id = fc.id
       WHERE fc.empresa_id = ${empresa_id}::uuid
         AND fc.cliente_id = ${cliente_id}::uuid
         AND fc.estado <> 'Anulado'
         AND (${desde}::timestamp IS NULL OR fc.dfeemide >= ${desde}::timestamp)
    `;
    const fila = filas[0];
    return { total: Number(fila?.total ?? 0), cantidad: Number(fila?.cantidad ?? 0) };
  }

  /**
   * Línea de tiempo unificada.
   *
   * Cada fuente trae `limite + 1` eventos anteriores al cursor; después se
   * mezclan, se ordenan y se corta. Traer de más es necesario: si una fuente
   * aportara exactamente `limite`, al mezclarla con otra quedarían eventos
   * fuera de la página que nunca se verían al pedir la siguiente.
   */
  async timeline(
    cliente_id: string,
    empresa_id: string,
    opciones: { cursor?: string; limite?: number; tipos?: TipoEventoTimeline[] } = {},
  ) {
    await this.clienteDeEmpresa(cliente_id, empresa_id);

    const limite = Math.min(Math.max(opciones.limite ?? 30, 1), 100);
    const antes = opciones.cursor ? new Date(opciones.cursor) : null;
    const quiere = (t: TipoEventoTimeline) => !opciones.tipos?.length || opciones.tipos.includes(t);
    const corte = <T extends string>(campo: T) => (antes ? { [campo]: { lt: antes } } : {});
    const tomar = limite + 1;

    const eventos: EventoTimeline[] = [];

    if (quiere('actividad')) {
      const filas = await this.prisma.crm_actividades.findMany({
        where: { empresa_id, cliente_id, ...corte('fecha_programada') },
        orderBy: { fecha_programada: 'desc' },
        take: tomar,
      });
      eventos.push(...filas.map((a) => ({
        tipo: 'actividad' as const,
        id: a.id,
        fecha: a.fecha_realizada ?? a.fecha_programada,
        titulo: `${a.tipo}: ${a.asunto}`,
        detalle: a.resultado ?? a.detalle,
        estado: a.estado,
        ref: { oportunidad_id: a.oportunidad_id },
      })));
    }

    if (quiere('oportunidad')) {
      const filas = await this.prisma.crm_oportunidades.findMany({
        where: { empresa_id, cliente_id, ...corte('created_at') },
        orderBy: { created_at: 'desc' },
        take: tomar,
      });
      eventos.push(...filas.map((o) => ({
        tipo: 'oportunidad' as const,
        id: o.id,
        fecha: o.created_at,
        titulo: `${o.numero} · ${o.titulo}`,
        monto: Number(o.valor_unico) + Number(o.valor_mrr),
        estado: o.estado,
        ref: { numero: o.numero, etapa_id: o.etapa_id },
      })));
    }

    if (quiere('presupuesto')) {
      const filas = await this.prisma.presupuesto_cab.findMany({
        where: { empresa_id, cliente_id, ...corte('fecha_emision') },
        orderBy: { fecha_emision: 'desc' },
        take: tomar,
        select: { id: true, numero: true, fecha_emision: true, total: true, estado: true },
      });
      eventos.push(...filas.map((p) => ({
        tipo: 'presupuesto' as const,
        id: p.id,
        fecha: p.fecha_emision,
        titulo: `Presupuesto ${p.numero}`,
        monto: Number(p.total ?? 0),
        estado: String(p.estado ?? ''),
      })));
    }

    if (quiere('factura')) {
      const filas = await this.prisma.factura_cab.findMany({
        where: { empresa_id, cliente_id, ...corte('dfeemide') },
        orderBy: { dfeemide: 'desc' },
        take: tomar,
        select: {
          id: true, dest: true, dpunexp: true, dnumdoc: true, dfeemide: true,
          estado: true, estado_sifen: true, dticam: true,
          // El monto vive en factura_subtotales, no en la cabecera: ver el método
          // `facturadoDesde` y el aviso de src/ai-dashboard/chat/schema-context.ts.
          factura_subtotales: { select: { dtotgralope: true } },
        },
      });
      eventos.push(...filas.map((f) => ({
        tipo: 'factura' as const,
        id: f.id,
        fecha: f.dfeemide,
        titulo: `Factura ${f.dest}-${f.dpunexp}-${f.dnumdoc}`,
        monto: Number(f.factura_subtotales?.[0]?.dtotgralope ?? 0) * Number(f.dticam ?? 1),
        estado: f.estado,
        // El estado SIFEN viaja aparte: "Pendiente" local con SIFEN en error no
        // es lo mismo que "Pendiente" a secas, y el vendedor tiene que verlo.
        ref: { estado_sifen: f.estado_sifen },
      })));
    }

    if (quiere('nota_credito')) {
      const filas = await this.prisma.nota_credito_cab.findMany({
        where: { empresa_id, cliente_id, ...corte('dfeemide') },
        orderBy: { dfeemide: 'desc' },
        take: tomar,
        select: {
          id: true, dest: true, dpunexp: true, dnumdoc: true, dfeemide: true, estado: true,
          nota_credito_subtotal: { select: { dtotope: true } },
        },
      });
      eventos.push(...filas.map((n) => ({
        tipo: 'nota_credito' as const,
        id: n.id,
        fecha: n.dfeemide,
        titulo: `Nota de crédito ${n.dest}-${n.dpunexp}-${n.dnumdoc}`,
        monto: Number(n.nota_credito_subtotal?.[0]?.dtotope ?? 0),
        estado: n.estado,
      })));
    }

    if (quiere('cobro')) {
      const filas = await this.prisma.recibos_cobro.findMany({
        where: { empresa_id, cliente_id, ...corte('fecha_emision') },
        orderBy: { fecha_emision: 'desc' },
        take: tomar,
        select: { id: true, numero_recibo: true, fecha_emision: true, monto_total: true, estado: true },
      });
      eventos.push(...filas.map((r) => ({
        tipo: 'cobro' as const,
        id: r.id,
        fecha: r.fecha_emision,
        titulo: `Recibo ${r.numero_recibo}`,
        monto: Number(r.monto_total ?? 0),
        estado: r.estado,
      })));
    }

    if (quiere('lead')) {
      const filas = await this.prisma.crm_leads.findMany({
        where: { empresa_id, cliente_id, ...corte('created_at') },
        orderBy: { created_at: 'desc' },
        take: tomar,
        select: { id: true, nombre: true, created_at: true, estado: true, campana: true },
      });
      eventos.push(...filas.map((l) => ({
        tipo: 'lead' as const,
        id: l.id,
        fecha: l.created_at,
        titulo: `Lead: ${l.nombre}`,
        detalle: l.campana,
        estado: l.estado,
      })));
    }

    const ordenados = eventos
      .filter((e) => e.fecha)
      .sort((a, b) => new Date(b.fecha).getTime() - new Date(a.fecha).getTime());

    const pagina = ordenados.slice(0, limite);
    const hay_mas = ordenados.length > limite;

    return {
      eventos: pagina,
      // El cursor es la fecha del último evento devuelto: la próxima página pide
      // lo anterior a eso.
      siguiente_cursor: hay_mas && pagina.length ? pagina[pagina.length - 1].fecha : null,
      hay_mas,
    };
  }
}
