import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { MonedasService } from 'src/monedas/monedas.service';
import { ViajeCobranzaService } from './viaje-cobranza.service';
import { ViajeCostosService } from './viaje-costos.service';
import { ViajeDocumentosService } from './viaje-documentos.service';
import { ViajesService } from './viajes.service';
import { TIPO_DOC_VIAJE } from './viajes.constants';

/**
 * Moneda base de la empresa (Global Constraints del plan).
 *
 * Va como constante de módulo y NO como campo de clase: los specs construyen el
 * servicio con `Object.create(ViajeCalculosService.prototype)`, que no ejecuta
 * el constructor ni los inicializadores de campo, así que un `private readonly
 * MONEDA_BASE = 'PYG'` llegaría `undefined` y haría convertir hasta los montos
 * que ya están en guaraníes.
 */
const MONEDA_BASE = 'PYG';

type Acumulado = {
  producto_id: string;
  producto: string;
  cod_producto: string | null;
  planificado_unid: number;
  remitido_unid: number;
  facturado_unid: number;
};

@Injectable()
export class ViajeCalculosService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly documentos: ViajeDocumentosService,
    private readonly viajes: ViajesService,
    private readonly monedas: MonedasService,
    private readonly cobranzas: ViajeCobranzaService,
    private readonly costosViaje: ViajeCostosService,
  ) {}

  /** factor_conversion de la presentación por defecto, por producto. */
  private async factoresPorProducto(productoIds: string[]): Promise<Map<string, number>> {
    if (productoIds.length === 0) return new Map();
    const presentaciones = await this.prisma.producto_presentaciones.findMany({
      where: { producto_id: { in: productoIds }, active: true },
      orderBy: [{ es_default: 'desc' }, { orden: 'asc' }],
      select: { producto_id: true, factor_conversion: true },
    });
    const mapa = new Map<string, number>();
    for (const p of presentaciones) {
      if (!mapa.has(p.producto_id)) mapa.set(p.producto_id, Number(p.factor_conversion) || 0);
    }
    return mapa;
  }

  /**
   * Líneas de lo planificado. Manda la orden de venta: si el viaje tiene pedidos,
   * los presupuestos ni se leen (el que derivó en pedido contaría dos veces lo mismo).
   */
  async lineasPlanificadas(viajeId: string) {
    const [pedidoIds, presupuestoIds] = await Promise.all([
      this.documentos.idsVigentes(viajeId, TIPO_DOC_VIAJE.PEDIDO),
      this.documentos.idsVigentes(viajeId, TIPO_DOC_VIAJE.PRESUPUESTO),
    ]);
    if (pedidoIds.length === 0 && presupuestoIds.length === 0) return { origen: null, lineas: [] };

    const lineasPedido = pedidoIds.length
      ? await this.prisma.pedido_detalle.findMany({
          where: { pedido_id: { in: pedidoIds } },
          select: {
            producto_id: true,
            cantidad_base: true,
            // En `pedido_detalle` la relación se llama `producto` (singular).
            subtotal: true,
            producto: { select: { descripcion: true, cod_producto: true } },
            pedido: {
              select: {
                cliente_id: true,
                moneda: { select: { codigo: true } },
                cliente: { select: { personas: { select: { razon_social: true } } } },
              },
            },
          },
        })
      : [];

    const lineasPresupuesto = presupuestoIds.length
      ? await this.prisma.presupuesto_det.findMany({
          where: { presupuesto_id: { in: presupuestoIds }, producto_id: { not: null } },
          select: {
            producto_id: true,
            cantidad: true,
            subtotal_con_iva: true,
            productos: { select: { descripcion: true, cod_producto: true } },
            presupuesto: {
              select: {
                cliente_id: true,
                pedido_id: true,
                moneda: { select: { codigo: true } },
                clientes: { select: { personas: { select: { razon_social: true } } } },
              },
            },
          },
        })
      : [];

    // La dedup es POR PAR, no global: se descarta sólo el presupuesto cuya orden
    // de venta está en este mismo viaje (contarlo sería contar dos veces lo
    // mismo). Un presupuesto sin orden, o cuya orden no está enganchada, sigue
    // aportando: si no, un solo pedido borraba lo planificado de los demás
    // clientes, que es el camino real (se facturan más presupuestos de los que
    // se convierten).
    const pedidosDelViaje = new Set(pedidoIds);
    const presupuestosUtiles = lineasPresupuesto.filter(
      (l) => !(l.presupuesto?.pedido_id && pedidosDelViaje.has(l.presupuesto.pedido_id)),
    );

    const lineas = [
      ...lineasPedido.map((l) => ({
        producto_id: l.producto_id,
        cliente_id: l.pedido?.cliente_id ?? null,
        cliente: l.pedido?.cliente?.personas?.razon_social ?? 'Sin nombre',
        cantidad_base: Number(l.cantidad_base ?? 0),
        producto: l.producto?.descripcion ?? 'Sin nombre',
        cod_producto: l.producto?.cod_producto ?? null,
        importe: Number(l.subtotal ?? 0),
        moneda: l.pedido?.moneda?.codigo ?? MONEDA_BASE,
      })),
      ...presupuestosUtiles.map((l) => ({
        producto_id: l.producto_id as string,
        cliente_id: l.presupuesto?.cliente_id ?? null,
        cliente: l.presupuesto?.clientes?.personas?.razon_social ?? 'Sin nombre',
        // El presupuesto no tiene presentación: la cantidad ya está en unidades.
        cantidad_base: Number(l.cantidad ?? 0),
        producto: l.productos?.descripcion ?? 'Sin nombre',
        cod_producto: l.productos?.cod_producto ?? null,
        importe: Number(l.subtotal_con_iva ?? 0),
        moneda: l.presupuesto?.moneda?.codigo ?? MONEDA_BASE,
      })),
    ];

    // El origen informa de dónde salió la mayor parte, para el chip de la UI.
    const origen = lineasPedido.length
      ? TIPO_DOC_VIAJE.PEDIDO
      : presupuestosUtiles.length
        ? TIPO_DOC_VIAJE.PRESUPUESTO
        : null;

    return { origen, lineas };
  }

  async carga(viajeId: string, empresaId: string) {
    // findOne valida que el viaje sea de esta empresa. Sin esto, cualquiera con
    // el permiso podía leer la carga de un viaje ajeno pasando su uuid.
    await this.viajes.findOne(viajeId, empresaId);

    const [remisionIds, facturaIds] = await Promise.all([
      this.documentos.idsVigentes(viajeId, TIPO_DOC_VIAJE.REMISION),
      this.documentos.idsVigentes(viajeId, TIPO_DOC_VIAJE.FACTURA),
    ]);

    const [lineasRemision, lineasFactura, planificado] = await Promise.all([
      remisionIds.length
        ? this.prisma.nota_remision_det.findMany({
            where: { nota_remision_cab_id: { in: remisionIds } },
            // La cantidad de la remisión es `dcantproser`, igual que en factura_det.
            select: { producto_id: true, dcantproser: true, productos: { select: { descripcion: true, cod_producto: true } } },
          })
        : Promise.resolve([]),
      facturaIds.length
        ? this.prisma.factura_det.findMany({
            where: { factura_cab_id: { in: facturaIds } },
            select: { producto_id: true, dcantproser: true, productos: { select: { descripcion: true, cod_producto: true } } },
          })
        : Promise.resolve([]),
      this.lineasPlanificadas(viajeId),
    ]);

    const acumulados = new Map<string, Acumulado>();
    const sumar = (
      productoId: string | null,
      nombre: string,
      codigo: string | null,
      campo: 'planificado_unid' | 'remitido_unid' | 'facturado_unid',
      cantidad: number,
    ) => {
      if (!productoId) return;
      const actual =
        acumulados.get(productoId) ??
        {
          producto_id: productoId,
          producto: nombre,
          cod_producto: codigo,
          planificado_unid: 0,
          remitido_unid: 0,
          facturado_unid: 0,
        };
      actual[campo] += cantidad;
      acumulados.set(productoId, actual);
    };

    for (const l of planificado.lineas) {
      sumar(l.producto_id, l.producto, l.cod_producto, 'planificado_unid', l.cantidad_base);
    }
    for (const l of lineasRemision) {
      sumar(l.producto_id, l.productos?.descripcion ?? 'Sin nombre', l.productos?.cod_producto ?? null, 'remitido_unid', Number(l.dcantproser ?? 0));
    }
    for (const l of lineasFactura) {
      sumar(l.producto_id, l.productos?.descripcion ?? 'Sin nombre', l.productos?.cod_producto ?? null, 'facturado_unid', Number(l.dcantproser ?? 0));
    }

    const factores = await this.factoresPorProducto([...acumulados.keys()]);
    const aCajas = (unid: number, factor?: number) =>
      factor && factor > 0 ? Number((unid / factor).toFixed(2)) : null;

    const items = [...acumulados.values()]
      .map((a) => {
        const factor = factores.get(a.producto_id);
        // Positiva: volvió al depósito o falta facturar. Negativa: se facturó
        // más de lo que salió (suele ser un documento mal enganchado).
        const diferencia = a.remitido_unid - a.facturado_unid;
        return {
          ...a,
          unidad_por_caja: factor && factor > 0 ? factor : null,
          planificado_cajas: aCajas(a.planificado_unid, factor),
          remitido_cajas: aCajas(a.remitido_unid, factor),
          facturado_cajas: aCajas(a.facturado_unid, factor),
          diferencia_unid: diferencia,
          diferencia_cajas: aCajas(diferencia, factor),
        };
      })
      .sort((x, y) => x.producto.localeCompare(y.producto, 'es'));

    return { items, origen_planificado: planificado.origen };
  }

  async matriz(viajeId: string, empresaId: string, fuente: 'planificado' | 'entregado' | 'comparado' = 'entregado') {
    // Igual que en carga: sin esta validación se filtraban datos entre empresas.
    await this.viajes.findOne(viajeId, empresaId);

    const [facturaIds, pedidoIds, presupuestoIds] = await Promise.all([
      this.documentos.idsVigentes(viajeId, TIPO_DOC_VIAJE.FACTURA),
      this.documentos.idsVigentes(viajeId, TIPO_DOC_VIAJE.PEDIDO),
      this.documentos.idsVigentes(viajeId, TIPO_DOC_VIAJE.PRESUPUESTO),
    ]);

    // Sin órdenes de venta ni presupuestos no hay planificado: se cae a entregado y se avisa.
    let fuenteEfectiva = fuente;
    let aviso: string | undefined;
    if ((fuente === 'planificado' || fuente === 'comparado') && pedidoIds.length === 0 && presupuestoIds.length === 0) {
      fuenteEfectiva = 'entregado';
      aviso = 'El viaje no tiene órdenes de venta ni presupuestos enganchados: se muestra lo entregado.';
    } else if (fuente !== 'entregado' && pedidoIds.length === 0 && presupuestoIds.length > 0) {
      aviso = 'Lo planificado sale de presupuestos: sin cajas ni entregas parciales.';
    }

    const lineasFactura =
      facturaIds.length && fuenteEfectiva !== 'planificado'
        ? await this.prisma.factura_det.findMany({
            where: { factura_cab_id: { in: facturaIds } },
            select: {
              producto_id: true,
              dcantproser: true,
              dtotopeitem: true,
              productos: { select: { descripcion: true, cod_producto: true } },
              factura_cab: {
                select: {
                  cliente_id: true,
                  dticam: true,
                  moneda: { select: { codigo: true } },
                  clientes: { select: { personas: { select: { razon_social: true } } } },
                },
              },
            },
          })
        : [];

    // Lo planificado sale del helper compartido: pedidos si hay, y si no, presupuestos.
    const planificado =
      fuenteEfectiva !== 'entregado' ? await this.lineasPlanificadas(viajeId) : { origen: null, lineas: [] };
    const lineasPedido = planificado.lineas;

    const clientes = new Map<string, { id: string; nombre: string; moneda: string }>();
    const productos = new Map<string, { producto_id: string; producto: string }>();
    const celdas: Record<string, any> = {};
    const totalesCliente: Record<
      string,
      { por_moneda: Record<string, number>; importe_base: number; moneda: string }
    > = {};
    const totalesProducto: Record<string, { unid: number; cajas: number | null }> = {};

    const celda = (productoId: string, clienteId: string) => {
      const clave = `${productoId}::${clienteId}`;
      celdas[clave] = celdas[clave] ?? {
        unid: 0,
        cajas: null,
        // `por_moneda` guarda cada moneda por separado; `importe_base` es el
        // equivalente convertido. Sumar importes de monedas distintas en un solo
        // número daba "US$ 700.100" cuando eran 700.000 Gs + US$ 100.
        por_moneda: {} as Record<string, number>,
        importe_base: 0,
        unid_plan: 0,
        cajas_plan: null,
      };
      return celdas[clave];
    };

    const marca = { estimada: false };
    const cacheTasas = new Map<string, number>();

    for (const l of lineasFactura) {
      const clienteId = l.factura_cab?.cliente_id;
      if (!l.producto_id || !clienteId) continue;
      const moneda = l.factura_cab?.moneda?.codigo ?? MONEDA_BASE;
      const importe = Number(l.dtotopeitem ?? 0);
      const enBase = await this.aBase(importe, moneda, l.factura_cab?.dticam, empresaId, marca, cacheTasas);

      clientes.set(clienteId, {
        id: clienteId,
        nombre: l.factura_cab?.clientes?.personas?.razon_social ?? 'Sin nombre',
        // La moneda del cliente es informativa: si factura en varias, manda la base.
        moneda: clientes.has(clienteId) && clientes.get(clienteId)!.moneda !== moneda
          ? MONEDA_BASE
          : moneda,
      });
      productos.set(l.producto_id, { producto_id: l.producto_id, producto: l.productos?.descripcion ?? 'Sin nombre' });

      const c = celda(l.producto_id, clienteId);
      c.unid += Number(l.dcantproser ?? 0);
      c.por_moneda[moneda] = (c.por_moneda[moneda] ?? 0) + importe;
      c.importe_base += enBase;

      const t = (totalesCliente[clienteId] = totalesCliente[clienteId] ?? {
        por_moneda: {} as Record<string, number>,
        importe_base: 0,
        moneda,
      });
      t.por_moneda[moneda] = (t.por_moneda[moneda] ?? 0) + importe;
      t.importe_base += enBase;
      if (Object.keys(t.por_moneda).length > 1) t.moneda = MONEDA_BASE;
    }

    for (const l of lineasPedido) {
      const clienteId = l.cliente_id;
      if (!l.producto_id || !clienteId) continue;
      if (!clientes.has(clienteId)) {
        clientes.set(clienteId, { id: clienteId, nombre: l.cliente, moneda: MONEDA_BASE });
      }
      productos.set(l.producto_id, { producto_id: l.producto_id, producto: l.producto });
      const c = celda(l.producto_id, clienteId);
      c.unid_plan += Number(l.cantidad_base ?? 0);
      if (fuenteEfectiva === 'planificado') {
        c.unid = c.unid_plan;
        // En modo planificado el importe sale del pedido/presupuesto: si no, la
        // matriz mostraba cantidades sin ningún importe y los totales en "—".
        const moneda = (l as any).moneda ?? MONEDA_BASE;
        const importe = Number((l as any).importe ?? 0);
        if (importe) {
          c.por_moneda[moneda] = (c.por_moneda[moneda] ?? 0) + importe;
          const enBase = await this.aBase(importe, moneda, null, empresaId, marca, cacheTasas);
          c.importe_base += enBase;
          const t = (totalesCliente[clienteId] = totalesCliente[clienteId] ?? {
            por_moneda: {} as Record<string, number>,
            importe_base: 0,
            moneda,
          });
          t.por_moneda[moneda] = (t.por_moneda[moneda] ?? 0) + importe;
          t.importe_base += enBase;
        }
      }
    }

    const factores = await this.factoresPorProducto([...productos.keys()]);
    const aCajas = (unid: number, factor?: number) =>
      factor && factor > 0 ? Number((unid / factor).toFixed(2)) : null;

    for (const [clave, c] of Object.entries(celdas)) {
      const productoId = clave.split('::')[0];
      const factor = factores.get(productoId);
      c.cajas = aCajas(c.unid, factor);
      c.cajas_plan = aCajas(c.unid_plan, factor);
      const tp = (totalesProducto[productoId] = totalesProducto[productoId] ?? { unid: 0, cajas: null });
      tp.unid += c.unid;
      tp.cajas = aCajas(tp.unid, factor);
    }

    return {
      fuente,
      fuente_efectiva: fuenteEfectiva,
      aviso,
      clientes: [...clientes.values()].sort((a, b) => a.nombre.localeCompare(b.nombre, 'es')),
      productos: [...productos.values()]
        .map((p) => ({ ...p, unidad_por_caja: factores.get(p.producto_id) ?? null }))
        .sort((a, b) => a.producto.localeCompare(b.producto, 'es')),
      celdas,
      totales_cliente: totalesCliente,
      totales_producto: totalesProducto,
      moneda_base: MONEDA_BASE,
      cotizacion_estimada: marca.estimada,
    };
  }

  /** Convierte a la moneda base con la cotización del documento; si falta, la vigente. */
  private async aBase(
    monto: number,
    codigo: string,
    tasaDoc: unknown,
    empresaId: string,
    marca: { estimada: boolean },
    cache: Map<string, number>,
  ) {
    if (!monto || codigo === MONEDA_BASE) return monto;
    const tasa = Number(tasaDoc);
    if (tasa > 0) return monto * tasa;
    marca.estimada = true;
    if (!cache.has(codigo)) {
      const v = await this.monedas.getCotizacionVigente(empresaId, codigo).catch(() => null);
      cache.set(codigo, Number(v?.tasa) > 0 ? Number(v?.tasa) : 0);
    }
    return monto * (cache.get(codigo) as number);
  }

  /**
   * Resultado del viaje. Vendido, cobrado y pendiente salen de la cobranza: el
   * cobrado es solo lo de ESTE viaje (la deuda anterior va aparte) y el pendiente
   * es el saldo vivo de las facturas, no vendido − cobrado.
   */
  async resumen(viajeId: string, empresaId: string, usuarioId?: string) {
    const viaje = await this.viajes.findOne(viajeId, empresaId);
    const [cob, costos] = await Promise.all([
      this.cobranzas.cobranza(viajeId, empresaId, usuarioId),
      this.costosViaje.costos(viajeId, empresaId),
    ]);

    const t = cob.totales;
    const costoBase = costos.costo_base;
    const m = costos.margen;
    const clientesVisitados = cob.clientes.filter((c: any) => c.id !== 'sin-cliente').length;
    const kmRecorrido =
      viaje.km_salida != null && viaje.km_retorno != null ? viaje.km_retorno - viaje.km_salida : null;

    return {
      moneda_base: MONEDA_BASE,
      facturado_base: t.facturado_base,
      notas_credito_base: t.notas_credito_base,
      vendido: cob.vendido,
      vendido_base: t.vendido_base,
      cobrado: cob.cobrado,
      cobrado_base: t.cobrado_base,
      deuda_anterior_cobrada_base: t.deuda_anterior_cobrada_base,
      // Cobrado de facturas del viaje con recibos que no están enganchados.
      cobrado_sin_enganchar_base: t.cancelado_sin_enganchar_base,
      pendiente_base: t.por_cobrar_base,
      pendiente_vencido_base: t.por_cobrar_vencido_base,
      sin_rendir_base: cob.control.sin_rendir_base,
      cobro_ruta_sin_registrar_base: cob.control.cobro_ruta_sin_registrar_base,
      recibos_sin_enganchar: cob.recibos_sin_enganchar.length,
      costo_base: costoBase,
      rendiciones: costos.rendiciones_activas,
      // Margen bruto solo con las líneas que tienen costo cargado (ver cobertura).
      margen_bruto_base: m.margen_bruto_base,
      margen_pct: m.margen_pct,
      cobertura_costo: m.cobertura,
      // Resultado = margen bruto − viáticos. Con cobertura < 100 % queda corto: la
      // UI lo rotula como "con el costo conocido".
      resultado_base: m.margen_bruto_base != null ? m.margen_bruto_base - costoBase : null,
      viaticos_sobre_venta: t.vendido_base > 0 ? costoBase / t.vendido_base : null,
      venta_por_cliente: clientesVisitados > 0 ? t.vendido_base / clientesVisitados : null,
      km_recorrido: kmRecorrido,
      costo_por_km: kmRecorrido && kmRecorrido > 0 ? costoBase / kmRecorrido : null,
      clientes_visitados: clientesVisitados,
      sin_rendicion: costos.rendiciones_activas === 0,
      cotizacion_estimada: cob.cotizacion_estimada || costos.cotizacion_estimada,
    };
  }
}
