import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import {
  alcanceSucursalUsuario,
  filtroFiscalPorSucursal,
  filtroSucursalOperativo,
} from 'src/utils/alcance-sucursal';
import { endOfDayAsuncion, ymdInAsuncion } from 'src/utils/utilidades';
import { ViajesService } from './viajes.service';
import { ESTADO_VIAJE, TIPO_DOC_VIAJE, TipoDocViaje } from './viajes.constants';
import { ESTADOS_ANULADOS, estaDeBaja, estadosBaja, pedidoNoCancelado } from './viajes-estados';

// Las reglas de baja viven en viajes-estados.ts: una sola fuente para la propuesta,
// el enganche, los cálculos y la pestaña Documentos.
const TIPO_RENDICION = 'RENDICION_VIATICO';

@Injectable()
export class ViajeDocumentosService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly viajes: ViajesService,
  ) {}

  /** Ids de los documentos de un tipo enganchados al viaje. Lo usan carga, matriz y resumen. */
  async idsPorTipo(viajeId: string, tipo: TipoDocViaje): Promise<string[]> {
    const filas = await this.prisma.viaje_documentos.findMany({
      where: { viaje_id: viajeId, tipo },
      select: { documento_id: true },
    });
    return filas.map((f) => f.documento_id);
  }

  /** Estado de cada documento (y `estado_orden` en las OV), por tipo. */
  private async estadosPorTipo(tipo: string, ids: string[]): Promise<Map<string, any>> {
    const mapa = new Map<string, any>();
    if (ids.length === 0) return mapa;
    const where = { id: { in: ids } };
    let filas: any[] = [];
    switch (tipo) {
      case TIPO_DOC_VIAJE.FACTURA: filas = await this.prisma.factura_cab.findMany({ where, select: { id: true, estado: true } }); break;
      case TIPO_DOC_VIAJE.NOTA_CREDITO: filas = await this.prisma.nota_credito_cab.findMany({ where, select: { id: true, estado: true } }); break;
      case TIPO_DOC_VIAJE.REMISION: filas = await this.prisma.nota_remision_cab.findMany({ where, select: { id: true, estado: true } }); break;
      case TIPO_DOC_VIAJE.RECIBO: filas = await this.prisma.recibos_cobro.findMany({ where, select: { id: true, estado: true } }); break;
      case TIPO_DOC_VIAJE.PEDIDO: filas = await this.prisma.pedidos.findMany({ where, select: { id: true, estado: true, estado_orden: true } }); break;
      case TIPO_DOC_VIAJE.PRESUPUESTO: filas = await this.prisma.presupuesto_cab.findMany({ where, select: { id: true, estado: true } }); break;
    }
    for (const f of filas) mapa.set(f.id, f);
    return mapa;
  }

  /**
   * Ids de los documentos del viaje que siguen VIGENTES: lo que usan los cálculos.
   * Un documento anulado desde su pantalla (factura, NC, recibo, remisión) o una OV
   * cancelada sigue enganchado —se ve marcado en Documentos—, pero no suma en nada.
   */
  async idsVigentes(viajeId: string, tipo: TipoDocViaje): Promise<string[]> {
    const ids = await this.idsPorTipo(viajeId, tipo);
    if (ids.length === 0) return [];
    const estados = await this.estadosPorTipo(tipo, ids);
    return ids.filter((id) => estados.has(id) && !estaDeBaja(tipo, estados.get(id)));
  }

  /**
   * Documentos ya asignados a otro viaje DE ESTA EMPRESA, para excluirlos de la
   * propuesta. Acotar por empresa importa: sin el filtro se traían los uuids de
   * todas las empresas del ERP para meterlos en un `notIn`, que con el tiempo
   * degrada la consulta hasta volverla inusable.
   */
  private async idsAsignados(tipo: TipoDocViaje, empresaId: string): Promise<string[]> {
    const filas = await this.prisma.viaje_documentos.findMany({
      where: { tipo, viaje: { empresa_id: empresaId } },
      select: { documento_id: true },
    });
    return filas.map((f) => f.documento_id);
  }

  async sugeridos(viajeId: string, empresaId: string, usuarioId?: string) {
    const viaje = await this.viajes.findOne(viajeId, empresaId);
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresaId);
    const filtroFiscal = await filtroFiscalPorSucursal(this.prisma, alcance);
    // `pedidos` y `presupuesto_cab` tienen sucursal_id propio: se filtran por ahí.
    const filtroSucursal = filtroSucursalOperativo(alcance);

    const desde = viaje.fecha_salida;
    // `fecha_retorno` es @db.Date: vuelve como medianoche. Compararlo con un
    // timestamp dejaba afuera TODO el último día de reparto, que suele ser el de
    // mayor facturación. Se extiende al fin del día en hora de Asunción.
    const hasta = viaje.fecha_retorno
      ? endOfDayAsuncion(new Date(viaje.fecha_retorno).toISOString().slice(0, 10))
      : endOfDayAsuncion(ymdInAsuncion());
    const [asignadasFact, asignadasRem, asignadosRec, asignadosPed, asignadosPre, asignadasNc] =
      await Promise.all([
      this.idsAsignados(TIPO_DOC_VIAJE.FACTURA, empresaId),
      this.idsAsignados(TIPO_DOC_VIAJE.REMISION, empresaId),
      this.idsAsignados(TIPO_DOC_VIAJE.RECIBO, empresaId),
      this.idsAsignados(TIPO_DOC_VIAJE.PEDIDO, empresaId),
      this.idsAsignados(TIPO_DOC_VIAJE.PRESUPUESTO, empresaId),
      this.idsAsignados(TIPO_DOC_VIAJE.NOTA_CREDITO, empresaId),
    ]);

    const facturas = await this.prisma.factura_cab.findMany({
      where: {
        empresa_id: empresaId,
        estado: { notIn: ESTADOS_ANULADOS },
        dfeemide: { gte: desde, lte: hasta },
        ...(viaje.vendedor_id && { vendedor_id: viaje.vendedor_id }),
        ...(filtroFiscal && { dest: filtroFiscal.dest }),
        id: { notIn: asignadasFact },
      },
      select: {
        id: true, dest: true, dpunexp: true, dnumdoc: true, dfeemide: true,
        moneda: { select: { codigo: true } },
        clientes: { select: { personas: { select: { razon_social: true } } } },
        factura_subtotales: { select: { dtotgralope: true } },
        presupuesto_id: true,
        pedido_id: true,
        _count: {
          select: {
            nota_remision_factura: { where: { nota_remision_cab: { estado: { notIn: ESTADOS_ANULADOS } } } },
            nota_credito_cab: { where: { estado: { notIn: ESTADOS_ANULADOS } } },
          },
        },
      },
      orderBy: { dfeemide: 'asc' },
    });

    // Las NC restan del vendido: sin esto no había forma de engancharlas.
    const notasCredito = await this.prisma.nota_credito_cab.findMany({
      where: {
        empresa_id: empresaId,
        estado: { notIn: ESTADOS_ANULADOS },
        dfeemide: { gte: desde, lte: hasta },
        ...(filtroFiscal && { dest: filtroFiscal.dest }),
        id: { notIn: asignadasNc },
      },
      select: {
        id: true, dest: true, dpunexp: true, dnumdoc: true, dfeemide: true,
        moneda: { select: { codigo: true } },
        clientes: { select: { personas: { select: { razon_social: true } } } },
        nota_credito_subtotal: { select: { dtotgralope: true } },
      },
      orderBy: { dfeemide: 'asc' },
    });

    const remisiones = await this.prisma.nota_remision_cab.findMany({
      where: {
        empresa_id: empresaId,
        estado: { notIn: ESTADOS_ANULADOS },
        dfeemide: { gte: desde, lte: hasta },
        ...(filtroFiscal && { dest: filtroFiscal.dest }),
        id: { notIn: asignadasRem },
      },
      select: {
        id: true, dest: true, dpunexp: true, dnumdoc: true, dfeemide: true,
        clientes: { select: { personas: { select: { razon_social: true } } } },
      },
      orderBy: { dfeemide: 'asc' },
    });

    const recibos = await this.prisma.recibos_cobro.findMany({
      where: {
        empresa_id: empresaId,
        estado: { notIn: ESTADOS_ANULADOS },
        fecha_emision: { gte: desde, lte: hasta },
        ...(alcance && { sucursal_id: { in: alcance.sucursalIds } }),
        id: { notIn: asignadosRec },
      },
      select: {
        id: true, numero_recibo: true, fecha_emision: true, monto_total: true,
        moneda: { select: { codigo: true } },
        clientes: { select: { personas: { select: { razon_social: true } } } },
      },
      orderBy: { fecha_emision: 'asc' },
    });

    // Planificación: órdenes de venta y presupuestos aceptados del mismo período.
    const pedidos = await this.prisma.pedidos.findMany({
      where: {
        empresa_id: empresaId,
        estado: { notIn: estadosBaja(TIPO_DOC_VIAJE.PEDIDO) },
        ...pedidoNoCancelado,
        fecha_pedido: { gte: desde, lte: hasta },
        ...(viaje.vendedor_id && { vendedor_id: viaje.vendedor_id }),
        ...(filtroSucursal !== undefined && { sucursal_id: filtroSucursal }),
        id: { notIn: asignadosPed },
      },
      select: {
        id: true, numero_pedido: true, fecha_pedido: true, total: true,
        moneda: { select: { codigo: true } },
        // En `pedidos` la relación se llama `cliente` (singular), a diferencia
        // de factura_cab/presupuesto_cab, que usan `clientes`.
        cliente: { select: { personas: { select: { razon_social: true } } } },
      },
      orderBy: { fecha_pedido: 'asc' },
    });

    const presupuestos = await this.prisma.presupuesto_cab.findMany({
      where: {
        empresa_id: empresaId,
        deleted: false,
        // Sólo los que representan un compromiso: aceptado, convertido o facturado.
        estado: { in: ['ACCEPTED', 'CONVERTED', 'FACTURADO'] as any },
        fecha_emision: { gte: desde, lte: hasta },
        ...(viaje.vendedor_id && { vendedor_id: viaje.vendedor_id }),
        ...(filtroSucursal !== undefined && { sucursal_id: filtroSucursal }),
        id: { notIn: asignadosPre },
      },
      select: {
        id: true, numero: true, fecha_emision: true, total: true, pedido_id: true,
        moneda: { select: { codigo: true } },
        clientes: { select: { personas: { select: { razon_social: true } } } },
      },
      orderBy: { fecha_emision: 'asc' },
    });

    const nombre = (d: any) => d?.clientes?.personas?.razon_social ?? 'Sin nombre';

    return {
      facturas: facturas.map((f) => ({
        id: f.id,
        tipo: TIPO_DOC_VIAJE.FACTURA,
        numero: `${f.dest}-${f.dpunexp}-${f.dnumdoc}`,
        fecha: f.dfeemide,
        cliente: nombre(f),
        total: Number(f.factura_subtotales?.[0]?.dtotgralope ?? 0),
        moneda: f.moneda?.codigo ?? 'PYG',
        // Documentos que se arrastran solos al enganchar esta factura.
        vinculados:
          (f.presupuesto_id ? 1 : 0) + (f.pedido_id ? 1 : 0)
          + Number(f._count?.nota_remision_factura ?? 0) + Number(f._count?.nota_credito_cab ?? 0),
      })),
      notas_credito: notasCredito.map((n) => ({
        id: n.id,
        tipo: TIPO_DOC_VIAJE.NOTA_CREDITO,
        numero: `${n.dest}-${n.dpunexp}-${n.dnumdoc}`,
        fecha: n.dfeemide,
        cliente: nombre(n),
        total: Number(n.nota_credito_subtotal?.[0]?.dtotgralope ?? 0),
        moneda: n.moneda?.codigo ?? 'PYG',
      })),
      remisiones: remisiones.map((r) => ({
        id: r.id,
        tipo: TIPO_DOC_VIAJE.REMISION,
        numero: `${r.dest}-${r.dpunexp}-${r.dnumdoc}`,
        fecha: r.dfeemide,
        cliente: nombre(r),
        total: null,
        moneda: null,
      })),
      recibos: recibos.map((r) => ({
        id: r.id,
        tipo: TIPO_DOC_VIAJE.RECIBO,
        numero: r.numero_recibo,
        fecha: r.fecha_emision,
        cliente: nombre(r),
        total: Number(r.monto_total ?? 0),
        moneda: r.moneda?.codigo ?? 'PYG',
      })),
      pedidos: pedidos.map((p) => ({
        id: p.id,
        tipo: TIPO_DOC_VIAJE.PEDIDO,
        numero: p.numero_pedido,
        fecha: p.fecha_pedido,
        cliente: p.cliente?.personas?.razon_social ?? 'Sin nombre',
        total: Number(p.total ?? 0),
        moneda: p.moneda?.codigo ?? 'PYG',
      })),
      // El presupuesto que ya derivó en una orden de venta se marca: engancharlo
      // además de su pedido contaría dos veces lo planificado.
      presupuestos: presupuestos.map((p) => ({
        id: p.id,
        tipo: TIPO_DOC_VIAJE.PRESUPUESTO,
        numero: p.numero,
        fecha: p.fecha_emision,
        cliente: nombre(p),
        total: Number(p.total ?? 0),
        moneda: p.moneda?.codigo ?? 'PYG',
        ya_tiene_orden: !!p.pedido_id,
      })),
    };
  }

  /**
   * Busca el documento y valida empresa Y alcance por sucursal. Sin el alcance,
   * un usuario podía saltearse el filtro de la propuesta pasando el uuid directo
   * de un documento de una sucursal que no le corresponde.
   */
  private async buscarDocumento(
    tipo: string,
    id: string,
    empresaId: string,
    filtroFiscal?: { dest: { in: string[] } } | null,
    filtroSucursal?: string | { in: string[] },
  ) {
    const fiscal = filtroFiscal ? { dest: filtroFiscal.dest } : {};
    const operativo = filtroSucursal !== undefined ? { sucursal_id: filtroSucursal } : {};
    const where = { id, empresa_id: empresaId };
    switch (tipo) {
      case TIPO_DOC_VIAJE.FACTURA:
        return this.prisma.factura_cab.findFirst({ where: { ...where, ...fiscal }, select: { id: true, estado: true } });
      case TIPO_DOC_VIAJE.NOTA_CREDITO:
        return this.prisma.nota_credito_cab.findFirst({ where: { ...where, ...fiscal }, select: { id: true, estado: true } });
      case TIPO_DOC_VIAJE.REMISION:
        return this.prisma.nota_remision_cab.findFirst({ where: { ...where, ...fiscal }, select: { id: true, estado: true } });
      case TIPO_DOC_VIAJE.RECIBO:
        return this.prisma.recibos_cobro.findFirst({ where: { ...where, ...operativo }, select: { id: true, estado: true } });
      case TIPO_DOC_VIAJE.PEDIDO:
        return this.prisma.pedidos.findFirst({ where: { ...where, ...operativo }, select: { id: true, estado: true, estado_orden: true } });
      case TIPO_DOC_VIAJE.PRESUPUESTO:
        return this.prisma.presupuesto_cab.findFirst({ where: { ...where, ...operativo }, select: { id: true, estado: true } });
      default:
        throw new BadRequestException(`Tipo de documento desconocido: ${tipo}`);
    }
  }

  /**
   * Documentos vinculados a cada factura: su presupuesto, su orden de venta, sus
   * remisiones y sus notas de crédito. La factura ya los conoce, así que se
   * enganchan solos con ella en vez de pedir que se busquen uno por uno.
   */
  private async vinculadosDeFacturas(facturaIds: string[]) {
    if (facturaIds.length === 0) return [];
    const facturas = await this.prisma.factura_cab.findMany({
      where: { id: { in: facturaIds } },
      select: {
        id: true, presupuesto_id: true, pedido_id: true,
        nota_remision_factura: { select: { nota_remision_cab_id: true } },
        nota_credito_cab: { select: { id: true } },
      },
    });
    const out: { tipo: string; documento_id: string; factura_id: string }[] = [];
    for (const f of facturas as any[]) {
      if (f.presupuesto_id) out.push({ tipo: TIPO_DOC_VIAJE.PRESUPUESTO, documento_id: f.presupuesto_id, factura_id: f.id });
      if (f.pedido_id) out.push({ tipo: TIPO_DOC_VIAJE.PEDIDO, documento_id: f.pedido_id, factura_id: f.id });
      for (const r of f.nota_remision_factura ?? []) out.push({ tipo: TIPO_DOC_VIAJE.REMISION, documento_id: r.nota_remision_cab_id, factura_id: f.id });
      for (const n of f.nota_credito_cab ?? []) out.push({ tipo: TIPO_DOC_VIAJE.NOTA_CREDITO, documento_id: n.id, factura_id: f.id });
    }
    return out;
  }

  async agregar(
    viajeId: string,
    items: { tipo: string; documento_id: string; agregado_auto?: boolean }[],
    empresaId: string,
    userId: string,
  ) {
    await this.viajes.exigirEditable(viajeId, empresaId);
    const alcance = await alcanceSucursalUsuario(this.prisma, userId, empresaId);
    const filtroFiscal = await filtroFiscalPorSucursal(this.prisma, alcance);
    const filtroSucursal = filtroSucursalOperativo(alcance);

    for (const item of items) {
      const doc = await this.buscarDocumento(item.tipo, item.documento_id, empresaId, filtroFiscal, filtroSucursal);
      if (!doc) {
        throw new NotFoundException(
          `Documento ${item.tipo} no encontrado en esta empresa, o es de una sucursal fuera de tu alcance`,
        );
      }
      if (estaDeBaja(item.tipo, doc)) {
        throw new BadRequestException(
          `El documento ${item.tipo} está dado de baja (${(doc as any).estado_orden === 'cancelada' ? 'cancelada' : doc.estado}): no se puede enganchar al viaje`,
        );
      }
      const yaAsignado = await this.prisma.viaje_documentos.findFirst({
        where: { tipo: item.tipo, documento_id: item.documento_id },
        select: { id: true, viaje_id: true },
      });
      if (yaAsignado) {
        throw new BadRequestException(
          yaAsignado.viaje_id === viajeId
            ? `El documento ${item.tipo} ya está en este viaje`
            : `El documento ${item.tipo} ya está en otro viaje`,
        );
      }
    }

    // Arrastre: lo vinculado a las facturas entra solo. Lo que está dado de baja se
    // saltea; lo que ya está en otro viaje no se mueve y se informa.
    const enLote = new Set(items.map((i) => `${i.tipo}::${i.documento_id}`));
    const arrastrados: { tipo: string; documento_id: string; factura_id: string }[] = [];
    const omitidos: { tipo: string; documento_id: string; factura_id: string; motivo: string }[] = [];
    const facturaIds = items.filter((i) => i.tipo === TIPO_DOC_VIAJE.FACTURA).map((i) => i.documento_id);
    for (const c of await this.vinculadosDeFacturas(facturaIds)) {
      const clave = `${c.tipo}::${c.documento_id}`;
      if (enLote.has(clave)) continue;
      const doc = await this.buscarDocumento(c.tipo, c.documento_id, empresaId, filtroFiscal, filtroSucursal);
      if (!doc) { omitidos.push({ ...c, motivo: 'fuera de tu alcance por sucursal' }); continue; }
      if (estaDeBaja(c.tipo, doc)) continue;
      const ya = await this.prisma.viaje_documentos.findFirst({
        where: { tipo: c.tipo, documento_id: c.documento_id },
        select: { id: true, viaje_id: true },
      });
      if (ya) {
        if (ya.viaje_id !== viajeId) omitidos.push({ ...c, motivo: 'ya está en otro viaje' });
        continue;
      }
      enLote.add(clave);
      arrastrados.push(c);
    }

    await this.prisma.viaje_documentos.createMany({
      data: [
        ...items.map((item) => ({
          viaje_id: viajeId,
          tipo: item.tipo,
          documento_id: item.documento_id,
          agregado_auto: item.agregado_auto ?? false,
          created_by: userId,
        })),
        ...arrastrados.map((c) => ({
          viaje_id: viajeId, tipo: c.tipo, documento_id: c.documento_id, agregado_auto: true, created_by: userId,
        })),
      ],
    });

    const viaje = await this.viajes.findOne(viajeId, empresaId);
    return {
      ...viaje,
      arrastrados: arrastrados.length,
      arrastrados_por_tipo: arrastrados.reduce((acc: Record<string, number>, c) => ({ ...acc, [c.tipo]: (acc[c.tipo] ?? 0) + 1 }), {}),
      omitidos,
    };
  }

  /**
   * Quita de un clic los documentos que se anularon (o cancelaron) desde su propia
   * pantalla después de engancharlos. No se quitan solos: si una factura se anula y
   * se rehace, el aviso recuerda enganchar la nueva.
   */
  async quitarAnulados(viajeId: string, empresaId: string) {
    const viaje = await this.viajes.findOne(viajeId, empresaId);
    if (viaje.estado === ESTADO_VIAJE.CERRADO) {
      throw new BadRequestException('El viaje está cerrado: reabrilo para modificarlo');
    }
    const filas = await this.prisma.viaje_documentos.findMany({
      where: { viaje_id: viajeId },
      select: { id: true, tipo: true, documento_id: true },
    });
    const anulados = (await this.detallar(filas)).filter((d: any) => d.anulado).map((d: any) => d.id);
    if (anulados.length) {
      await this.prisma.viaje_documentos.deleteMany({ where: { id: { in: anulados }, viaje_id: viajeId } });
    }
    return { id: viajeId, quitados: anulados.length };
  }

  /**
   * Anula el viaje y LIBERA sus documentos y rendiciones para que puedan ir a otro
   * viaje. Antes quedaban enganchados al viaje anulado y había que quitarlos uno por
   * uno. Para no perder el rastro, el viaje guarda la lista de lo que tenía.
   */
  async anularViaje(viajeId: string, motivo: string, empresaId: string, userId: string) {
    const viaje: any = await this.viajes.findOne(viajeId, empresaId);
    const documentos = (await this.detallar(viaje.documentos ?? [])).map((d: any) => ({
      tipo: d.tipo, numero: d.numero, cliente: d.cliente, fecha: d.fecha, total: d.total, moneda: d.moneda, estado: d.estado ?? null,
    }));
    const rendiciones = (
      await this.prisma.viaje_rendiciones.findMany({
        where: { viaje_id: viajeId },
        select: {
          rendicion: {
            select: { numero_rendicion: true, monto_rendido_total: true, empleado: { select: { nombres: true, apellidos: true } } },
          },
        },
      })
    ).map((r: any) => ({
      tipo: TIPO_RENDICION,
      numero: r.rendicion?.numero_rendicion ?? null,
      empleado: [r.rendicion?.empleado?.nombres, r.rendicion?.empleado?.apellidos].filter(Boolean).join(' ') || null,
      total: Number(r.rendicion?.monto_rendido_total ?? 0),
    }));
    return this.viajes.anular(viajeId, motivo, empresaId, userId, { documentos, rendiciones, liberados_el: new Date().toISOString() });
  }

  /**
   * Desenganchar NO exige que el viaje sea editable.
   *
   * Al anular, el viaje ya libera todo (anularViaje). Esto queda para los viajes
   * anulados antes de ese cambio, que conservan documentos. Cerrado sí bloquea:
   * para eso está reabrir.
   */
  async quitar(viajeId: string, filaId: string, empresaId: string) {
    const viaje = await this.viajes.findOne(viajeId, empresaId);
    if (viaje.estado === ESTADO_VIAJE.CERRADO) {
      throw new BadRequestException('El viaje está cerrado: reabrilo para modificarlo');
    }
    const fila = await this.prisma.viaje_documentos.findFirst({ where: { id: filaId, viaje_id: viajeId } });
    if (!fila) throw new NotFoundException('El documento no pertenece a este viaje');
    await this.prisma.viaje_documentos.delete({ where: { id: filaId } });
    return this.viajes.findOne(viajeId, empresaId);
  }

  /**
   * Resuelve número, fecha, cliente y total de cada documento enganchado.
   *
   * `viaje_documentos` guarda sólo `tipo` + `documento_id` (no hay FK porque el
   * tipo define la tabla), así que sin esto la pantalla mostraría un uuid. Se
   * agrupa por tipo: una consulta por tabla, no una por documento.
   *
   * Un documento que ya no existe no se pierde: la fila se devuelve con
   * `numero: null`, para que se vea que está enganchado y se pueda quitar.
   */
  async detallar(documentos: { id: string; tipo: string; documento_id: string }[]) {
    if (!documentos || documentos.length === 0) return [];

    const porTipo = new Map<string, string[]>();
    for (const d of documentos) {
      const ids = porTipo.get(d.tipo) ?? [];
      ids.push(d.documento_id);
      porTipo.set(d.tipo, ids);
    }

    const nombre = (d: any) => d?.clientes?.personas?.razon_social ?? d?.cliente?.personas?.razon_social ?? null;
    const resueltos = new Map<string, any>();
    const guardar = (tipo: string, id: string, datos: any) => resueltos.set(`${tipo}::${id}`, datos);

    const ids = (tipo: string) => porTipo.get(tipo) ?? [];
    const sifen = (d: any) => `${d.dest ?? '001'}-${d.dpunexp ?? '001'}-${d.dnumdoc ?? '0000000'}`;

    await Promise.all([
      ids(TIPO_DOC_VIAJE.FACTURA).length
        ? this.prisma.factura_cab
            .findMany({
              where: { id: { in: ids(TIPO_DOC_VIAJE.FACTURA) } },
              select: {
                id: true, estado: true, dest: true, dpunexp: true, dnumdoc: true, dfeemide: true,
                moneda: { select: { codigo: true } },
                clientes: { select: { personas: { select: { razon_social: true } } } },
                factura_subtotales: { select: { dtotgralope: true } },
              },
            })
            .then((rows) =>
              rows.forEach((d) =>
                guardar(TIPO_DOC_VIAJE.FACTURA, d.id, {
                  estado: d.estado ?? null,
                  anulado: estaDeBaja(TIPO_DOC_VIAJE.FACTURA, d),
                  numero: sifen(d),
                  fecha: d.dfeemide,
                  cliente: nombre(d),
                  total: Number(d.factura_subtotales?.[0]?.dtotgralope ?? 0),
                  moneda: d.moneda?.codigo ?? null,
                }),
              ),
            )
        : null,
      ids(TIPO_DOC_VIAJE.NOTA_CREDITO).length
        ? this.prisma.nota_credito_cab
            .findMany({
              where: { id: { in: ids(TIPO_DOC_VIAJE.NOTA_CREDITO) } },
              select: {
                id: true, estado: true, dest: true, dpunexp: true, dnumdoc: true, dfeemide: true,
                moneda: { select: { codigo: true } },
                clientes: { select: { personas: { select: { razon_social: true } } } },
                nota_credito_subtotal: { select: { dtotgralope: true } },
              },
            })
            .then((rows) =>
              rows.forEach((d) =>
                guardar(TIPO_DOC_VIAJE.NOTA_CREDITO, d.id, {
                  estado: d.estado ?? null,
                  anulado: estaDeBaja(TIPO_DOC_VIAJE.NOTA_CREDITO, d),
                  numero: sifen(d),
                  fecha: d.dfeemide,
                  cliente: nombre(d),
                  total: Number(d.nota_credito_subtotal?.[0]?.dtotgralope ?? 0),
                  moneda: d.moneda?.codigo ?? null,
                }),
              ),
            )
        : null,
      ids(TIPO_DOC_VIAJE.REMISION).length
        ? this.prisma.nota_remision_cab
            .findMany({
              where: { id: { in: ids(TIPO_DOC_VIAJE.REMISION) } },
              select: {
                id: true, estado: true, dest: true, dpunexp: true, dnumdoc: true, dfeemide: true,
                clientes: { select: { personas: { select: { razon_social: true } } } },
              },
            })
            .then((rows) =>
              rows.forEach((d) =>
                guardar(TIPO_DOC_VIAJE.REMISION, d.id, {
                  estado: d.estado ?? null,
                  anulado: estaDeBaja(TIPO_DOC_VIAJE.REMISION, d),
                  numero: sifen(d),
                  fecha: d.dfeemide,
                  cliente: nombre(d),
                  total: null,
                  moneda: null,
                }),
              ),
            )
        : null,
      ids(TIPO_DOC_VIAJE.RECIBO).length
        ? this.prisma.recibos_cobro
            .findMany({
              where: { id: { in: ids(TIPO_DOC_VIAJE.RECIBO) } },
              select: {
                id: true, estado: true, numero_recibo: true, fecha_emision: true, monto_total: true,
                moneda: { select: { codigo: true } },
                clientes: { select: { personas: { select: { razon_social: true } } } },
              },
            })
            .then((rows) =>
              rows.forEach((d) =>
                guardar(TIPO_DOC_VIAJE.RECIBO, d.id, {
                  estado: d.estado ?? null,
                  anulado: estaDeBaja(TIPO_DOC_VIAJE.RECIBO, d),
                  numero: d.numero_recibo,
                  fecha: d.fecha_emision,
                  cliente: nombre(d),
                  total: Number(d.monto_total ?? 0),
                  moneda: d.moneda?.codigo ?? null,
                }),
              ),
            )
        : null,
      ids(TIPO_DOC_VIAJE.PEDIDO).length
        ? this.prisma.pedidos
            .findMany({
              where: { id: { in: ids(TIPO_DOC_VIAJE.PEDIDO) } },
              select: {
                id: true, estado: true, estado_orden: true, numero_pedido: true, fecha_pedido: true, total: true,
                moneda: { select: { codigo: true } },
                cliente: { select: { personas: { select: { razon_social: true } } } },
              },
            })
            .then((rows) =>
              rows.forEach((d) =>
                guardar(TIPO_DOC_VIAJE.PEDIDO, d.id, {
                  estado: d.estado ?? null,
                  anulado: estaDeBaja(TIPO_DOC_VIAJE.PEDIDO, d),
                  numero: d.numero_pedido,
                  fecha: d.fecha_pedido,
                  cliente: nombre(d),
                  total: Number(d.total ?? 0),
                  moneda: d.moneda?.codigo ?? null,
                }),
              ),
            )
        : null,
      ids(TIPO_DOC_VIAJE.PRESUPUESTO).length
        ? this.prisma.presupuesto_cab
            .findMany({
              where: { id: { in: ids(TIPO_DOC_VIAJE.PRESUPUESTO) } },
              select: {
                id: true, estado: true, numero: true, fecha_emision: true, total: true,
                moneda: { select: { codigo: true } },
                clientes: { select: { personas: { select: { razon_social: true } } } },
              },
            })
            .then((rows) =>
              rows.forEach((d) =>
                guardar(TIPO_DOC_VIAJE.PRESUPUESTO, d.id, {
                  estado: d.estado ?? null,
                  anulado: estaDeBaja(TIPO_DOC_VIAJE.PRESUPUESTO, d),
                  numero: d.numero,
                  fecha: d.fecha_emision,
                  cliente: nombre(d),
                  total: Number(d.total ?? 0),
                  moneda: d.moneda?.codigo ?? null,
                }),
              ),
            )
        : null,
    ]);

    return documentos.map((d) => ({
      ...d,
      // Un documento que ya no existe se marca como anulado: no cuenta y se puede quitar.
      ...(resueltos.get(`${d.tipo}::${d.documento_id}`) ?? {
        numero: null,
        fecha: null,
        cliente: null,
        total: null,
        moneda: null,
        estado: 'No existe',
        anulado: true,
      }),
    }));
  }
}
