import { Injectable, Logger } from '@nestjs/common';
import { Prisma, presupuesto_estado } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { alcanceSucursalUsuario, filtroSucursalOperativo } from 'src/utils/alcance-sucursal';

type MesRow = { mes: string; count: number; monto: number };

/** Estados que implican que el presupuesto fue enviado al cliente. */
const ESTADOS_ENVIADOS: presupuesto_estado[] = [
  presupuesto_estado.SENT,
  presupuesto_estado.VIEWED,
  presupuesto_estado.ACCEPTED,
  presupuesto_estado.CONVERTED,
  presupuesto_estado.REJECTED,
  presupuesto_estado.EXPIRED,
];
const PRESUPUESTO_ESTADO_FACTURADO = 'FACTURADO' as unknown as presupuesto_estado;
ESTADOS_ENVIADOS.push(PRESUPUESTO_ESTADO_FACTURADO);
const ESTADOS_PIPELINE: presupuesto_estado[] = [
  presupuesto_estado.SENT,
  presupuesto_estado.VIEWED,
  presupuesto_estado.ACCEPTED,
];

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

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Pipeline comercial: resumen, embudo, distribución por estado y serie mensual.
   * Mismo alcance por sucursal (y rubro del selector) que el listado: sin esto
   * el widget del dashboard mostraba los presupuestos de toda la empresa.
   */
  async getDashboard(empresa_id: string, desde?: string, hasta?: string, usuarioId?: string) {
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresa_id);
    const filtroSucursal = filtroSucursalOperativo(alcance);
    const sucursalIds = filtroSucursal === undefined ? null : typeof filtroSucursal === 'string' ? [filtroSucursal] : filtroSucursal.in;
    const rangoFecha =
      desde || hasta
        ? {
            fecha_emision: {
              ...(desde ? { gte: new Date(desde) } : {}),
              ...(hasta ? { lte: new Date(hasta) } : {}),
            },
          }
        : {};

    const whereBase: Prisma.presupuesto_cabWhereInput = {
      empresa_id,
      deleted: false,
      ...rangoFecha,
      ...(filtroSucursal !== undefined ? { sucursal_id: filtroSucursal } : {}),
    };

    const [grupos, config, porMes] = await Promise.all([
      this.prisma.presupuesto_cab.groupBy({
        by: ['estado'],
        where: whereBase,
        _count: { _all: true },
        _sum: { total: true },
      }),
      this.prisma.presupuesto_config.findUnique({ where: { empresa_id } }),
      this.serieMensual(empresa_id, sucursalIds),
    ]);

    const countDe = (estados: presupuesto_estado[]) =>
      grupos.filter((g) => estados.includes(g.estado)).reduce((s, g) => s + g._count._all, 0);
    const montoDe = (estados: presupuesto_estado[]) =>
      grupos.filter((g) => estados.includes(g.estado)).reduce((s, g) => s + Number(g._sum.total ?? 0), 0);

    const total = grupos.reduce((s, g) => s + g._count._all, 0);
    const montoTotal = grupos.reduce((s, g) => s + Number(g._sum.total ?? 0), 0);
    const enviados = countDe(ESTADOS_ENVIADOS);
    const vistos = countDe([
      presupuesto_estado.VIEWED,
      presupuesto_estado.ACCEPTED,
      presupuesto_estado.CONVERTED,
      PRESUPUESTO_ESTADO_FACTURADO,
    ]);
    const aceptados = countDe([
      presupuesto_estado.ACCEPTED,
      presupuesto_estado.CONVERTED,
      PRESUPUESTO_ESTADO_FACTURADO,
    ]);
    const convertidos = countDe([presupuesto_estado.CONVERTED, PRESUPUESTO_ESTADO_FACTURADO]);

    // Por vencer: SENT/VIEWED cuya vigencia cae dentro de los próximos N días.
    const diasAlerta = config?.dias_alerta_vencimiento ?? 3;
    const hoy = new Date();
    const limite = new Date(hoy.getTime() + diasAlerta * 24 * 60 * 60 * 1000);
    const vencenPronto = await this.prisma.presupuesto_cab.count({
      where: {
        empresa_id,
        deleted: false,
        ...(filtroSucursal !== undefined ? { sucursal_id: filtroSucursal } : {}),
        estado: { in: [presupuesto_estado.SENT, presupuesto_estado.VIEWED] },
        fecha_vigencia: { gte: hoy, lte: limite },
      },
    });

    return {
      resumen: {
        total,
        monto_total: montoTotal,
        monto_convertido: montoDe([presupuesto_estado.CONVERTED, PRESUPUESTO_ESTADO_FACTURADO]),
        monto_pipeline: montoDe(ESTADOS_PIPELINE),
        tasa_conversion: enviados > 0 ? Math.round((convertidos / enviados) * 1000) / 10 : 0, // %
        vencen_pronto: vencenPronto,
      },
      por_estado: grupos
        .map((g) => ({ estado: g.estado, count: g._count._all, monto: Number(g._sum.total ?? 0) }))
        .sort((a, b) => b.count - a.count),
      funnel: [
        { etapa: 'Creados', count: total },
        { etapa: 'Enviados', count: enviados },
        { etapa: 'Vistos', count: vistos },
        { etapa: 'Aceptados', count: aceptados },
        { etapa: 'Convertidos', count: convertidos },
      ],
      por_mes: porMes,
    };
  }

  /**
   * Serie de los últimos 6 meses: emitidos vs convertidos (count + monto).
   * `sucursalIds` null = sin restricción; `[]` = nada visible.
   */
  private async serieMensual(empresa_id: string, sucursalIds: string[] | null) {
    const filtroSucursal =
      sucursalIds === null ? Prisma.empty : Prisma.sql`AND sucursal_id = ANY(${sucursalIds}::uuid[])`;
    const emitidos = await this.prisma.$queryRaw<MesRow[]>`
      SELECT to_char(date_trunc('month', fecha_emision), 'YYYY-MM') AS mes,
             COUNT(*)::int AS count, COALESCE(SUM(total), 0)::float AS monto
      FROM presupuesto_cab
      WHERE empresa_id = ${empresa_id}::uuid AND deleted = false ${filtroSucursal}
        AND fecha_emision >= date_trunc('month', now()) - interval '5 months'
      GROUP BY 1`;
    const convertidos = await this.prisma.$queryRaw<MesRow[]>`
      SELECT to_char(date_trunc('month', fecha_conversion), 'YYYY-MM') AS mes,
             COUNT(*)::int AS count, COALESCE(SUM(total), 0)::float AS monto
      FROM presupuesto_cab
      WHERE empresa_id = ${empresa_id}::uuid AND deleted = false AND fecha_conversion IS NOT NULL ${filtroSucursal}
        AND fecha_conversion >= date_trunc('month', now()) - interval '5 months'
      GROUP BY 1`;

    const emMap = new Map(emitidos.map((r) => [r.mes, r]));
    const coMap = new Map(convertidos.map((r) => [r.mes, r]));

    const now = new Date();
    const out: Array<{ mes: string; emitidos: number; emitidos_monto: number; convertidos: number; convertidos_monto: number }> = [];
    for (let i = 5; i >= 0; i--) {
      const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
      const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
      out.push({
        mes: key,
        emitidos: emMap.get(key)?.count ?? 0,
        emitidos_monto: emMap.get(key)?.monto ?? 0,
        convertidos: coMap.get(key)?.count ?? 0,
        convertidos_monto: coMap.get(key)?.monto ?? 0,
      });
    }
    return out;
  }

  /**
   * Marca como EXPIRED los presupuestos SENT/VIEWED cuya vigencia ya pasó.
   * Usado por el cron diario. Registra el cambio en el historial.
   */
  async vencerExpirados(): Promise<number> {
    const vencidos = await this.prisma.presupuesto_cab.findMany({
      where: {
        deleted: false,
        estado: { in: [presupuesto_estado.SENT, presupuesto_estado.VIEWED] },
        fecha_vigencia: { lt: new Date() },
      },
      select: { id: true, estado: true },
    });
    if (vencidos.length === 0) return 0;

    await this.prisma.$transaction([
      this.prisma.presupuesto_cab.updateMany({
        where: { id: { in: vencidos.map((v) => v.id) } },
        data: { estado: presupuesto_estado.EXPIRED, updated_at: new Date() },
      }),
      this.prisma.presupuesto_historial.createMany({
        data: vencidos.map((v) => ({
          presupuesto_id: v.id,
          estado_anterior: v.estado,
          estado_nuevo: presupuesto_estado.EXPIRED,
          comentario: 'Vencido automáticamente (vigencia expirada)',
        })),
      }),
    ]);
    return vencidos.length;
  }
}
