import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { QueryMesaGestionDto } from './dto/mesa-gestion.dto';

type WorklistRow = {
  cliente_id: string;
  nombre_fantasia: string | null;
  zona: string | null;
  cobrador_id: string | null;
  saldo_pendiente: Prisma.Decimal | null;
  razon_social: string | null;
  ruc: string | null;
  nro_documento: string | null;
  telefono: string | null;
  celular: string | null;
  email: string | null;
  direccion: string | null;
  oldest_overdue: Date | null;
  saldo_vencido: Prisma.Decimal | null;
  facturas_vencidas: bigint;
  ultima_gestion_at: Date | null;
  promesas_incumplidas: bigint;
  promesas_vigentes: bigint;
  gestionado_por_mi: boolean;
  dias_max_vencido: number | null;
  dias_sin_gestion: number | null;
};

const ORDENES: Record<string, Prisma.Sql> = {
  prioridad: Prisma.sql`ORDER BY (CASE WHEN ug.ultima_gestion_at IS NULL THEN 0 ELSE 1 END) ASC, v.oldest_overdue ASC, v.saldo_vencido DESC`,
  vencimiento: Prisma.sql`ORDER BY v.oldest_overdue ASC, v.saldo_vencido DESC`,
  saldo: Prisma.sql`ORDER BY v.saldo_vencido DESC, v.oldest_overdue ASC`,
  sin_gestion: Prisma.sql`ORDER BY ug.ultima_gestion_at ASC NULLS FIRST, v.oldest_overdue ASC`,
};

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

  constructor(private readonly prisma: PrismaService) {}

  async getWorklist(empresa_id: string, query: QueryMesaGestionDto, user_id?: string) {
    const page = query.page && query.page > 0 ? query.page : 1;
    const limit = query.limit && query.limit > 0 ? query.limit : 50;
    const offset = (page - 1) * limit;
    const orden = query.orden && ORDENES[query.orden] ? query.orden : 'prioridad';

    const filtros: Prisma.Sql[] = [];
    if (query.zona) {
      filtros.push(Prisma.sql`c.zona ILIKE ${'%' + query.zona + '%'}`);
    }
    if (query.cobrador_id) {
      filtros.push(Prisma.sql`c.cobrador_id = ${query.cobrador_id}::uuid`);
    }
    if (query.min_dias_vencido !== undefined && query.min_dias_vencido !== null) {
      filtros.push(
        Prisma.sql`EXTRACT(DAY FROM (NOW() - v.oldest_overdue))::int >= ${Number(query.min_dias_vencido)}`,
      );
    }
    if (query.sin_gestion_dias !== undefined && query.sin_gestion_dias !== null) {
      filtros.push(
        Prisma.sql`(ug.ultima_gestion_at IS NULL OR EXTRACT(DAY FROM (NOW() - ug.ultima_gestion_at))::int >= ${Number(query.sin_gestion_dias)})`,
      );
    }
    if (query.solo_promesas_vencidas === 'true') {
      filtros.push(Prisma.sql`COALESCE(pv.promesas_incumplidas, 0) > 0`);
    }
    if (query.estado_gestion && query.estado_gestion !== 'todos') {
      if (query.estado_gestion === 'por_contactar') {
        filtros.push(
          Prisma.sql`(ug.ultima_gestion_at IS NULL OR EXTRACT(DAY FROM (NOW() - ug.ultima_gestion_at))::int >= 7)`,
        );
      } else if (query.estado_gestion === 'en_gestion') {
        filtros.push(
          Prisma.sql`ug.ultima_gestion_at IS NOT NULL AND EXTRACT(DAY FROM (NOW() - ug.ultima_gestion_at))::int < 7`,
        );
      } else if (query.estado_gestion === 'con_promesa_vigente') {
        filtros.push(Prisma.sql`COALESCE(pvg.promesas_vigentes, 0) > 0`);
      }
    }
    if (query.solo_mis_gestiones === 'true' && user_id) {
      filtros.push(Prisma.sql`mg.cliente_id IS NOT NULL`);
    }
    if (query.solo_agenda_hoy === 'true') {
      // Cliente tiene AL MENOS una cuenta cuya política de cobro coincide con hoy
      // (día fijo del mes / día semanal), o una cuota que vence dentro del período de gracia.
      filtros.push(Prisma.sql`EXISTS (
        SELECT 1
        FROM v_agenda_cobrador va
        WHERE va.cliente_id = c.id
          AND va.empresa_id = ${empresa_id}::uuid
          AND (
            va.dia_cobro_semana = (
              CASE EXTRACT(DOW FROM CURRENT_DATE)::int
                WHEN 0 THEN 7
                ELSE EXTRACT(DOW FROM CURRENT_DATE)::int
              END
            )
            OR va.dia_fijo_pago_1 = EXTRACT(DAY FROM CURRENT_DATE)::int
            OR va.dia_fijo_pago_2 = EXTRACT(DAY FROM CURRENT_DATE)::int
            OR (va.fecha_vencimiento::date - CURRENT_DATE) BETWEEN -va.dias_gracia_efectivo AND 3
          )
      )`);
    }
    if (query.search) {
      const s = `%${query.search.trim()}%`;
      filtros.push(
        Prisma.sql`(c.nombre_fantasia ILIKE ${s} OR p.razon_social ILIKE ${s} OR p.ruc ILIKE ${s} OR p.nro_documento ILIKE ${s})`,
      );
    }

    const whereExtra = filtros.length
      ? Prisma.sql`AND ${Prisma.join(filtros, ' AND ')}`
      : Prisma.empty;

    const orderBy = ORDENES[orden];

    const baseCte = Prisma.sql`
      WITH vencidas AS (
        SELECT
          fc.cliente_id,
          MIN(fcu.dvenccuo) AS oldest_overdue,
          SUM(fcu.saldo_pendiente) AS saldo_vencido,
          COUNT(DISTINCT fc.id) AS facturas_vencidas
        FROM factura_cab fc
        JOIN factura_cuotas fcu ON fcu.factura_cab_id = fc.id
        WHERE fc.empresa_id = ${empresa_id}::uuid
          AND (fc.estado IS NULL OR fc.estado <> 'Anulado')
          AND fcu.estado = 'pendiente'
          AND fcu.saldo_pendiente > 0
          AND fcu.dvenccuo < NOW()
        GROUP BY fc.cliente_id
      ),
      ultima_g AS (
        SELECT cliente_id, MAX(fecha) AS ultima_gestion_at
        FROM cob_gestion
        WHERE empresa_id = ${empresa_id}::uuid AND anulada = false
        GROUP BY cliente_id
      ),
      prom_venc AS (
        SELECT cliente_id, COUNT(*)::bigint AS promesas_incumplidas
        FROM promesas_pago
        WHERE empresa_id = ${empresa_id}::uuid
          AND estado = 'pendiente'
          AND fecha_prometida < CURRENT_DATE
        GROUP BY cliente_id
      ),
      prom_vig AS (
        SELECT cliente_id, COUNT(*)::bigint AS promesas_vigentes
        FROM promesas_pago
        WHERE empresa_id = ${empresa_id}::uuid
          AND estado = 'pendiente'
          AND fecha_prometida >= CURRENT_DATE
        GROUP BY cliente_id
      ),
      mis_g AS (
        SELECT DISTINCT cliente_id
        FROM cob_gestion
        WHERE empresa_id = ${empresa_id}::uuid
          AND anulada = false
          AND usuario_id = ${user_id ?? '00000000-0000-0000-0000-000000000000'}::uuid
      )
    `;

    const fromAndWhere = Prisma.sql`
      FROM vencidas v
      JOIN clientes c ON c.id = v.cliente_id AND c.active = true AND c.deleted = false
      JOIN personas p ON p.id = c.persona_id
      LEFT JOIN ultima_g ug ON ug.cliente_id = c.id
      LEFT JOIN prom_venc pv ON pv.cliente_id = c.id
      LEFT JOIN prom_vig pvg ON pvg.cliente_id = c.id
      LEFT JOIN mis_g mg ON mg.cliente_id = c.id
      WHERE 1=1 ${whereExtra}
    `;

    const rowsSql = Prisma.sql`
      ${baseCte}
      SELECT
        c.id AS cliente_id,
        c.nombre_fantasia,
        c.zona,
        c.cobrador_id,
        c.saldo_pendiente,
        p.razon_social,
        p.ruc,
        p.nro_documento,
        p.telefono,
        p.celular,
        p.email,
        p.direccion,
        v.oldest_overdue,
        v.saldo_vencido,
        v.facturas_vencidas,
        ug.ultima_gestion_at,
        COALESCE(pv.promesas_incumplidas, 0) AS promesas_incumplidas,
        COALESCE(pvg.promesas_vigentes, 0) AS promesas_vigentes,
        (mg.cliente_id IS NOT NULL) AS gestionado_por_mi,
        EXTRACT(DAY FROM (NOW() - v.oldest_overdue))::int AS dias_max_vencido,
        CASE WHEN ug.ultima_gestion_at IS NULL THEN NULL
             ELSE EXTRACT(DAY FROM (NOW() - ug.ultima_gestion_at))::int END AS dias_sin_gestion
      ${fromAndWhere}
      ${orderBy}
      LIMIT ${limit} OFFSET ${offset}
    `;

    const countSql = Prisma.sql`
      ${baseCte}
      SELECT COUNT(*)::bigint AS total
      ${fromAndWhere}
    `;

    const [rows, totalRows] = await Promise.all([
      this.prisma.$queryRaw<WorklistRow[]>(rowsSql),
      this.prisma.$queryRaw<{ total: bigint }[]>(countSql),
    ]);

    const data = rows.map((r) => ({
      cliente_id: r.cliente_id,
      nombre_fantasia: r.nombre_fantasia,
      razon_social: r.razon_social,
      ruc: r.ruc,
      nro_documento: r.nro_documento,
      telefono: r.telefono,
      celular: r.celular,
      email: r.email,
      direccion: r.direccion,
      zona: r.zona,
      cobrador_id: r.cobrador_id,
      saldo_pendiente: Number(r.saldo_pendiente ?? 0),
      saldo_vencido: Number(r.saldo_vencido ?? 0),
      facturas_vencidas: Number(r.facturas_vencidas ?? 0),
      oldest_overdue: r.oldest_overdue,
      dias_max_vencido: r.dias_max_vencido ?? 0,
      ultima_gestion_at: r.ultima_gestion_at,
      dias_sin_gestion: r.dias_sin_gestion,
      promesas_incumplidas: Number(r.promesas_incumplidas ?? 0),
      promesas_vigentes: Number(r.promesas_vigentes ?? 0),
      gestionado_por_mi: !!r.gestionado_por_mi,
      prioridad: this.calcularPrioridad({
        dias_max_vencido: r.dias_max_vencido ?? 0,
        saldo_vencido: Number(r.saldo_vencido ?? 0),
        dias_sin_gestion: r.dias_sin_gestion,
        promesas_incumplidas: Number(r.promesas_incumplidas ?? 0),
      }),
    }));

    return {
      data,
      total: Number(totalRows?.[0]?.total ?? 0),
      page,
      limit,
      totalPages: Math.ceil(Number(totalRows?.[0]?.total ?? 0) / limit),
    };
  }

  /**
   * Variante de getWorklist sin paginar — pensada para export Excel.
   * Reutiliza la misma lógica pero usa limit muy alto. Hace cap en 20.000
   * filas para evitar timeouts; con esos volúmenes el gestor debería usar
   * filtros más estrictos.
   */
  async getWorklistFull(empresa_id: string, query: QueryMesaGestionDto, user_id?: string) {
    const safeQuery: QueryMesaGestionDto = { ...query, page: 1, limit: 20000 };
    const base = await this.getWorklist(empresa_id, safeQuery, user_id);
    if (!base.data.length) return { ...base, cuotas: [] };

    // Detalle de cuotas vencidas por cliente — una sola query batch.
    const clienteIds = base.data.map((r) => r.cliente_id);
    const cuotas = await this.prisma.factura_cuotas.findMany({
      where: {
        estado: 'pendiente',
        saldo_pendiente: { gt: 0 },
        dvenccuo: { lt: new Date() },
        factura_cab: {
          empresa_id,
          cliente_id: { in: clienteIds },
          OR: [{ estado: null }, { estado: { not: 'Anulado' } }],
        },
      },
      select: {
        id: true,
        nro_cuota: true,
        dvenccuo: true,
        dmoncuota: true,
        saldo_pendiente: true,
        factura_cab: {
          select: {
            id: true,
            cliente_id: true,
            dest: true,
            dpunexp: true,
            dnumdoc: true,
            dfeemide: true,
            total_factura: true,
            moneda: { select: { codigo: true } },
            clientes: {
              select: {
                nombre_fantasia: true,
                cobrador: { select: { nombre: true, apellido: true } },
                personas: { select: { razon_social: true, ruc: true, nro_documento: true } },
              },
            },
          },
        },
      },
      orderBy: [{ factura_cab: { cliente_id: 'asc' } }, { dvenccuo: 'asc' }],
    });

    // Items por factura (batch) — "2x Producto A; 1x Producto B"
    const facturaIds = Array.from(new Set(cuotas.map((c) => c.factura_cab!.id)));
    const detalles = facturaIds.length
      ? await this.prisma.factura_det.findMany({
          where: { factura_cab_id: { in: facturaIds } },
          select: { factura_cab_id: true, ddesproser: true, dcantproser: true },
        })
      : [];
    const itemsPorFactura = new Map<string, string>();
    const fmtCant = (n: any) => {
      const num = Number(n ?? 0);
      return Number.isInteger(num) ? String(num) : num.toFixed(2).replace(/\.?0+$/, '');
    };
    for (const d of detalles) {
      const linea = `${fmtCant(d.dcantproser)}x ${d.ddesproser ?? ''}`.trim();
      const prev = itemsPorFactura.get(d.factura_cab_id);
      itemsPorFactura.set(d.factura_cab_id, prev ? `${prev}; ${linea}` : linea);
    }

    const hoy = Date.now();
    const cuotasFlat = cuotas.map((c) => {
      const fc = c.factura_cab!;
      const cli = fc.clientes!;
      const personas = cli.personas;
      const dias = c.dvenccuo
        ? Math.floor((hoy - new Date(c.dvenccuo).getTime()) / 86400000)
        : 0;
      return {
        cliente_id: fc.cliente_id,
        cliente: personas?.razon_social || cli.nombre_fantasia || '',
        ruc: personas?.ruc || personas?.nro_documento || '',
        cobrador: cli.cobrador
          ? `${cli.cobrador.nombre ?? ''} ${cli.cobrador.apellido ?? ''}`.trim()
          : '',
        factura: `${String(fc.dest).padStart(3, '0')}-${String(fc.dpunexp).padStart(3, '0')}-${String(fc.dnumdoc).padStart(7, '0')}`,
        fecha_emision: fc.dfeemide,
        moneda: fc.moneda?.codigo ?? 'PYG',
        total_factura: Number(fc.total_factura ?? 0),
        items: itemsPorFactura.get(fc.id) ?? '',
        nro_cuota: c.nro_cuota,
        vencimiento: c.dvenccuo,
        monto_cuota: Number(c.dmoncuota ?? 0),
        saldo_pendiente: Number(c.saldo_pendiente ?? 0),
        dias_vencido: dias,
      };
    });

    return { ...base, cuotas: cuotasFlat };
  }

  async getResumen(empresa_id: string) {
    const baseCte = Prisma.sql`
      WITH vencidas AS (
        SELECT
          fc.cliente_id,
          MIN(fcu.dvenccuo) AS oldest_overdue,
          SUM(fcu.saldo_pendiente) AS saldo_vencido
        FROM factura_cab fc
        JOIN factura_cuotas fcu ON fcu.factura_cab_id = fc.id
        WHERE fc.empresa_id = ${empresa_id}::uuid
          AND (fc.estado IS NULL OR fc.estado <> 'Anulado')
          AND fcu.estado = 'pendiente'
          AND fcu.saldo_pendiente > 0
          AND fcu.dvenccuo < NOW()
        GROUP BY fc.cliente_id
      ),
      ultima_g AS (
        SELECT cliente_id, MAX(fecha) AS ultima_gestion_at
        FROM cob_gestion
        WHERE empresa_id = ${empresa_id}::uuid AND anulada = false
        GROUP BY cliente_id
      )
    `;

    const rs = await this.prisma.$queryRaw<
      {
        total_clientes: bigint;
        saldo_total: Prisma.Decimal | null;
        sin_gestion: bigint;
        mas_30: bigint;
        mas_60: bigint;
        mas_90: bigint;
      }[]
    >(Prisma.sql`
      ${baseCte}
      SELECT
        COUNT(*)::bigint AS total_clientes,
        COALESCE(SUM(v.saldo_vencido), 0) AS saldo_total,
        SUM(CASE WHEN ug.ultima_gestion_at IS NULL THEN 1 ELSE 0 END)::bigint AS sin_gestion,
        SUM(CASE WHEN EXTRACT(DAY FROM (NOW() - v.oldest_overdue))::int > 30 THEN 1 ELSE 0 END)::bigint AS mas_30,
        SUM(CASE WHEN EXTRACT(DAY FROM (NOW() - v.oldest_overdue))::int > 60 THEN 1 ELSE 0 END)::bigint AS mas_60,
        SUM(CASE WHEN EXTRACT(DAY FROM (NOW() - v.oldest_overdue))::int > 90 THEN 1 ELSE 0 END)::bigint AS mas_90
      FROM vencidas v
      JOIN clientes c ON c.id = v.cliente_id AND c.active = true AND c.deleted = false
      LEFT JOIN ultima_g ug ON ug.cliente_id = c.id
    `);

    const r = rs?.[0];
    return {
      total_clientes: Number(r?.total_clientes ?? 0),
      saldo_total: Number(r?.saldo_total ?? 0),
      sin_gestion: Number(r?.sin_gestion ?? 0),
      mas_30: Number(r?.mas_30 ?? 0),
      mas_60: Number(r?.mas_60 ?? 0),
      mas_90: Number(r?.mas_90 ?? 0),
    };
  }

  /**
   * Score 0-100 que mezcla mora, saldo y gap de gestión. Sirve como guía visual.
   * Pesos: días vencido 50%, sin gestión 25%, promesas incumplidas 15%, saldo 10%.
   */
  private calcularPrioridad(args: {
    dias_max_vencido: number;
    saldo_vencido: number;
    dias_sin_gestion: number | null;
    promesas_incumplidas: number;
  }) {
    const vencido = Math.min(args.dias_max_vencido / 90, 1) * 50;
    const sinGestion =
      args.dias_sin_gestion === null ? 25 : Math.min(args.dias_sin_gestion / 30, 1) * 25;
    const promesas = Math.min(args.promesas_incumplidas, 3) * 5;
    const saldoNorm = Math.min(args.saldo_vencido / 10_000_000, 1) * 10;
    return Math.round(vencido + sinGestion + promesas + saldoNorm);
  }
}
