import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import type { LoginUserInfo } from 'src/auth/dto/login.dto';
import { GetUser } from 'src/auth/get-user.decorator';
import { RequireModule, RequirePermission } from 'src/auth/decorators';
import { ModuleGuard } from 'src/auth/guards/module.guard';
import { PermissionGuard } from 'src/auth/guards/permission.guard';
import { PrismaService } from '../prisma/prisma.service';

@ApiTags('Panel Cobrador')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'), ModuleGuard, PermissionGuard)
@RequireModule('COBRANZAS')
@Controller({ path: 'panel-cobrador', version: '1' })
export class PanelCobradorController {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * Cuotas asignadas al cobrador del usuario logueado.
   * Ordenadas: vencidas primero, luego por fecha_vencimiento ASC.
   * Paginado para performance en móvil.
   */
  @Get('cuotas')
  @RequirePermission('COBRANZAS', 'COB_CBR_PANEL_COBRADOR_VER')
  @ApiOperation({ summary: 'Cuotas asignadas al cobrador logueado ordenadas por urgencia' })
  @ApiQuery({ name: 'page', required: false, type: Number })
  @ApiQuery({ name: 'pageSize', required: false, type: Number })
  @ApiQuery({ name: 'estado', required: false, description: 'pendiente | vencida | cobrado | todas' })
  @ApiQuery({ name: 'fecha_desde', required: false, type: String, description: 'Filtrar por vencimiento YYYY-MM-DD (manual)' })
  @ApiQuery({ name: 'fecha_hasta', required: false, type: String, description: 'Filtrar por vencimiento YYYY-MM-DD' })
  @ApiQuery({ name: 'ver_todas', required: false, type: Boolean, description: 'true = ver cuotas de toda la empresa (no solo asignadas)' })
  @ApiQuery({ name: 'search', required: false, type: String, description: 'Búsqueda libre (RUC, razón social, nombre fantasía, nro factura). Omite filtros de fecha/estado.' })
  async getCuotasCobrador(
    @GetUser() user: LoginUserInfo,
    @Query('page') page = '1',
    @Query('pageSize') pageSize = '20',
    @Query('estado') estado?: string,
    @Query('fecha_desde') fechaDesde?: string,
    @Query('fecha_hasta') fechaHasta?: string,
    @Query('ver_todas') verTodas?: string,
    @Query('search') search?: string,
  ) {
    const pageNum = Math.max(1, parseInt(page));
    const size = Math.min(500, Math.max(1, parseInt(pageSize)));
    const skip = (pageNum - 1) * size;
    const verTodasBool = verTodas === 'true';

    const vendedorCobrador = await this.prisma.vendedores_cobradores.findFirst({
      where: { usuario_id: user.id, empresa_id: user.empresa_id, active: true },
    });

    const searchTermInit = (search || '').trim();
    if (!vendedorCobrador && searchTermInit.length === 0) {
      return { data: [], total: 0, page: pageNum, pageSize: size, cobrador: null };
    }

    const hoy = new Date();
    hoy.setHours(0, 0, 0, 0);

    const searchTerm = (search || '').trim();
    const hasSearch = searchTerm.length > 0;
    const esCobrado = !hasSearch && estado === 'cobrado';

    const searchTermEarly = (search || '').trim();
    // Filtro de asignación: por defecto solo las del cobrador.
    // Con ver_todas O cuando hay búsqueda libre, se muestra toda la empresa.
    // Precedencia del cobrador (más específico → más general):
    //   1. factura_cab.cobrador_id           (override por documento)
    //   2. cliente_direcciones.cobrador_id   (asignado a la dirección de entrega)
    //   3. clientes.cobrador_id              (default del cliente)
    //   4. asignacion_facturas.cobrador_id   (asignación manual explícita, retrocompat)
    // Cada nivel solo aplica cuando el anterior está vacío.
    const filtroAsignacion = (verTodasBool || searchTermEarly.length > 0)
      ? { empresa_id: user.empresa_id }
      : {
          empresa_id: user.empresa_id,
          OR: [
            // 1. Factura con cobrador directo
            { cobrador_id: vendedorCobrador.id },
            // 2. Factura sin cobrador, con dirección cuyo cobrador coincide
            {
              cobrador_id: null,
              cliente_direccion: { cobrador_id: vendedorCobrador.id },
            },
            // 3. Factura sin cobrador, sin dirección o dirección sin cobrador,
            //    cliente con cobrador default que coincide
            {
              cobrador_id: null,
              OR: [
                { cliente_direccion_id: null },
                { cliente_direccion: { cobrador_id: null } },
              ],
              clientes: { cobrador_id: vendedorCobrador.id },
            },
            // 4. Asignación manual explícita (retrocompat)
            {
              asignacion_facturas: {
                some: { cobrador_id: vendedorCobrador.id, estado: { not: 'cancelado' } },
              },
            },
          ],
        };

    // factura_cuotas: estado pagado/cobrado vs pendiente
    const estadosPagado = ['pagado', 'Pagado', 'cobrado', 'Cobrado'];
    const facturaCabWhere: any = { ...filtroAsignacion, estado: { not: 'Anulado' } };
    if (hasSearch) {
      facturaCabWhere.OR = [
        { dnumdoc: { contains: searchTerm, mode: 'insensitive' } },
        { clientes: { nombre_fantasia: { contains: searchTerm, mode: 'insensitive' } } },
        { clientes: { personas: { razon_social: { contains: searchTerm, mode: 'insensitive' } } } },
        { clientes: { personas: { ruc: { contains: searchTerm, mode: 'insensitive' } } } },
        { clientes: { personas: { nro_documento: { contains: searchTerm, mode: 'insensitive' } } } },
      ];
    }
    const whereBase: any = {
      factura_cab: facturaCabWhere,
      estado: esCobrado
        ? { in: estadosPagado }
        : hasSearch
          ? undefined
          : { notIn: estadosPagado },
    };
    if (whereBase.estado === undefined) delete whereBase.estado;

    // Filtro por fecha de vencimiento (campo dvenccuo)
    // Regla: cuotas vencidas (< hoy) SIEMPRE se incluyen para pendientes.
    if (hasSearch) {
      // búsqueda libre: ignorar filtros de fecha/estado
    } else if (fechaHasta && !esCobrado) {
      const hasta = new Date(fechaHasta);
      hasta.setHours(23, 59, 59, 999);
      whereBase.dvenccuo = fechaDesde
        ? { gte: new Date(fechaDesde), lte: hasta }  // rango manual exacto
        : { lte: hasta };                             // chip: vencidas + hasta fecha
    } else if (fechaHasta && esCobrado) {
      whereBase.dvenccuo = {};
      if (fechaDesde) whereBase.dvenccuo.gte = new Date(fechaDesde);
      const hasta = new Date(fechaHasta);
      hasta.setHours(23, 59, 59, 999);
      whereBase.dvenccuo.lte = hasta;
    } else if (!fechaHasta && !fechaDesde && !esCobrado) {
      if (estado === 'vencida')   whereBase.dvenccuo = { lt: hoy };
      else if (estado === 'pendiente') whereBase.dvenccuo = { gte: hoy };
    }

    // Config mora para interés estimado
    const configMora = await this.prisma.config_mora.findFirst({
      where: { empresa_id: user.empresa_id },
    });

    const [cuotas, total] = await Promise.all([
      this.prisma.factura_cuotas.findMany({
        where: whereBase,
        include: {
          factura_cab: {
            select: {
              id: true,
              dnumdoc: true,
              dest: true,
              dpunexp: true,
              total_factura: true,
              saldo_pendiente: true,
              cobrador_id: true,
              clientes: {
                select: {
                  id: true,
                  nombre_fantasia: true,
                  zona: true,
                  referencia_domicilio: true,
                  personas: {
                    select: {
                      razon_social: true,
                      telefono: true,
                      celular: true,
                      direccion: true,
                    },
                  },
                },
              },
            },
          },
        },
        orderBy: [{ dvenccuo: 'asc' }],
        skip,
        take: size,
      }),
      this.prisma.factura_cuotas.count({ where: whereBase }),
    ]);

    const cuotasEnriquecidas = cuotas.map((c) => {
      const fechaVenc = new Date(c.dvenccuo);
      fechaVenc.setHours(0, 0, 0, 0);
      const diasVencido = Math.floor((hoy.getTime() - fechaVenc.getTime()) / 86400000);
      const factura = c.factura_cab ?? null;
      const montoCuota = Number(c.dmoncuota || 0);

      let mora_estimada = 0;
      if (configMora?.activo && diasVencido > 0 && !esCobrado) {
        const diasEfectivos = Math.max(0, diasVencido - (configMora.periodo_gracia ?? 0));
        if (diasEfectivos > 0) {
          const tasa = Number(configMora.tasa || 0);
          let mora = 0;
          if (configMora.tipo_calculo === 'diario')         mora = montoCuota * (tasa / 100) * diasEfectivos;
          else if (configMora.tipo_calculo === 'mensual')   mora = montoCuota * (tasa / 100) * (diasEfectivos / 30);
          else if (configMora.tipo_calculo === 'fijo_por_cuota') mora = tasa;
          const montoMin = Number((configMora as any).monto_minimo_mora || 0);
          const montoMax = Number((configMora as any).monto_maximo_mora || 0);
          if (montoMin && mora < montoMin) mora = montoMin;
          if (montoMax && mora > montoMax) mora = montoMax;
          mora_estimada = Math.round(mora);
        }
      }

      // Serializar fechas y decimales explícitamente — Prisma devuelve Date/Decimal como objetos
      const fmtDate = (d: any) => d ? new Date(d).toISOString().split('T')[0] : null;
      return {
        id: c.id,
        factura_id: c.factura_cab_id,
        numero_cuota: c.nro_cuota,
        dmoncuota: montoCuota,
        monto_cuota: montoCuota,
        saldo_pendiente: Number(c.saldo_pendiente || c.dmoncuota),
        fecha_vencimiento: fmtDate(c.dvenccuo),
        estado: c.estado,
        monto_pagado: esCobrado ? montoCuota : 0,
        factura: factura ? {
          ...factura,
          total_factura: Number(factura.total_factura || 0),
          saldo_pendiente: Number(factura.saldo_pendiente || 0),
        } : null,
        dias_vencido: diasVencido > 0 ? diasVencido : 0,
        urgencia: diasVencido > 30 ? 'critica' : diasVencido > 0 ? 'vencida' : diasVencido === 0 ? 'hoy' : 'proxima',
        mora_estimada,
      };
    });

    return {
      data: cuotasEnriquecidas,
      total,
      page: pageNum,
      pageSize: size,
      totalPages: Math.ceil(total / size),
      cobrador: vendedorCobrador
        ? { id: vendedorCobrador.id, nombre: vendedorCobrador.nombre, apellido: vendedorCobrador.apellido }
        : null,
    };
  }

  /** Historial de cobros de un cliente para el panel cobrador (solo lectura) */
  @Get('historial-cliente')
  @RequirePermission('COBRANZAS', 'COB_CBR_PANEL_COBRADOR_VER')
  @ApiOperation({ summary: 'Historial de recibos de un cliente (solo lectura para cobrador)' })
  @ApiQuery({ name: 'cliente_id', required: true, type: String })
  @ApiQuery({ name: 'page', required: false, type: Number })
  async getHistorialCliente(
    @GetUser() user: LoginUserInfo,
    @Query('cliente_id') clienteId: string,
    @Query('page') page = '1',
  ) {
    if (!clienteId) return { data: [], total: 0 };
    const pageNum = Math.max(1, parseInt(page));
    const size = 10;
    const skip = (pageNum - 1) * size;
    const fmtDate = (d: any) => d ? new Date(d).toISOString().split('T')[0] : null;

    const [recibos, total] = await Promise.all([
      this.prisma.recibos_cobro.findMany({
        where: { empresa_id: user.empresa_id, cliente_id: clienteId },
        select: {
          id: true,
          numero_recibo: true,
          fecha_emision: true,
          monto_total: true,
          mora_total: true,
          descuento_monto: true,
          estado: true,
          moneda: { select: { codigo: true, simbolo: true } },
          recibo_cobro_detalle: {
            select: {
              monto_pagado: true,
              mora_monto: true,
              descuento_aplicado: true,
              es_pago_parcial: true,
              factura_cuotas: {
                select: {
                  nro_cuota: true,
                  dmoncuota: true,
                  dvenccuo: true,
                  factura_cab: { select: { dpunexp: true, dnumdoc: true, dest: true } },
                },
              },
            },
          },
        },
        orderBy: { fecha_emision: 'desc' },
        skip,
        take: size,
      }),
      this.prisma.recibos_cobro.count({ where: { empresa_id: user.empresa_id, cliente_id: clienteId } }),
    ]);

    const data = recibos.map((r) => ({
      id: r.id,
      numero_recibo: r.numero_recibo,
      fecha_emision: fmtDate(r.fecha_emision),
      monto_total: Number(r.monto_total || 0),
      mora_total: Number(r.mora_total || 0),
      descuento_monto: Number(r.descuento_monto || 0),
      estado: r.estado,
      moneda: r.moneda,
      recibo_cobro_detalle: r.recibo_cobro_detalle.map((d) => ({
        monto_pagado: Number(d.monto_pagado || 0),
        mora_monto: Number(d.mora_monto || 0),
        descuento_aplicado: Number(d.descuento_aplicado || 0),
        es_pago_parcial: d.es_pago_parcial,
        factura_cuotas: d.factura_cuotas ? {
          nro_cuota: d.factura_cuotas.nro_cuota,
          dmoncuota: Number(d.factura_cuotas.dmoncuota || 0),
          dvenccuo: fmtDate(d.factura_cuotas.dvenccuo),
          factura_cab: d.factura_cuotas.factura_cab,
        } : null,
      })),
    }));

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

  /** Resumen rápido para la cabecera del panel */
  @Get('resumen')
  @RequirePermission('COBRANZAS', 'COB_CBR_PANEL_COBRADOR_VER')
  @ApiOperation({ summary: 'Resumen de cuotas del cobrador (totales por estado)' })
  @ApiQuery({ name: 'ver_todas', required: false, type: Boolean })
  async getResumen(
    @GetUser() user: LoginUserInfo,
    @Query('ver_todas') verTodas?: string,
  ) {
    const vendedorCobrador = await this.prisma.vendedores_cobradores.findFirst({
      where: { usuario_id: user.id, empresa_id: user.empresa_id, active: true },
    });

    if (!vendedorCobrador) return { cobrador: null, resumen: null };

    const hoy = new Date();
    hoy.setHours(0, 0, 0, 0);
    const proximos7 = new Date(hoy);
    proximos7.setDate(proximos7.getDate() + 7);
    const verTodasBool = verTodas === 'true';

    const estadosPagado = ['pagado', 'Pagado', 'cobrado', 'Cobrado'];
    const filtroAsignacion = verTodasBool
      ? { empresa_id: user.empresa_id }
      : {
          empresa_id: user.empresa_id,
          OR: [
            { cobrador_id: vendedorCobrador.id },
            {
              asignacion_facturas: {
                some: { cobrador_id: vendedorCobrador.id, estado: { not: 'cancelado' } },
              },
            },
          ],
        };
    const whereBase: any = {
      factura_cab: filtroAsignacion,
      estado: { notIn: estadosPagado },
    };

    const [vencidas, proximas, total] = await Promise.all([
      this.prisma.factura_cuotas.count({ where: { ...whereBase, dvenccuo: { lt: hoy } } }),
      this.prisma.factura_cuotas.count({ where: { ...whereBase, dvenccuo: { gte: hoy, lte: proximos7 } } }),
      this.prisma.factura_cuotas.count({ where: whereBase }),
    ]);

    return {
      cobrador: { id: vendedorCobrador.id, nombre: vendedorCobrador.nombre, apellido: vendedorCobrador.apellido },
      resumen: { vencidas, proximas_7_dias: proximas, total_pendientes: total },
    };
  }

  /**
   * Facturas pendientes de un cliente asignadas a un cobrador.
   * Usado por el admin al crear un descuento para seleccionar la cuenta a cobrar específica.
   */
  @Get('facturas-cliente')
  @RequirePermission('COBRANZAS', 'COB_CBR_PANEL_COBRADOR_VER')
  @ApiOperation({ summary: 'Facturas pendientes de un cliente. Si se pasa cobrador_id, filtra por sus asignaciones; si no, devuelve todas las del cliente.' })
  @ApiQuery({ name: 'cobrador_id', required: false, type: String })
  @ApiQuery({ name: 'cliente_id', required: true, type: String })
  async getFacturasCliente(
    @GetUser() user: LoginUserInfo,
    @Query('cliente_id') cliente_id: string,
    @Query('cobrador_id') cobrador_id?: string,
  ) {
    const cobradorFilter = cobrador_id
      ? {
          OR: [
            { cobrador_id },
            {
              asignacion_facturas: {
                some: { cobrador_id, estado: { not: 'cancelado' } },
              },
            },
          ],
        }
      : {};

    const facturas = await this.prisma.factura_cab.findMany({
      where: {
        empresa_id: user.empresa_id,
        cliente_id,
        condicion_operacion: { codigo: 2 }, // Crédito
        factura_cuotas: {
          some: { estado: { notIn: ['pagado', 'Pagado'] } },
        },
        ...cobradorFilter,
      },
      select: {
        id: true,
        dnumdoc: true,
        dest: true,
        dpunexp: true,
        total_factura: true,
        saldo_pendiente: true,
        dfeemide: true,
        dticam: true,
        moneda: { select: { codigo: true } },
        factura_cuotas: {
          select: { dmoncuota: true, saldo_pendiente: true, estado: true },
        },
      },
      orderBy: { dfeemide: 'desc' },
      take: 50,
    });

    // Fallback: si total_factura / saldo_pendiente no están persistidos en cabecera,
    // los calculamos desde las cuotas (suma de dmoncuota / saldos pendientes).
    const data = facturas.map((f) => {
      const totalCabecera = Number(f.total_factura ?? 0);
      const saldoCabecera = Number(f.saldo_pendiente ?? 0);
      const totalCuotas = f.factura_cuotas.reduce((acc, c) => acc + Number(c.dmoncuota ?? 0), 0);
      const saldoCuotas = f.factura_cuotas
        .filter((c) => !['pagado', 'Pagado'].includes(String(c.estado ?? '')))
        .reduce((acc, c) => acc + Number(c.saldo_pendiente ?? c.dmoncuota ?? 0), 0);
      return {
        id: f.id,
        dnumdoc: f.dnumdoc,
        dest: f.dest,
        dpunexp: f.dpunexp,
        dfeemide: f.dfeemide,
        total_factura: totalCabecera > 0 ? totalCabecera : totalCuotas,
        saldo_pendiente: saldoCabecera > 0 ? saldoCabecera : saldoCuotas,
      };
    });

    return { data };
  }

  /** Descuentos autorizados por el admin vigentes para las facturas del cobrador */
  @Get('descuentos-activos')
  @RequirePermission('COBRANZAS', 'COB_CBR_PANEL_COBRADOR_VER')
  @ApiOperation({ summary: 'Descuentos autorizados por admin vigentes para el cobrador' })
  async getDescuentosActivos(@GetUser() user: LoginUserInfo) {
    const vendedorCobrador = await this.prisma.vendedores_cobradores.findFirst({
      where: { usuario_id: user.id, empresa_id: user.empresa_id, active: true },
    });

    if (!vendedorCobrador) return { data: [] };

    const descuentos = await this.prisma.autorizaciones_descuento.findMany({
      where: {
        empresa_id: user.empresa_id,
        cobrador_id: vendedorCobrador.id,
        estado: { in: ['aprobado', 'aprobada'] },
        expires_at: { gt: new Date() },
      },
      include: {
        cliente: {
          select: {
            id: true,
            nombre_fantasia: true,
            personas: { select: { razon_social: true } },
          },
        },
        factura: { select: { id: true, dnumdoc: true, dest: true, dpunexp: true } },
      },
      orderBy: { created_at: 'desc' },
    });

    return { data: descuentos };
  }

  /**
   * Agenda del cobrador para HOY según política acordada por cuenta.
   * Combina:
   *  - Cuentas con dia_cobro_semana = día actual (semanales).
   *  - Cuentas con dia_fijo_pago_1 o dia_fijo_pago_2 = día del mes actual (mensuales/quincenales).
   *  - Cuotas venciendo entre hoy y hoy + dias_gracia_efectivo (fallback / cobertura de vencimientos inminentes).
   *
   * Ver FASE 3.8 en docs/plan-solicitud_credito.md.
   */
  @Get('agenda-hoy')
  @RequirePermission('COBRANZAS', 'COB_CBR_PANEL_COBRADOR_VER')
  @ApiOperation({ summary: 'Agenda del día del cobrador (basada en política acordada por cuenta)' })
  @ApiQuery({ name: 'cobrador_id', required: false, type: String, description: 'UUID del cobrador. Si se omite, usa el vinculado al usuario logueado.' })
  @ApiQuery({ name: 'ver_todos', required: false, type: Boolean, description: 'true = ver agenda de toda la empresa' })
  async getAgendaHoy(
    @GetUser() user: LoginUserInfo,
    @Query('cobrador_id') cobradorIdParam?: string,
    @Query('ver_todos') verTodos?: string,
  ) {
    const verTodosBool = verTodos === 'true';

    let cobradorId: string | null = cobradorIdParam ?? null;
    if (!cobradorId && !verTodosBool) {
      const vc = await this.prisma.vendedores_cobradores.findFirst({
        where: { usuario_id: user.id, empresa_id: user.empresa_id, active: true },
        select: { id: true },
      });
      cobradorId = vc?.id ?? null;
    }

    // Consulta la vista v_agenda_cobrador filtrando por:
    //  - empresa
    //  - cobrador (si aplica)
    //  - hoy matchea con día de la política O vencimiento próximo (dentro de gracia)
    const rows = await this.prisma.$queryRawUnsafe<
      Array<{
        cuenta_id: string;
        empresa_id: string;
        cliente_id: string;
        cobrador_id: string | null;
        cobrador_origen: 'FACTURA' | 'DIRECCION' | 'CLIENTE' | null;
        factura_cab_id: string;
        factura_ref: string;
        cuota_id: string;
        nro_cuota: number;
        fecha_vencimiento: Date;
        saldo_cuota: string;
        saldo_cuenta: string;
        dias_vencido: number;
        dias_gracia_efectivo: number;
        dia_fijo_pago_1: number | null;
        dia_fijo_pago_2: number | null;
        dia_cobro_semana: number | null;
        intervalo_dias: number | null;
        moneda_iso: string | null;
        cliente_razon_social: string | null;
        cliente_ruc: string | null;
        cliente_documento: string | null;
        cliente_telefono: string | null;
        cliente_celular: string | null;
        cliente_direccion: string | null;
        cliente_direccion_id: string | null;
        cliente_direccion_label: string | null;
        cliente_zona: string | null;
      }>
    >(
      `
      SELECT *
      FROM v_agenda_cobrador
      WHERE empresa_id = $1::uuid
        ${cobradorId ? 'AND cobrador_id = $2::uuid' : ''}
        AND (
          -- Semanal: día de la semana coincide
          dia_cobro_semana = (
            CASE EXTRACT(DOW FROM CURRENT_DATE)::int
              WHEN 0 THEN 7  -- Domingo
              ELSE EXTRACT(DOW FROM CURRENT_DATE)::int
            END
          )
          -- Mensual/quincenal: día del mes coincide
          OR dia_fijo_pago_1 = EXTRACT(DAY FROM CURRENT_DATE)::int
          OR dia_fijo_pago_2 = EXTRACT(DAY FROM CURRENT_DATE)::int
          -- Vencimiento dentro del período de gracia (cubre casos sin política y últimos avisos)
          OR (fecha_vencimiento::date - CURRENT_DATE) BETWEEN -dias_gracia_efectivo AND 3
        )
      ORDER BY dias_vencido DESC, fecha_vencimiento ASC
      LIMIT 500
      `,
      user.empresa_id,
      ...(cobradorId ? [cobradorId] : []),
    );

    const items = rows.map((r) => ({
      cuenta_id: r.cuenta_id,
      cliente: {
        id: r.cliente_id,
        razon_social: r.cliente_razon_social,
        ruc: r.cliente_ruc,
        documento: r.cliente_documento,
        telefono: r.cliente_telefono,
        celular: r.cliente_celular,
        direccion: r.cliente_direccion,
        direccion_id: r.cliente_direccion_id,
        direccion_label: r.cliente_direccion_label,
        zona: r.cliente_zona,
      },
      cobrador_id: r.cobrador_id,
      cobrador_origen: r.cobrador_origen,
      factura: { id: r.factura_cab_id, ref: r.factura_ref },
      cuota: {
        id: r.cuota_id,
        nro: r.nro_cuota,
        fecha_vencimiento: r.fecha_vencimiento,
        saldo: Number(r.saldo_cuota),
      },
      saldo_cuenta: Number(r.saldo_cuenta),
      moneda: r.moneda_iso,
      dias_vencido: r.dias_vencido,
      dias_gracia: r.dias_gracia_efectivo,
      politica: {
        intervalo_dias: r.intervalo_dias,
        dia_fijo_pago_1: r.dia_fijo_pago_1,
        dia_fijo_pago_2: r.dia_fijo_pago_2,
        dia_cobro_semana: r.dia_cobro_semana,
      },
      // Motivo de aparición en la agenda de hoy
      motivo: (() => {
        const hoyMes = new Date().getDate();
        const jsDow = new Date().getDay();
        const hoyDow = jsDow === 0 ? 7 : jsDow;
        if (r.dia_fijo_pago_1 === hoyMes) return 'DIA_FIJO_1';
        if (r.dia_fijo_pago_2 === hoyMes) return 'DIA_FIJO_2';
        if (r.dia_cobro_semana === hoyDow) return 'DIA_SEMANAL';
        if (r.dias_vencido > 0) return 'VENCIDA';
        return 'PROXIMA';
      })(),
    }));

    return {
      fecha: new Date().toISOString().slice(0, 10),
      total: items.length,
      items,
    };
  }

  /**
   * Distribución de la cartera por día de cobro (según política acordada por cuenta).
   * Sirve para dimensionar carga de trabajo del equipo de cobranzas por día del mes /
   * día de la semana, detectar clusters, planear rutas, etc.
   *
   * Ver FASE 3.12 en docs/plan-solicitud_credito.md.
   */
  @Get('cartera-por-dia-cobro')
  @RequirePermission('COBRANZAS', 'COB_CBR_PANEL_COBRADOR_VER')
  @ApiOperation({ summary: 'Distribución de la cartera vigente por día de cobro acordado' })
  @ApiQuery({ name: 'cobrador_id', required: false, type: String })
  async carteraPorDiaCobro(
    @GetUser() user: LoginUserInfo,
    @Query('cobrador_id') cobradorIdParam?: string,
  ) {
    // Distribución mensual: por día 1..31 (uniendo dia_fijo_pago_1 y _2)
    const porDiaMes = await this.prisma.$queryRawUnsafe<
      Array<{ dia: number; clientes: bigint; cuentas: bigint; saldo: string }>
    >(
      `
      WITH agenda AS (
        SELECT DISTINCT ON (cuenta_id, dia)
          cuenta_id, cliente_id, saldo_cuenta, dia
        FROM (
          SELECT cuenta_id, cliente_id, saldo_cuenta, dia_fijo_pago_1 AS dia
          FROM v_agenda_cobrador
          WHERE empresa_id = $1::uuid
            AND dia_fijo_pago_1 IS NOT NULL
            ${cobradorIdParam ? 'AND cobrador_id = $2::uuid' : ''}
          UNION ALL
          SELECT cuenta_id, cliente_id, saldo_cuenta, dia_fijo_pago_2 AS dia
          FROM v_agenda_cobrador
          WHERE empresa_id = $1::uuid
            AND dia_fijo_pago_2 IS NOT NULL
            ${cobradorIdParam ? 'AND cobrador_id = $2::uuid' : ''}
        ) x
      )
      SELECT
        dia::int,
        COUNT(DISTINCT cliente_id)::bigint AS clientes,
        COUNT(DISTINCT cuenta_id)::bigint  AS cuentas,
        COALESCE(SUM(saldo_cuenta), 0)::text AS saldo
      FROM agenda
      GROUP BY dia
      ORDER BY dia
      `,
      user.empresa_id,
      ...(cobradorIdParam ? [cobradorIdParam] : []),
    );

    // Distribución semanal (1=Lunes..7=Domingo)
    const porDiaSemana = await this.prisma.$queryRawUnsafe<
      Array<{ dia: number; clientes: bigint; cuentas: bigint; saldo: string }>
    >(
      `
      SELECT
        dia_cobro_semana::int AS dia,
        COUNT(DISTINCT cliente_id)::bigint AS clientes,
        COUNT(DISTINCT cuenta_id)::bigint  AS cuentas,
        COALESCE(SUM(saldo_cuenta), 0)::text AS saldo
      FROM v_agenda_cobrador
      WHERE empresa_id = $1::uuid
        AND dia_cobro_semana IS NOT NULL
        ${cobradorIdParam ? 'AND cobrador_id = $2::uuid' : ''}
      GROUP BY dia_cobro_semana
      ORDER BY dia_cobro_semana
      `,
      user.empresa_id,
      ...(cobradorIdParam ? [cobradorIdParam] : []),
    );

    // Cuentas sin política definida (fallback a intervalo o cuotas sueltas)
    const sinPolitica = await this.prisma.$queryRawUnsafe<
      Array<{ clientes: bigint; cuentas: bigint; saldo: string }>
    >(
      `
      SELECT
        COUNT(DISTINCT cliente_id)::bigint AS clientes,
        COUNT(DISTINCT cuenta_id)::bigint  AS cuentas,
        COALESCE(SUM(saldo_cuenta), 0)::text AS saldo
      FROM v_agenda_cobrador
      WHERE empresa_id = $1::uuid
        AND dia_fijo_pago_1 IS NULL
        AND dia_fijo_pago_2 IS NULL
        AND dia_cobro_semana IS NULL
        ${cobradorIdParam ? 'AND cobrador_id = $2::uuid' : ''}
      `,
      user.empresa_id,
      ...(cobradorIdParam ? [cobradorIdParam] : []),
    );

    const diasSemanaLabel = ['', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];

    return {
      por_dia_mes: porDiaMes.map((r) => ({
        dia: r.dia,
        clientes: Number(r.clientes),
        cuentas: Number(r.cuentas),
        saldo: Number(r.saldo),
      })),
      por_dia_semana: porDiaSemana.map((r) => ({
        dia: r.dia,
        label: diasSemanaLabel[r.dia] ?? String(r.dia),
        clientes: Number(r.clientes),
        cuentas: Number(r.cuentas),
        saldo: Number(r.saldo),
      })),
      sin_politica: sinPolitica[0]
        ? {
            clientes: Number(sinPolitica[0].clientes),
            cuentas: Number(sinPolitica[0].cuentas),
            saldo: Number(sinPolitica[0].saldo),
          }
        : { clientes: 0, cuentas: 0, saldo: 0 },
    };
  }
}
