import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import type { AlcanceVisibilidad } from '../oportunidades/oportunidades.service';

export interface RangoReporte {
  desde?: string;
  hasta?: string;
  pipeline_id?: string;
}

/**
 * Reportes de F1 (ticket CRM-19). Ver docs/spec-crm-360.md §6.13 (M102).
 *
 * De la lista de la spec, F1 cubre lo de **Ventas**: embudo, conversión por
 * origen, ciclo de venta, motivos de pérdida y ranking de vendedores. Actividad
 * queda para cuando haya volumen con el que valga la pena mirarla, y Atención,
 * Recurrente y Marketing dependen de F2/F3 (tickets, contratos, Meta Ads).
 *
 * Todo respeta el alcance de visibilidad de CRM-12: un vendedor sin
 * `VER_AJENAS` ve su propio embudo, no el de la empresa.
 */
@Injectable()
export class ReportesCrmService {
  constructor(private readonly prisma: PrismaService) {}

  /** Embudo: cuántas oportunidades y cuánta plata hay en cada etapa, hoy. */
  async pipeline(empresa_id: string, alcance: AlcanceVisibilidad, filtros: RangoReporte) {
    const etapas = await this.prisma.crm_etapas.findMany({
      where: { empresa_id, activo: true, ...(filtros.pipeline_id ? { pipeline_id: filtros.pipeline_id } : {}) },
      orderBy: { orden: 'asc' },
    });

    const abiertas = await this.prisma.crm_oportunidades.groupBy({
      by: ['etapa_id'],
      where: { empresa_id, estado: 'abierta', ...this.visibilidad(alcance), ...this.rango(filtros) },
      _count: { _all: true },
      _sum: { valor_unico: true, valor_mrr: true },
    });

    const porEtapa = new Map(abiertas.map((a) => [a.etapa_id, a]));

    const filas = etapas
      .filter((e) => !e.es_ganada && !e.es_perdida)
      .map((e) => {
        const d = porEtapa.get(e.id);
        const valor = Number(d?._sum.valor_unico ?? 0);
        return {
          etapa_id: e.id,
          etapa: e.nombre,
          orden: e.orden,
          probabilidad: e.probabilidad,
          cantidad: d?._count._all ?? 0,
          valor,
          valor_mrr: Number(d?._sum.valor_mrr ?? 0),
          // Valor ponderado: lo que la spec llama forecast. Sumar el total crudo
          // del embudo siempre da un número que no se parece a lo que se cobra.
          valor_ponderado: Math.round(valor * (e.probabilidad / 100)),
        };
      });

    return {
      data: {
        etapas: filas,
        totales: {
          cantidad: filas.reduce((a, f) => a + f.cantidad, 0),
          valor: filas.reduce((a, f) => a + f.valor, 0),
          valor_ponderado: filas.reduce((a, f) => a + f.valor_ponderado, 0),
          valor_mrr: filas.reduce((a, f) => a + f.valor_mrr, 0),
        },
      },
    };
  }

  /**
   * Conversión por origen: de los leads que entraron por cada canal, cuántos
   * terminaron en venta. Es el reporte que decide dónde poner la plata de
   * publicidad.
   */
  async conversion(empresa_id: string, alcance: AlcanceVisibilidad, filtros: RangoReporte) {
    const origenes = await this.prisma.crm_origenes.findMany({
      where: { empresa_id },
      select: { id: true, codigo: true, descripcion: true },
    });

    const [leads, oportunidades] = await Promise.all([
      this.prisma.crm_leads.groupBy({
        by: ['origen_id'],
        where: { empresa_id, ...this.rango(filtros) },
        _count: { _all: true },
      }),
      this.prisma.crm_oportunidades.groupBy({
        by: ['origen_id', 'estado'],
        where: { empresa_id, ...this.visibilidad(alcance), ...this.rango(filtros) },
        _count: { _all: true },
        _sum: { valor_unico: true },
      }),
    ]);

    const leadsPor = new Map(leads.map((l) => [l.origen_id ?? 'sin', l._count._all]));

    const filas = [...origenes, { id: null, codigo: 'SIN_ORIGEN', descripcion: 'Sin origen' }].map((o) => {
      const clave = o.id ?? 'sin';
      const propias = oportunidades.filter((x) => (x.origen_id ?? 'sin') === clave);
      const total = propias.reduce((a, x) => a + x._count._all, 0);
      const ganadas = propias.filter((x) => x.estado === 'ganada');
      const cantidadGanadas = ganadas.reduce((a, x) => a + x._count._all, 0);
      const leadsOrigen = leadsPor.get(clave) ?? 0;

      return {
        origen_id: o.id,
        origen: o.descripcion ?? o.codigo,
        leads: leadsOrigen,
        oportunidades: total,
        ganadas: cantidadGanadas,
        perdidas: propias.filter((x) => x.estado === 'perdida').reduce((a, x) => a + x._count._all, 0),
        monto_ganado: ganadas.reduce((a, x) => a + Number(x._sum.valor_unico ?? 0), 0),
        // Sobre oportunidades cerradas, no sobre todas: las abiertas todavía no
        // ganaron ni perdieron y contarlas como fracaso hunde el porcentaje.
        tasa_conversion: porcentaje(cantidadGanadas, total - propias.filter((x) => x.estado === 'abierta').reduce((a, x) => a + x._count._all, 0)),
      };
    });

    return { data: filas.filter((f) => f.leads > 0 || f.oportunidades > 0) };
  }

  /** Por qué se pierde. Sin esto el embudo dice que algo anda mal pero no qué. */
  async motivosPerdida(empresa_id: string, alcance: AlcanceVisibilidad, filtros: RangoReporte) {
    const perdidas = await this.prisma.crm_oportunidades.groupBy({
      by: ['motivo_perdida_id'],
      where: { empresa_id, estado: 'perdida', ...this.visibilidad(alcance), ...this.rango(filtros) },
      _count: { _all: true },
      _sum: { valor_unico: true },
    });

    const ids = perdidas.map((p) => p.motivo_perdida_id).filter((x): x is string => !!x);
    const motivos = ids.length
      ? await this.prisma.crm_motivos.findMany({ where: { id: { in: ids } }, select: { id: true, descripcion: true } })
      : [];
    const nombre = new Map(motivos.map((m) => [m.id, m.descripcion]));
    const total = perdidas.reduce((a, p) => a + p._count._all, 0);

    return {
      data: perdidas
        .map((p) => ({
          motivo_id: p.motivo_perdida_id,
          motivo: p.motivo_perdida_id ? nombre.get(p.motivo_perdida_id) ?? 'Motivo dado de baja' : 'Sin motivo',
          cantidad: p._count._all,
          monto: Number(p._sum.valor_unico ?? 0),
          porcentaje: porcentaje(p._count._all, total),
        }))
        .sort((a, b) => b.cantidad - a.cantidad),
    };
  }

  /** Ranking de vendedores por lo que cerraron, no por lo que tienen abierto. */
  async vendedores(empresa_id: string, alcance: AlcanceVisibilidad, filtros: RangoReporte) {
    const porVendedor = await this.prisma.crm_oportunidades.groupBy({
      by: ['vendedor_id', 'estado'],
      where: { empresa_id, ...this.visibilidad(alcance), ...this.rango(filtros) },
      _count: { _all: true },
      _sum: { valor_unico: true },
    });

    const ids = porVendedor.map((v) => v.vendedor_id).filter((x): x is string => !!x);
    const vendedores = ids.length
      ? await this.prisma.vendedores_cobradores.findMany({
          where: { id: { in: ids } },
          select: { id: true, nombre: true, apellido: true },
        })
      : [];
    const nombre = new Map(vendedores.map((v) => [v.id, [v.nombre, v.apellido].filter(Boolean).join(' ')]));

    const claves = [...new Set(porVendedor.map((v) => v.vendedor_id ?? 'sin'))];
    const filas = claves.map((clave) => {
      const propias = porVendedor.filter((x) => (x.vendedor_id ?? 'sin') === clave);
      const de = (estado: string) => propias.filter((x) => x.estado === estado);
      const cant = (estado: string) => de(estado).reduce((a, x) => a + x._count._all, 0);
      const monto = (estado: string) => de(estado).reduce((a, x) => a + Number(x._sum.valor_unico ?? 0), 0);
      const ganadas = cant('ganada');
      const cerradas = ganadas + cant('perdida');

      return {
        vendedor_id: clave === 'sin' ? null : clave,
        vendedor: clave === 'sin' ? 'Sin asignar' : nombre.get(clave) ?? 'Vendedor dado de baja',
        abiertas: cant('abierta'),
        monto_abierto: monto('abierta'),
        ganadas,
        monto_ganado: monto('ganada'),
        perdidas: cant('perdida'),
        tasa_cierre: porcentaje(ganadas, cerradas),
      };
    });

    return { data: filas.sort((a, b) => b.monto_ganado - a.monto_ganado) };
  }

  /**
   * Ciclo de venta: cuántos días pasan entre crear la oportunidad y ganarla.
   *
   * Devuelve promedio y **mediana**. El promedio solo engaña: una venta de ocho
   * meses entre veinte de una semana lo duplica, y la mediana sigue diciendo la
   * verdad de lo que tarda una venta normal.
   */
  async cicloVenta(empresa_id: string, alcance: AlcanceVisibilidad, filtros: RangoReporte) {
    const ganadas = await this.prisma.crm_oportunidades.findMany({
      where: {
        empresa_id, estado: 'ganada', fecha_cierre_real: { not: null },
        ...this.visibilidad(alcance), ...this.rango(filtros),
      },
      select: { created_at: true, fecha_cierre_real: true, valor_unico: true },
    });

    const dias = ganadas
      .map((o) => {
        if (!o.created_at || !o.fecha_cierre_real) return null;
        const ms = o.fecha_cierre_real.getTime() - o.created_at.getTime();
        return Math.max(0, Math.round(ms / 86_400_000));
      })
      .filter((d): d is number => d !== null)
      .sort((a, b) => a - b);

    return {
      data: {
        ganadas: dias.length,
        monto_ganado: ganadas.reduce((a, o) => a + Number(o.valor_unico), 0),
        dias_promedio: dias.length ? Math.round(dias.reduce((a, d) => a + d, 0) / dias.length) : 0,
        dias_mediana: mediana(dias),
        dias_minimo: dias[0] ?? 0,
        dias_maximo: dias[dias.length - 1] ?? 0,
      },
    };
  }

  /** Los cinco reportes de una sola vez: es lo que pinta el dashboard. */
  async resumen(empresa_id: string, alcance: AlcanceVisibilidad, filtros: RangoReporte) {
    const [pipeline, conversion, motivos, vendedores, ciclo] = await Promise.all([
      this.pipeline(empresa_id, alcance, filtros),
      this.conversion(empresa_id, alcance, filtros),
      this.motivosPerdida(empresa_id, alcance, filtros),
      this.vendedores(empresa_id, alcance, filtros),
      this.cicloVenta(empresa_id, alcance, filtros),
    ]);

    return {
      data: {
        pipeline: pipeline.data,
        conversion_por_origen: conversion.data,
        motivos_perdida: motivos.data,
        vendedores: vendedores.data,
        ciclo_venta: ciclo.data,
      },
    };
  }

  // ---------------------------------------------------------------------------

  /**
   * Mismo criterio que CRM-12: sin `VER_AJENAS` se filtra por el vendedor del
   * usuario, y si no tiene ficha de vendedor no ve nada en vez de verlo todo.
   */
  private visibilidad(alcance: AlcanceVisibilidad): Prisma.crm_oportunidadesWhereInput {
    if (alcance.verAjenas) return {};
    return { vendedor_id: alcance.vendedor_id ?? '00000000-0000-0000-0000-000000000000' };
  }

  /** El rango se aplica sobre la fecha de alta: es la única que tienen todas. */
  private rango(filtros: RangoReporte) {
    if (!filtros.desde && !filtros.hasta) return {};
    return {
      created_at: {
        ...(filtros.desde ? { gte: new Date(filtros.desde) } : {}),
        // `hasta` inclusive: quien pide "hasta el 31" espera que el 31 entre.
        ...(filtros.hasta ? { lt: sumarUnDia(filtros.hasta) } : {}),
      },
    };
  }
}

export function porcentaje(parte: number, total: number): number {
  if (!total) return 0;
  return Math.round((parte / total) * 1000) / 10;
}

export function mediana(ordenados: number[]): number {
  if (!ordenados.length) return 0;
  const medio = Math.floor(ordenados.length / 2);
  if (ordenados.length % 2) return ordenados[medio];
  return Math.round((ordenados[medio - 1] + ordenados[medio]) / 2);
}

function sumarUnDia(iso: string): Date {
  const d = new Date(iso);
  d.setDate(d.getDate() + 1);
  return d;
}
