import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
  AprobarRendicionDto,
  CreateRendicionDto,
  EnviarTesoreriaDto,
  EstadoRendicion,
  RechazarRendicionDto,
  RevisarRendicionDto,
  VerificarMedioPagoDto,
} from './dto/rendiciones.dto';
import { ReciboCobroEstado } from '../recibos/recibos.enums';
import { endOfDayAsuncion, startOfDayAsuncion, ymdInAsuncion } from '../utils/utilidades';

/**
 * Resuelve la categoría unificada (EFECTIVO/CHEQUE/TRANSFERENCIA/TARJETA/QR/OTRO)
 * a partir del medio de pago LEGACY de un detalle. Tolera empresas que tengan
 * códigos no canónicos (5,6,7,8 además de 1-4) y cae a matchear por descripción.
 * Debe mantenerse alineado con `metaPara`/`codigoToKey` del cliente mobile
 * (`RendicionScreen.tsx`) para que ambos clientes muestren los mismos buckets.
 */
export function resolverMedio(
  codigo: number | null | undefined,
  descripcion?: string | null,
): string {
  const tablaCodigo: Record<number, string> = {
    1: 'EFECTIVO',
    2: 'CHEQUE',
    3: 'TARJETA',
    4: 'TRANSFERENCIA',
    5: 'TRANSFERENCIA',
    6: 'CHEQUE',
    7: 'QR',
    8: 'TARJETA',
  };
  if (codigo != null && tablaCodigo[codigo]) return tablaCodigo[codigo];
  const desc = (descripcion ?? '').toLowerCase();
  if (desc.includes('cheque')) return 'CHEQUE';
  if (desc.includes('transf')) return 'TRANSFERENCIA';
  if (desc.includes('tarj')) return 'TARJETA';
  if (desc.includes('efectivo') || desc === 'cash') return 'EFECTIVO';
  if (desc.includes('qr')) return 'QR';
  return 'OTRO';
}

/**
 * RendicionesService — F2/F3/F4
 * ────────────────────────────────────────────────────────────────────────
 * Flujo de estados:
 *   BORRADOR  → cobrador arma rendición (editable)
 *   PENDIENTE → enviada a tesorería
 *   OBSERVADO → tesorería detectó diferencias, vuelve a cobrador
 *   APROBADO  → tesorería confirma; comisiones quedan liquidables
 *   RECHAZADO → cerrada como rechazada
 *   ANULADO   → cancelada
 */
@Injectable()
export class RendicionesService {
  constructor(private prisma: PrismaService) {}

  // Holdings ven sus subsidiarias
  private async resolveEmpresaIds(empresaId: string): Promise<string[]> {
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { type: true },
    });
    if (empresa?.type === 'holding') {
      const subs = await this.prisma.empresas.findMany({
        where: { parent_id: empresaId, active: true },
        select: { id: true },
      });
      return [empresaId, ...subs.map((s) => s.id)];
    }
    return [empresaId];
  }

  private empresaWhere(ids: string[]) {
    return ids.length === 1 ? { empresa_id: ids[0] } : { empresa_id: { in: ids } };
  }

  private async logHistorial(
    tx: Prisma.TransactionClient,
    rendicionId: string,
    estadoAnterior: string | null,
    estadoNuevo: EstadoRendicion,
    usuarioId: string,
    motivo?: string | null,
  ) {
    await tx.rendicion_historial.create({
      data: {
        rendicion_id: rendicionId,
        estado_anterior: estadoAnterior,
        estado_nuevo: estadoNuevo,
        usuario_id: usuarioId,
        motivo: motivo ?? null,
      },
    });
  }

  // ── Crear rendición (BORRADOR) ───────────────────────────────────────
  async create(empresaId: string, userId: string, dto: CreateRendicionDto) {
    const eIds = await this.resolveEmpresaIds(empresaId);

    const cobrador = await this.prisma.vendedores_cobradores.findFirst({
      where: { id: dto.cobrador_id, ...this.empresaWhere(eIds), active: true },
    });
    if (!cobrador) throw new NotFoundException('Cobrador no encontrado o inactivo');

    // Unicidad: solo una rendición activa por cobrador
    const activa = await this.prisma.rendiciones_cobranza.findFirst({
      where: {
        cobrador_id: dto.cobrador_id,
        estado: { in: ['BORRADOR', 'PENDIENTE', 'OBSERVADO'] },
        deleted_at: null,
      },
      select: { id: true, codigo: true, estado: true },
    });
    if (activa) {
      throw new BadRequestException(
        `El cobrador ya tiene una rendición ${activa.estado.toLowerCase()} (${activa.codigo ?? activa.id})`,
      );
    }

    let recibos: any[] = [];
    if (dto.recibo_ids?.length) {
      recibos = await this.prisma.recibos_cobro.findMany({
        where: {
          id: { in: dto.recibo_ids },
          ...this.empresaWhere(eIds),
          rendicion_id: null,
          estado: ReciboCobroEstado.EMITIDO,
        },
        include: {
          recibo_cobro_detalle: {
            include: { medio_pago: { select: { id: true, codigo: true, descripcion: true } } },
          },
          medios_pago_multi: { select: { medio: true, monto: true } },
        },
      });
      if (recibos.length !== dto.recibo_ids.length) {
        const encontrados = new Set(recibos.map((r) => r.id));
        const faltantes = dto.recibo_ids.filter((id) => !encontrados.has(id));
        // Identificá específicamente los anulados para dar mejor feedback al cobrador.
        const anulados = await this.prisma.recibos_cobro.findMany({
          where: { id: { in: faltantes }, estado: { not: ReciboCobroEstado.EMITIDO } },
          select: { numero_recibo: true },
        });
        if (anulados.length) {
          throw new BadRequestException(
            `Hay recibos anulados o ya rendidos en la selección: ${anulados
              .map((a) => a.numero_recibo)
              .join(', ')}. Refrescá la lista y reintentá.`,
          );
        }
        throw new BadRequestException(
          `Recibos no disponibles: ${faltantes.join(', ')}`,
        );
      }
    } else {
      recibos = await this.prisma.recibos_cobro.findMany({
        where: {
          ...this.empresaWhere(eIds),
          rendicion_id: null,
          estado: ReciboCobroEstado.EMITIDO,
          moneda_id: dto.moneda_id,
          cobrador_id: dto.cobrador_id,
          fecha_emision: {
            gte: startOfDayAsuncion(dto.fecha_cobranza),
            lte: endOfDayAsuncion(dto.fecha_cobranza),
          },
        },
        include: {
          recibo_cobro_detalle: {
            include: { medio_pago: { select: { id: true, codigo: true, descripcion: true } } },
          },
          medios_pago_multi: { select: { medio: true, monto: true } },
        },
      });
    }

    let montoEfectivo = 0;
    let montoCheque = 0;
    let montoTransferencia = 0;
    let montoTarjeta = 0;
    let montoCobrado = 0;

    for (const recibo of recibos) {
      if (recibo.modo === 'MULTI') {
        for (const mp of recibo.medios_pago_multi || []) {
          const monto = Number(mp.monto || 0);
          montoCobrado += monto;
          const m = String(mp.medio || '').toUpperCase();
          if (m === 'EFECTIVO') montoEfectivo += monto;
          else if (m === 'CHEQUE') montoCheque += monto;
          else if (m === 'TRANSFERENCIA') montoTransferencia += monto;
          else if (m === 'TARJETA') montoTarjeta += monto;
          else montoEfectivo += monto;
        }
      } else {
        for (const det of recibo.recibo_cobro_detalle) {
          const monto = Number(det.monto_pagado || 0);
          montoCobrado += monto;
          const medio = resolverMedio(det.medio_pago?.codigo, det.medio_pago?.descripcion);
          if (medio === 'EFECTIVO') montoEfectivo += monto;
          else if (medio === 'CHEQUE') montoCheque += monto;
          else if (medio === 'TRANSFERENCIA') montoTransferencia += monto;
          else if (medio === 'TARJETA') montoTarjeta += monto;
          else montoEfectivo += monto;
        }
      }
    }

    // Validar que TODOS los recibos compartan la moneda elegida
    const monedasDistintas = Array.from(
      new Set(recibos.map((r) => r.moneda_id).filter(Boolean)),
    );
    if (recibos.length > 0) {
      if (monedasDistintas.length > 1) {
        throw new BadRequestException(
          'Los recibos seleccionados pertenecen a distintas monedas. Una rendición agrupa una sola moneda.',
        );
      }
      if (monedasDistintas[0] && monedasDistintas[0] !== dto.moneda_id) {
        throw new BadRequestException(
          'La moneda de los recibos no coincide con la moneda seleccionada para la rendición.',
        );
      }
    }

    const montoEsperado = dto.monto_esperado ?? montoCobrado;
    const totalDeclarado = dto.total_declarado ?? montoCobrado;
    const diferencia = montoCobrado - montoEsperado;

    const rendicion = await this.prisma.$transaction(async (tx) => {
      const nueva = await tx.rendiciones_cobranza.create({
        data: {
          empresa_id: cobrador.empresa_id,
          cobrador_id: dto.cobrador_id,
          ruta_id: dto.ruta_id,
          moneda_id: dto.moneda_id,
          fecha_cobranza: new Date(dto.fecha_cobranza),
          monto_esperado: montoEsperado,
          monto_cobrado: montoCobrado,
          monto_efectivo: montoEfectivo,
          monto_cheque: montoCheque,
          monto_transferencia: montoTransferencia,
          monto_tarjeta: montoTarjeta,
          diferencia,
          cantidad_recibos: recibos.length,
          total_declarado: totalDeclarado,
          estado: 'BORRADOR',
          observaciones_cobrador: dto.observaciones_cobrador,
        },
      });

      if (recibos.length > 0) {
        await tx.recibos_cobro.updateMany({
          where: { id: { in: recibos.map((r) => r.id) } },
          data: { rendicion_id: nueva.id },
        });
      }

      await this.logHistorial(tx, nueva.id, null, 'BORRADOR', userId, 'Creación');
      return nueva;
    });

    return this.findOne(empresaId, rendicion.id);
  }

  // ── Enviar a tesorería (BORRADOR/OBSERVADO → PENDIENTE) ──────────────
  async enviarATesoreria(
    empresaId: string,
    id: string,
    userId: string,
    dto: EnviarTesoreriaDto,
  ) {
    const r = await this.findOne(empresaId, id);
    if (!['BORRADOR', 'OBSERVADO'].includes(r.estado)) {
      throw new BadRequestException(
        `Solo se puede enviar a tesorería desde BORRADOR u OBSERVADO (estado actual: ${r.estado})`,
      );
    }
    if ((r.cantidad_recibos ?? 0) === 0) {
      throw new BadRequestException('La rendición no tiene recibos vinculados');
    }

    return this.prisma.$transaction(async (tx) => {
      const updated = await tx.rendiciones_cobranza.update({
        where: { id },
        data: {
          estado: 'PENDIENTE',
          fecha_envio_tesoreria: new Date(),
          total_declarado: dto.total_declarado ?? r.total_declarado ?? r.monto_cobrado,
          observaciones_cobrador: dto.observaciones_cobrador ?? r.observaciones_cobrador,
        },
      });
      await this.logHistorial(tx, id, r.estado, 'PENDIENTE', userId);
      return updated;
    });
  }

  // ── Verificar un medio de pago (Tesorería, rendición en PENDIENTE) ───
  async verificarMedioPago(
    empresaId: string,
    rendicionId: string,
    medioPagoId: string,
    userId: string,
    dto: VerificarMedioPagoDto,
  ) {
    const r = await this.findOne(empresaId, rendicionId);
    if (r.estado !== 'PENDIENTE') {
      throw new BadRequestException(
        `Solo se verifica medios de pago en rendición PENDIENTE (estado: ${r.estado})`,
      );
    }

    // Buscar el medio de pago y validar que pertenezca a un recibo de la rendición
    const mp = await this.prisma.recibo_cobro_medios_pago.findUnique({
      where: { id: medioPagoId },
      include: { recibo: { select: { id: true, rendicion_id: true } } },
    });
    if (!mp || mp.recibo?.rendicion_id !== rendicionId) {
      throw new NotFoundException('Medio de pago no pertenece a esta rendición');
    }

    return this.prisma.recibo_cobro_medios_pago.update({
      where: { id: medioPagoId },
      data: {
        importe_verificado: dto.importe_verificado,
        verificacion_estado: dto.verificacion_estado,
        verificacion_observacion: dto.verificacion_observacion ?? null,
        verificado_por: userId,
        verificado_at: new Date(),
      },
    });
  }

  // ── Aprobar rendición (PENDIENTE → APROBADO) ─────────────────────────
  async aprobar(empresaId: string, id: string, userId: string, dto: AprobarRendicionDto) {
    const r = await this.findOne(empresaId, id);
    if (r.estado !== 'PENDIENTE') {
      throw new BadRequestException(
        `Solo se aprueba una rendición PENDIENTE (estado: ${r.estado})`,
      );
    }

    // Recolectar medios_pago de los recibos de la rendición
    const reciboIds = (r.recibos_cobro || []).map((rc: any) => rc.id);
    const medios = reciboIds.length
      ? await this.prisma.recibo_cobro_medios_pago.findMany({
          where: { recibo_cobro_id: { in: reciboIds } },
          select: {
            id: true,
            monto: true,
            importe_verificado: true,
            verificacion_estado: true,
          },
        })
      : [];

    const sinVerificar = medios.filter((m) => !m.verificacion_estado);
    if (sinVerificar.length > 0) {
      throw new BadRequestException(
        `Hay ${sinVerificar.length} medios de pago sin verificar. Verifique todos antes de aprobar.`,
      );
    }
    const rechazados = medios.filter((m) => m.verificacion_estado === 'RECHAZADO');
    if (rechazados.length > 0) {
      throw new BadRequestException(
        `Hay ${rechazados.length} medios de pago RECHAZADOS. No se puede aprobar; observe o rechace la rendición.`,
      );
    }

    // Si no hay medios_pago_multi (rendición de recibos LEGACY del panel de cobros),
    // la aprobación es por total declarado: se confía en los recibos individuales.
    const totalVerificado =
      medios.length === 0
        ? Number(r.total_declarado ?? r.monto_cobrado ?? 0)
        : medios.reduce(
            (s, m) => s + Number(m.importe_verificado ?? m.monto ?? 0),
            0,
          );

    return this.prisma.$transaction(async (tx) => {
      const updated = await tx.rendiciones_cobranza.update({
        where: { id },
        data: {
          estado: 'APROBADO',
          total_verificado: totalVerificado,
          fecha_aprobacion: new Date(),
          aprobado_por: userId,
          supervisor_id: userId,
          caja_destino_id: dto.caja_destino_id ?? null,
          observaciones_supervisor: dto.observaciones_supervisor ?? r.observaciones_supervisor,
          // asiento_id: TODO F3.B (integración asientos.service)
        },
      });

      // Marcar comisiones de los recibos como 'aprobada' (liquidables)
      if (reciboIds.length) {
        await tx.comisiones.updateMany({
          where: {
            recibo_id: { in: reciboIds },
            estado: 'pendiente',
            fecha_liquidacion: null,
          },
          data: { estado: 'aprobada' },
        });
      }

      await this.logHistorial(tx, id, r.estado, 'APROBADO', userId);
      return updated;
    });
  }

  // ── Observar o rechazar (PENDIENTE → OBSERVADO/RECHAZADO) ────────────
  async rechazar(empresaId: string, id: string, userId: string, dto: RechazarRendicionDto) {
    const r = await this.findOne(empresaId, id);
    if (r.estado !== 'PENDIENTE') {
      throw new BadRequestException(
        `Solo se observa/rechaza una rendición PENDIENTE (estado: ${r.estado})`,
      );
    }
    if (!dto.motivo?.trim()) {
      throw new BadRequestException('El motivo es obligatorio');
    }

    return this.prisma.$transaction(async (tx) => {
      const updated = await tx.rendiciones_cobranza.update({
        where: { id },
        data: {
          estado: dto.accion,
          motivo_rechazo: dto.motivo,
          supervisor_id: userId,
          // si OBSERVADO, vuelve al cobrador → no toca comisiones
          // si RECHAZADO, comisiones quedan en 'pendiente' (no liquidables)
        },
      });
      await this.logHistorial(tx, id, r.estado, dto.accion, userId, dto.motivo);
      return updated;
    });
  }

  // ── Listar rendiciones ───────────────────────────────────────────────
  async findAll(
    empresaId: string,
    filters: {
      cobrador_id?: string;
      fecha_desde?: string;
      fecha_hasta?: string;
      estado?: string;
      moneda_id?: string;
      page?: number;
      limit?: number;
    },
  ) {
    const { cobrador_id, fecha_desde, fecha_hasta, estado, moneda_id, page = 1, limit = 20 } = filters;
    const eIds = await this.resolveEmpresaIds(empresaId);
    const where: any = { ...this.empresaWhere(eIds), deleted_at: null };
    if (cobrador_id) where.cobrador_id = cobrador_id;
    if (estado) where.estado = estado;
    if (moneda_id) where.moneda_id = moneda_id;
    if (fecha_desde || fecha_hasta) {
      where.fecha_cobranza = {};
      if (fecha_desde) where.fecha_cobranza.gte = new Date(fecha_desde);
      if (fecha_hasta) where.fecha_cobranza.lte = new Date(fecha_hasta);
    }

    const [data, total] = await Promise.all([
      this.prisma.rendiciones_cobranza.findMany({
        where,
        include: {
          cobrador: { select: { id: true, nombre: true, apellido: true } },
          supervisor: { select: { id: true, nombres: true, apellidos: true } },
          aprobador: { select: { id: true, nombres: true, apellidos: true } },
          rutas_cobranza: { select: { id: true, fecha: true, estado: true } },
          moneda: { select: { id: true, codigo: true, simbolo: true, decimales: true } },
          _count: { select: { recibos_cobro: true } },
        },
        orderBy: { fecha_rendicion: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.rendiciones_cobranza.count({ where }),
    ]);

    return { data, total, page, limit, totalPages: Math.ceil(total / limit) };
  }

  // ── Detalle de una rendición ─────────────────────────────────────────
  async findOne(empresaId: string, id: string): Promise<any> {
    const eIds = await this.resolveEmpresaIds(empresaId);
    const rendicion = await this.prisma.rendiciones_cobranza.findFirst({
      where: { id, ...this.empresaWhere(eIds) },
      include: {
        cobrador: {
          select: { id: true, nombre: true, apellido: true, telefono: true, codigo: true },
        },
        supervisor: { select: { id: true, nombres: true, apellidos: true } },
        aprobador: { select: { id: true, nombres: true, apellidos: true } },
        rutas_cobranza: { select: { id: true, fecha: true, estado: true } },
        moneda: { select: { id: true, codigo: true, simbolo: true, decimales: true } },
        historial: {
          orderBy: { created_at: 'desc' },
          include: { usuario: { select: { id: true, nombres: true, apellidos: true } } },
        },
        recibos_cobro: {
          include: {
            clientes: {
              select: {
                id: true,
                personas: { select: { razon_social: true, ruc: true } },
              },
            },
            facturas_multi: {
              select: {
                id: true,
                factura: { select: { id: true, dnumdoc: true } },
              },
            },
            recibo_cobro_detalle: {
              include: {
                medio_pago: { select: { id: true, codigo: true, descripcion: true } },
                bancos: { select: { id: true, descripcion: true } },
                factura_cuotas: {
                  select: {
                    id: true,
                    factura_cab: { select: { id: true, dnumdoc: true, total_factura: true } },
                  },
                },
              },
            },
            medios_pago_multi: {
              include: {
                moneda: { select: { id: true, codigo: true } },
                verificador: { select: { id: true, nombres: true, apellidos: true } },
              },
            },
          },
          orderBy: { fecha_emision: 'desc' },
        },
      },
    });

    if (!rendicion) throw new NotFoundException('Rendición no encontrada');
    return this.normalizarRendicion(rendicion);
  }

  // ── Normalizador: unifica medios de pago MULTI + LEGACY ──────────────
  // Devuelve cada recibo con:
  //   - medios_pago_unificado: [{id, medio, monto, ..., verificacion, origen}]
  //   - facturas_numeros: ["001-001-0000014", ...]
  // Los recibos legacy se consideran auto-verificados (estado OK) porque
  // su validación ocurrió al momento de emitir el recibo.
  private normalizarRendicion(r: any): any {
    const codigoToMedio = resolverMedio;
    const recibos = (r.recibos_cobro || []).map((rec: any) => {
      const medios: any[] = [];
      const facturasSet = new Set<string>();

      (rec.facturas_multi || []).forEach((fm: any) => {
        if (fm.factura?.dnumdoc) facturasSet.add(String(fm.factura.dnumdoc));
      });

      if ((rec.medios_pago_multi || []).length > 0) {
        for (const m of rec.medios_pago_multi) {
          medios.push({
            id: m.id,
            origen: 'MULTI',
            medio: (m.medio || 'OTRO').toUpperCase(),
            monto: m.monto,
            moneda_id: m.moneda_id,
            cuenta_tesoreria_id: m.cuenta_tesoreria_id,
            referencia: m.referencia,
            cheque_numero: m.cheque_numero,
            cheque_banco: m.cheque_banco,
            cheque_titular: m.cheque_titular,
            verificacion: {
              estado: m.verificacion_estado,
              importe_verificado: m.importe_verificado,
              observacion: m.verificacion_observacion,
              verificado_at: m.verificado_at,
              verificador: m.verificador,
            },
          });
        }
      } else {
        for (const d of rec.recibo_cobro_detalle || []) {
          if (d.factura_cuotas?.factura_cab?.dnumdoc) {
            facturasSet.add(String(d.factura_cuotas.factura_cab.dnumdoc));
          }
          medios.push({
            id: `legacy:${d.id}`,
            origen: 'LEGACY',
            medio: codigoToMedio(d.medio_pago?.codigo, d.medio_pago?.descripcion),
            monto: d.monto_pagado,
            moneda_id: null,
            cuenta_tesoreria_id: null,
            referencia: d.codigo_autorizacion || null,
            cheque_numero: d.numero_cheque || null,
            cheque_banco: d.bancos?.descripcion || d.banco_emisor || null,
            cheque_titular: null,
            verificacion: {
              estado: 'OK',
              importe_verificado: d.monto_pagado,
              observacion: null,
              verificado_at: null,
              verificador: null,
            },
          });
        }
      }

      return {
        ...rec,
        medios_pago_unificado: medios,
        facturas_numeros: Array.from(facturasSet),
      };
    });

    return { ...r, recibos_cobro: recibos };
  }

  // ── Legacy: revisar → mapea a aprobar/rechazar nuevos ────────────────
  async revisar(empresaId: string, id: string, userId: string, dto: RevisarRendicionDto) {
    if (dto.accion === 'aprobada') {
      return this.aprobar(empresaId, id, userId, {
        observaciones_supervisor: dto.observaciones_supervisor,
      });
    }
    const accion = dto.accion === 'con_observacion' ? 'OBSERVADO' : 'RECHAZADO';
    return this.rechazar(empresaId, id, userId, {
      accion: accion as 'OBSERVADO' | 'RECHAZADO',
      motivo: dto.observaciones_supervisor ?? 'Sin motivo',
    });
  }

  // ── Resumen diario ───────────────────────────────────────────────────
  async getResumenDiario(empresaId: string, fecha?: string) {
    // Normalizar a día calendario Asunción. Si no viene, usa hoy en Asunción.
    const ymd = fecha ? fecha.slice(0, 10) : ymdInAsuncion();
    const fechaInicio = startOfDayAsuncion(ymd);
    const fechaFin = endOfDayAsuncion(ymd);

    const eIds = await this.resolveEmpresaIds(empresaId);

    const rendiciones = await this.prisma.rendiciones_cobranza.findMany({
      where: {
        ...this.empresaWhere(eIds),
        deleted_at: null,
        fecha_cobranza: { gte: fechaInicio, lte: fechaFin },
      },
      select: {
        estado: true,
        monto_cobrado: true,
        monto_esperado: true,
        monto_efectivo: true,
        monto_cheque: true,
        monto_transferencia: true,
        monto_tarjeta: true,
        diferencia: true,
        cantidad_recibos: true,
        moneda_id: true,
        moneda: { select: { id: true, codigo: true, simbolo: true, decimales: true } },
      },
    });

    const byEstado = (e: string) => rendiciones.filter((r) => r.estado === e).length;
    const sum = (k: keyof (typeof rendiciones)[number]) =>
      rendiciones.reduce((s, r) => s + Number((r as any)[k] || 0), 0);

    // Totales agrupados por moneda (una rendición = una moneda)
    const porMonedaMap = new Map<string, any>();
    for (const r of rendiciones) {
      const key = r.moneda_id || 'sin-moneda';
      if (!porMonedaMap.has(key)) {
        porMonedaMap.set(key, {
          moneda: r.moneda || null,
          cobrado: 0,
          esperado: 0,
          diferencia: 0,
          recibos: 0,
          cantidad_rendiciones: 0,
        });
      }
      const acc = porMonedaMap.get(key);
      acc.cobrado += Number(r.monto_cobrado || 0);
      acc.esperado += Number(r.monto_esperado || 0);
      acc.diferencia += Number(r.diferencia || 0);
      acc.recibos += r.cantidad_recibos || 0;
      acc.cantidad_rendiciones += 1;
    }

    return {
      fecha: ymd,
      total_rendiciones: rendiciones.length,
      por_estado: {
        borrador: byEstado('BORRADOR'),
        pendientes: byEstado('PENDIENTE'),
        observadas: byEstado('OBSERVADO'),
        aprobadas: byEstado('APROBADO'),
        rechazadas: byEstado('RECHAZADO'),
        anuladas: byEstado('ANULADO'),
      },
      totales: {
        cobrado: sum('monto_cobrado'),
        esperado: sum('monto_esperado'),
        diferencia: sum('diferencia'),
        recibos: rendiciones.reduce((s, r) => s + (r.cantidad_recibos || 0), 0),
      },
      totales_por_moneda: Array.from(porMonedaMap.values()),
      desglose_medios: {
        efectivo: sum('monto_efectivo'),
        cheque: sum('monto_cheque'),
        transferencia: sum('monto_transferencia'),
        tarjeta: sum('monto_tarjeta'),
      },
    };
  }

  // ── REPORTES (F7) ──────────────────────────────────────────────────

  // 1. Cobranzas del día agrupadas por cobrador
  async reporteCobranzasDia(
    empresaId: string,
    fechaDesde?: string,
    fechaHasta?: string,
  ) {
    const hoyAsuncion = ymdInAsuncion();
    const desdeYmd = (fechaDesde ?? hoyAsuncion).slice(0, 10);
    const hastaYmd = (fechaHasta ?? desdeYmd).slice(0, 10);
    const ini = startOfDayAsuncion(desdeYmd);
    const fin = endOfDayAsuncion(hastaYmd);

    const eIds = await this.resolveEmpresaIds(empresaId);
    const rows = await this.prisma.rendiciones_cobranza.findMany({
      where: {
        ...this.empresaWhere(eIds),
        deleted_at: null,
        fecha_cobranza: { gte: ini, lte: fin },
        estado: { notIn: ['ANULADO'] },
      },
      include: {
        cobrador: { select: { id: true, nombre: true, apellido: true } },
      },
      orderBy: [{ cobrador_id: 'asc' }, { fecha_rendicion: 'asc' }],
    });

    // Agrupar por cobrador
    const map = new Map<string, any>();
    for (const r of rows) {
      const key = r.cobrador_id;
      if (!map.has(key)) {
        map.set(key, {
          cobrador_id: r.cobrador_id,
          cobrador: r.cobrador
            ? `${r.cobrador.nombre} ${r.cobrador.apellido || ''}`.trim()
            : '—',
          rendiciones: 0,
          recibos: 0,
          total_cobrado: 0,
          total_efectivo: 0,
          total_cheque: 0,
          total_transferencia: 0,
          total_tarjeta: 0,
          diferencia: 0,
          aprobadas: 0,
          pendientes: 0,
          observadas: 0,
          rechazadas: 0,
        });
      }
      const acc = map.get(key);
      acc.rendiciones += 1;
      acc.recibos += r.cantidad_recibos || 0;
      acc.total_cobrado += Number(r.monto_cobrado || 0);
      acc.total_efectivo += Number(r.monto_efectivo || 0);
      acc.total_cheque += Number(r.monto_cheque || 0);
      acc.total_transferencia += Number(r.monto_transferencia || 0);
      acc.total_tarjeta += Number(r.monto_tarjeta || 0);
      acc.diferencia += Number(r.diferencia || 0);
      if (r.estado === 'APROBADO') acc.aprobadas += 1;
      else if (r.estado === 'PENDIENTE') acc.pendientes += 1;
      else if (r.estado === 'OBSERVADO') acc.observadas += 1;
      else if (r.estado === 'RECHAZADO') acc.rechazadas += 1;
    }

    const data = Array.from(map.values()).sort(
      (a, b) => b.total_cobrado - a.total_cobrado,
    );
    const totales = data.reduce(
      (s, r) => ({
        rendiciones: s.rendiciones + r.rendiciones,
        recibos: s.recibos + r.recibos,
        total_cobrado: s.total_cobrado + r.total_cobrado,
        total_efectivo: s.total_efectivo + r.total_efectivo,
        total_cheque: s.total_cheque + r.total_cheque,
        total_transferencia: s.total_transferencia + r.total_transferencia,
        total_tarjeta: s.total_tarjeta + r.total_tarjeta,
        diferencia: s.diferencia + r.diferencia,
      }),
      {
        rendiciones: 0,
        recibos: 0,
        total_cobrado: 0,
        total_efectivo: 0,
        total_cheque: 0,
        total_transferencia: 0,
        total_tarjeta: 0,
        diferencia: 0,
      },
    );

    return {
      fecha_desde: ini.toISOString().split('T')[0],
      fecha_hasta: fin.toISOString().split('T')[0],
      // Compat: campo `fecha` mantenido para clientes que aún esperan un día puntual
      fecha: ini.toISOString().split('T')[0],
      por_cobrador: data,
      totales,
    };
  }

  // 2. Rendiciones pendientes con antigüedad (en PENDIENTE / OBSERVADO)
  async reportePendientes(empresaId: string) {
    const eIds = await this.resolveEmpresaIds(empresaId);
    const rows = await this.prisma.rendiciones_cobranza.findMany({
      where: {
        ...this.empresaWhere(eIds),
        deleted_at: null,
        estado: { in: ['PENDIENTE', 'OBSERVADO'] },
      },
      include: {
        cobrador: { select: { id: true, nombre: true, apellido: true } },
      },
      orderBy: { fecha_envio_tesoreria: 'asc' },
    });

    const ahora = Date.now();
    return rows.map((r) => {
      const ref = (r.fecha_envio_tesoreria ?? r.fecha_rendicion).getTime();
      const horas = Math.floor((ahora - ref) / 36e5);
      const dias = Math.floor(horas / 24);
      return {
        id: r.id,
        codigo: r.codigo,
        cobrador_id: r.cobrador_id,
        cobrador: r.cobrador
          ? `${r.cobrador.nombre} ${r.cobrador.apellido || ''}`.trim()
          : '—',
        fecha_cobranza: r.fecha_cobranza,
        fecha_envio_tesoreria: r.fecha_envio_tesoreria,
        estado: r.estado,
        total_declarado: r.total_declarado,
        monto_cobrado: r.monto_cobrado,
        cantidad_recibos: r.cantidad_recibos,
        antiguedad_horas: horas,
        antiguedad_dias: dias,
      };
    });
  }

  // 3. Diferencias (usa vista vw_rendiciones_diferencias)
  async reporteDiferencias(
    empresaId: string,
    filtros: { nivel?: string; fecha_desde?: string; fecha_hasta?: string } = {},
  ) {
    const eIds = await this.resolveEmpresaIds(empresaId);
    const conds: string[] = ['empresa_id::text = ANY($1::text[])'];
    const params: any[] = [eIds];
    if (filtros.nivel) {
      params.push(filtros.nivel);
      conds.push(`nivel = $${params.length}`);
    }
    if (filtros.fecha_desde) {
      params.push(new Date(filtros.fecha_desde));
      conds.push(`fecha_rendicion >= $${params.length}`);
    }
    if (filtros.fecha_hasta) {
      params.push(new Date(filtros.fecha_hasta));
      conds.push(`fecha_rendicion <= $${params.length}`);
    }
    const sql = `SELECT * FROM vw_rendiciones_diferencias WHERE ${conds.join(' AND ')} ORDER BY diferencia_abs DESC LIMIT 500`;
    return this.prisma.$queryRawUnsafe<any[]>(sql, ...params);
  }

  // 4. Comisiones a liquidar (usa vista vw_comisiones_a_liquidar)
  async reporteComisionesLiquidar(empresaId: string, cobradorId?: string) {
    const eIds = await this.resolveEmpresaIds(empresaId);
    const params: any[] = [eIds];
    const conds = ['empresa_id::text = ANY($1::text[])'];
    if (cobradorId) {
      params.push(cobradorId);
      conds.push(`cobrador_id = $${params.length}::uuid`);
    }
    const sql = `SELECT * FROM vw_comisiones_a_liquidar WHERE ${conds.join(' AND ')} ORDER BY fecha_aprobacion DESC NULLS LAST LIMIT 1000`;
    return this.prisma.$queryRawUnsafe<any[]>(sql, ...params);
  }

  // ── Recibos disponibles para rendición ───────────────────────────────
  async getRecibosDisponibles(empresaId: string, cobradorId?: string, fecha?: string, monedaId?: string) {
    const eIds = await this.resolveEmpresaIds(empresaId);
    const where: any = {
      ...this.empresaWhere(eIds),
      rendicion_id: null,
      estado: ReciboCobroEstado.EMITIDO,
    };
    if (monedaId) where.moneda_id = monedaId;

    if (cobradorId) {
      const cobrador = await this.prisma.vendedores_cobradores.findUnique({
        where: { id: cobradorId },
        select: { usuario_id: true },
      });
      // Resiliencia: si hay data sucia con varios vendedores_cobradores apuntando al
      // mismo usuario_id, expandimos el filtro a TODOS los cobradores del mismo
      // usuario, así no perdemos recibos asignados al "hermano gemelo".
      let cobradorIds = [cobradorId];
      if (cobrador?.usuario_id) {
        const hermanos = await this.prisma.vendedores_cobradores.findMany({
          where: { usuario_id: cobrador.usuario_id, active: true, tipo: { in: ['cobrador', 'ambos'] } },
          select: { id: true },
        });
        cobradorIds = Array.from(new Set([cobradorId, ...hermanos.map((h) => h.id)]));
      }
      const cobradorFilters: any[] = [{ cobrador_id: { in: cobradorIds } }];
      if (cobrador?.usuario_id) {
        cobradorFilters.push({ cobrador_id: null, usuario_id: cobrador.usuario_id });
      }
      where.OR = cobradorFilters;
    }

    if (fecha) {
      where.fecha_emision = {
        gte: startOfDayAsuncion(fecha),
        lte: endOfDayAsuncion(fecha),
      };
    }

    const recibos = await this.prisma.recibos_cobro.findMany({
      where,
      include: {
        clientes: {
          select: { id: true, personas: { select: { razon_social: true, ruc: true } } },
        },
        cobrador: { select: { id: true, nombre: true, apellido: true } },
        moneda: { select: { id: true, codigo: true, descripcion: true, decimales: true, simbolo: true } },
        recibo_cobro_detalle: {
          include: {
            medio_pago: { select: { id: true, codigo: true, descripcion: true } },
            factura_cuotas: {
              select: {
                factura_cab: { select: { id: true, dest: true, dpunexp: true, dnumdoc: true } },
              },
            },
          },
        },
        facturas_multi: {
          select: {
            id: true,
            monto_pagado: true,
            factura: { select: { id: true, dest: true, dpunexp: true, dnumdoc: true } },
          },
        },
        medios_pago_multi: {
          select: {
            id: true,
            medio: true,
            monto: true,
            cheque_numero: true,
          },
        },
      },
      orderBy: { fecha_emision: 'desc' },
      take: 100,
    });

    // Enriquecer medios_pago_multi.medio (que es VARCHAR — a veces guarda el código,
    // a veces el nombre) con la descripción del catálogo. Útil para que la app
    // mobile muestre "Efectivo / Cheque" en lugar de "1 / 6".
    const codigosMedio = new Set<string>();
    for (const r of recibos) {
      for (const mp of r.medios_pago_multi ?? []) {
        if (mp.medio) codigosMedio.add(mp.medio);
      }
    }
    if (codigosMedio.size > 0) {
      // Lookup por codigo numérico (si parsea a int) y por descripción.
      const mediosNum = await this.prisma.medio_pago.findMany({
        select: { codigo: true, descripcion: true },
      });
      const porCodigo = new Map<string, string>();
      for (const m of mediosNum) {
        if (m.codigo != null && m.descripcion) {
          porCodigo.set(String(m.codigo), m.descripcion);
        }
      }
      for (const r of recibos) {
        for (const mp of r.medios_pago_multi ?? []) {
          (mp as any).medio_descripcion = porCodigo.get(mp.medio) ?? mp.medio;
        }
      }
    }
    return recibos;
  }

  // ── Recibos sin rendir de días anteriores (para notificación mobile) ─
  //
  // Devuelve, para un cobrador dado, los recibos EMITIDOS de días
  // calendario **anteriores** a hoy (en Asunción) que aún no fueron
  // asociados a ninguna rendición. Se agrupa por día × moneda para que la
  // app pueda mostrar una lista tipo "22/07 · Gs. 1.500.000 · 3 recibos".
  //
  // No filtra por antigüedad máxima: si el cobrador tiene recibos de hace
  // 3 días sin rendir, aparecen todos.
  async getPendientesDiasAnterioresPorCobrador(
    empresaId: string,
    cobradorId: string,
  ): Promise<
    Array<{
      fecha: string;
      cantidad_recibos: number;
      monto_total: number;
      moneda: { id: string; codigo: string | null; simbolo: string | null };
    }>
  > {
    const eIds = await this.resolveEmpresaIds(empresaId);

    // Expansión "hermanos gemelos": mismo criterio que getRecibosDisponibles.
    const cobrador = await this.prisma.vendedores_cobradores.findUnique({
      where: { id: cobradorId },
      select: { usuario_id: true },
    });
    let cobradorIds = [cobradorId];
    if (cobrador?.usuario_id) {
      const hermanos = await this.prisma.vendedores_cobradores.findMany({
        where: {
          usuario_id: cobrador.usuario_id,
          active: true,
          tipo: { in: ['cobrador', 'ambos'] },
        },
        select: { id: true },
      });
      cobradorIds = Array.from(new Set([cobradorId, ...hermanos.map((h) => h.id)]));
    }

    const cobradorFilters: any[] = [{ cobrador_id: { in: cobradorIds } }];
    if (cobrador?.usuario_id) {
      cobradorFilters.push({ cobrador_id: null, usuario_id: cobrador.usuario_id });
    }

    // Corte: inicio del día HOY en Asunción. Todo lo anterior es "pendiente".
    const hoyYmd = ymdInAsuncion();
    const corteInicioHoy = startOfDayAsuncion(hoyYmd);

    const recibos = await this.prisma.recibos_cobro.findMany({
      where: {
        ...this.empresaWhere(eIds),
        rendicion_id: null,
        estado: ReciboCobroEstado.EMITIDO,
        fecha_emision: { lt: corteInicioHoy },
        OR: cobradorFilters,
      },
      select: {
        monto_total: true,
        fecha_emision: true,
        moneda: { select: { id: true, codigo: true, simbolo: true } },
      },
    });

    // Agrupar por día calendario Asunción × moneda.
    const map = new Map<
      string,
      {
        fecha: string;
        cantidad_recibos: number;
        monto_total: number;
        moneda: { id: string; codigo: string | null; simbolo: string | null };
      }
    >();
    for (const r of recibos) {
      const fecha = ymdInAsuncion(r.fecha_emision);
      const monedaId = r.moneda?.id ?? 'sin_moneda';
      const key = `${fecha}|${monedaId}`;
      let acc = map.get(key);
      if (!acc) {
        acc = {
          fecha,
          cantidad_recibos: 0,
          monto_total: 0,
          moneda: {
            id: r.moneda?.id ?? '',
            codigo: r.moneda?.codigo ?? null,
            simbolo: r.moneda?.simbolo ?? null,
          },
        };
        map.set(key, acc);
      }
      acc.cantidad_recibos += 1;
      acc.monto_total += Number(r.monto_total ?? 0);
    }

    // Orden: día ascendente (los más viejos primero).
    return Array.from(map.values()).sort((a, b) => a.fecha.localeCompare(b.fecha));
  }
}
