'use strict';
const logger = require('../utils/logger');
const { db } = require('../utils/dbName');

const { getPool, sql } = require('../db');
const { localDateStr, toLocalISOStr } = require('../utils/localDate');
const operationalCutService = require('./operationalCutService');

const REPORT_SALES_COLUMNS = [
  'folio', 'numcheque', 'fecha', 'cierre', 'mesa', 'nopersonas', 'idmesero',
  'pagado', 'cancelado', 'idcliente', 'idarearestaurant', 'idturno', 'total',
  'totaldescuentos', 'totalcortesias', 'totalalimentos', 'totalbebidas', 'totalotros',
  'propina', 'subtotal', 'subtotalsinimpuestos', 'totalimpuesto1', 'totalconpropina',
  'cargo', 'totalconcargo', 'propinatarjeta', 'descuentomonedero', 'totalarticulos',
  'impresiones', 'reabiertas', 'razoncancelado', 'usuariocancelo', 'fechacancelado',
  'efectivo', 'tarjeta', 'vales', 'otros',
];

function reportSalesUnion(alias = 'c') {
  const finalCols = REPORT_SALES_COLUMNS.map(column => `ch.${column}`).join(', ');
  const tempCols = REPORT_SALES_COLUMNS.map(column => `tc.${column}`).join(', ');
  return `(
    SELECT ${finalCols}
    FROM ${db()}cheques ch
    UNION ALL
    SELECT ${tempCols}
    FROM ${db()}tempcheques tc
    WHERE ${operationalCutService.tempDedupePredicate('tc')}
  ) ${alias}`;
}

function reportPaymentsUnion(alias = 'cp') {
  return `(
    SELECT cp.folio, cp.idformadepago, fp.descripcion AS forma_pago, fp.tipo,
      cp.importe, cp.propina, cp.referencia, cp.cardBrand,
      ch.fecha, ch.cancelado, ch.pagado, ch.total
    FROM ${db()}chequespagos cp
    INNER JOIN ${db()}cheques ch ON ch.folio = cp.folio
    LEFT JOIN ${db()}formasdepago fp ON fp.idformadepago = cp.idformadepago
    UNION ALL
    SELECT tcp.folio, tcp.idformadepago, fp.descripcion AS forma_pago, fp.tipo,
      tcp.importe, tcp.propina, tcp.referencia, tcp.cardBrand,
      tc.fecha, tc.cancelado, tc.pagado, tc.total
    FROM ${db()}tempchequespagos tcp
    INNER JOIN ${db()}tempcheques tc ON tc.folio = tcp.folio
    LEFT JOIN ${db()}formasdepago fp ON fp.idformadepago = tcp.idformadepago
    WHERE ${operationalCutService.tempDedupePredicate('tc')}
  ) ${alias}`;
}

function reportDetailsUnion(alias = 'cd') {
  return `(
    SELECT d.foliodet, d.cantidad, d.idproducto, d.precio, d.modificador,
      ch.fecha, ch.cancelado, ch.pagado, ch.total
    FROM ${db()}cheqdet d
    INNER JOIN ${db()}cheques ch ON ch.folio = d.foliodet
    UNION ALL
    SELECT d.foliodet, d.cantidad, d.idproducto, d.precio, d.modificador,
      tc.fecha, tc.cancelado, tc.pagado, tc.total
    FROM ${db()}tempcheqdet d
    INNER JOIN ${db()}tempcheques tc ON tc.folio = d.foliodet
    WHERE ${operationalCutService.tempDedupePredicate('tc')}
  ) ${alias}`;
}

function safeDate(value) {
  return value instanceof Date ? toLocalISOStr(value) : value;
}

async function getReporteData(inicio, fin) {
  const pool = await getPool();

  const dias = Math.round((new Date(fin) - new Date(inicio)) / 86400000);
  const inicioAnterior = localDateStr(new Date(new Date(inicio).getTime() - (dias + 1) * 86400000));
  const finAnterior    = localDateStr(new Date(new Date(inicio).getTime() - 86400000));

  const mkReq = () => {
    const r = pool.request();
    // Enviar YYYY-MM-DD como texto evita que tedious desplace la fecha un día
    // al convertir sql.Date entre la zona local de Windows y SQL Server.
    r.input('ini', sql.VarChar(10), inicio);
    r.input('fin', sql.VarChar(10), fin);
    return r;
  };
  const mkReqAnt = () => {
    const r = pool.request();
    r.input('ini_ant', sql.VarChar(10), inicioAnterior);
    r.input('fin_ant', sql.VarChar(10), finAnterior);
    return r;
  };

  // ── Totales principales (con fallback si columnas opcionales no existen) ──
  let totRec = { tickets:0, venta_total:0, descuentos:0, cortesias:0, alimentos:0, bebidas:0, otros:0 };
  try {
    const r = await mkReq().query(`
      SELECT COUNT(*) AS tickets,
        ROUND(ISNULL(SUM(total),0),2)           AS venta_total,
        ROUND(ISNULL(SUM(totaldescuentos),0),2) AS descuentos,
        ROUND(ISNULL(SUM(totalcortesias),0),2)  AS cortesias,
        ROUND(ISNULL(SUM(totalalimentos),0),2)  AS alimentos,
        ROUND(ISNULL(SUM(totalbebidas),0),2)    AS bebidas,
        ROUND(ISNULL(SUM(totalotros),0),2)      AS otros
      FROM ${reportSalesUnion('c')}
      WHERE CAST(fecha AS DATE) BETWEEN @ini AND @fin AND cancelado=0 AND pagado=1 AND total>0
    `);
    if (r.recordset[0]) totRec = r.recordset[0];
  } catch(_) {
    // Fallback: solo columnas garantizadas en todas las versiones SR
    try {
      const r2 = await mkReq().query(`
        SELECT COUNT(*) AS tickets, ROUND(ISNULL(SUM(total),0),2) AS venta_total,
          0 AS descuentos, 0 AS cortesias, 0 AS alimentos, 0 AS bebidas, 0 AS otros
        FROM ${reportSalesUnion('c')}
        WHERE CAST(fecha AS DATE) BETWEEN @ini AND @fin AND cancelado=0 AND pagado=1 AND total>0
      `);
      if (r2.recordset[0]) totRec = r2.recordset[0];
    } catch(_2) {}
  }

  // ── Periodo anterior ──
  let anteriorRec = { tickets:0, venta_total:0 };
  try {
    const r = await mkReqAnt().query(`
      SELECT COUNT(*) AS tickets, ROUND(ISNULL(SUM(total),0),2) AS venta_total
      FROM ${reportSalesUnion('c')}
      WHERE CAST(fecha AS DATE) BETWEEN @ini_ant AND @fin_ant AND cancelado=0 AND pagado=1 AND total>0
    `);
    if (r.recordset[0]) anteriorRec = r.recordset[0];
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  // ── Ventas por dia ──
  let ventasPorDia = [];
  try {
    const r = await mkReq().query(`
      SELECT CAST(fecha AS DATE) AS fecha, COUNT(*) AS tickets, ROUND(SUM(total),2) AS venta
      FROM ${reportSalesUnion('c')}
      WHERE CAST(fecha AS DATE) BETWEEN @ini AND @fin AND cancelado=0 AND pagado=1 AND total>0
      GROUP BY CAST(fecha AS DATE) ORDER BY fecha
    `);
    ventasPorDia = r.recordset.map(r => ({
      ...r, fecha: r.fecha instanceof Date ? localDateStr(r.fecha) : r.fecha,
    }));
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  const ventasPorTipo = [
    { tipo: 'Alimentos', total: totRec.alimentos || 0 },
    { tipo: 'Bebidas',   total: totRec.bebidas   || 0 },
    { tipo: 'Otros',     total: totRec.otros      || 0 },
  ].filter(x => x.total > 0);

  // ── Formas de pago ──
  let formasPago = [];
  try {
    const r = await mkReq().query(`
      SELECT cp.forma_pago, cp.idformadepago, cp.tipo,
        COUNT(DISTINCT cp.folio) AS tickets,
        ROUND(SUM(cp.importe),2) AS total,
        ROUND(ISNULL(SUM(cp.propina),0),2) AS propina
      FROM ${reportPaymentsUnion('cp')}
      WHERE CAST(cp.fecha AS DATE) BETWEEN @ini AND @fin AND cp.cancelado=0 AND cp.pagado=1 AND cp.total>0
      GROUP BY cp.idformadepago, cp.forma_pago, cp.tipo ORDER BY total DESC
    `);
    formasPago = r.recordset;
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  // ── Ventas por servicio (requiere areasrestaurant + idarearestaurant en cheques) ──
  let ventasServicio = [];
  try {
    const r = await mkReq().query(`
      SELECT ar.descripcion AS servicio, COUNT(c.folio) AS tickets,
        ROUND(ISNULL(SUM(c.total),0),2) AS total
      FROM ${db()}areasrestaurant ar
      LEFT JOIN ${reportSalesUnion('c')}
        ON c.idarearestaurant=ar.idarearestaurant
        AND CAST(c.fecha AS DATE) BETWEEN @ini AND @fin
        AND c.cancelado=0 AND c.pagado=1 AND c.total>0
      GROUP BY ar.idarearestaurant,ar.descripcion ORDER BY ar.idarearestaurant
    `);
    ventasServicio = r.recordset;
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  // ── Ventas por hora ──
  let ventasHora = [];
  try {
    const r = await mkReq().query(`
      SELECT DATEPART(HOUR, fecha) AS hora, COUNT(*) AS tickets,
        ROUND(SUM(total),2) AS venta
      FROM ${reportSalesUnion('c')}
      WHERE CAST(fecha AS DATE) BETWEEN @ini AND @fin
        AND cancelado=0 AND pagado=1 AND total>0
      GROUP BY DATEPART(HOUR, fecha) ORDER BY hora
    `);
    ventasHora = r.recordset;
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  // ── Meseros ──
  let meseros = [];
  try {
    const r = await mkReq().query(`
      SELECT c.idmesero,
        LTRIM(RTRIM(ISNULL(m.nombre, CAST(c.idmesero AS NVARCHAR(80))))) AS mesero,
        COUNT(*) AS tickets,
        ROUND(SUM(c.total),2) AS venta,
        ROUND(AVG(c.total),2) AS ticket_promedio,
        ROUND(ISNULL(SUM(c.totaldescuentos),0),2) AS descuentos,
        ROUND(ISNULL(SUM(c.totalcortesias),0),2) AS cortesias,
        ROUND(ISNULL(SUM(c.propina),0),2) AS propina,
        ROUND(ISNULL(SUM(c.nopersonas),0),0) AS personas
      FROM ${reportSalesUnion('c')}
      LEFT JOIN ${db()}meseros m ON CAST(m.idmesero AS NVARCHAR(80)) = CAST(c.idmesero AS NVARCHAR(80))
      WHERE CAST(c.fecha AS DATE) BETWEEN @ini AND @fin
        AND c.cancelado=0 AND c.pagado=1 AND c.total>0
      GROUP BY c.idmesero, m.nombre ORDER BY venta DESC
    `);
    meseros = r.recordset;
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  let top_productos = [];
  try {
    const r = await mkReq().query(`
      SELECT TOP 20
        LTRIM(RTRIM(ISNULL(p.descripcion, CAST(cd.idproducto AS VARCHAR)))) AS producto,
        ROUND(ISNULL(SUM(cd.cantidad), 0), 0)                               AS cantidad_vendida,
        ROUND(ISNULL(SUM(cd.cantidad * cd.precio), 0), 2)                   AS total_venta
      FROM ${reportDetailsUnion('cd')}
      LEFT JOIN ${db()}productos p ON cd.idproducto = p.idproducto
      WHERE CAST(cd.fecha AS DATE) BETWEEN @ini AND @fin
        AND cd.cancelado = 0 AND cd.pagado = 1 AND cd.total > 0
        AND (ISNULL(cd.modificador, 0) = 0 OR ISNULL(cd.precio, 0) > 0)
      GROUP BY cd.idproducto, p.descripcion
      ORDER BY cantidad_vendida DESC, total_venta DESC
    `);
    top_productos = r.recordset;
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  let resumenOperativo = {
    subtotal: 0, subtotal_sin_impuestos: 0, impuestos: 0, venta_total: 0,
    total_con_propina: 0, cargos: 0, total_con_cargo: 0, propinas: 0,
    propina_tarjeta: 0, descuentos: 0, cortesias: 0, descuento_monedero: 0,
    personas: 0, articulos: 0,
  };
  try {
    const r = await mkReq().query(`
      SELECT
        ROUND(ISNULL(SUM(subtotal),0),2) AS subtotal,
        ROUND(ISNULL(SUM(subtotalsinimpuestos),0),2) AS subtotal_sin_impuestos,
        ROUND(ISNULL(SUM(totalimpuesto1),0),2) AS impuestos,
        ROUND(ISNULL(SUM(total),0),2) AS venta_total,
        ROUND(ISNULL(SUM(totalconpropina),0),2) AS total_con_propina,
        ROUND(ISNULL(SUM(cargo),0),2) AS cargos,
        ROUND(ISNULL(SUM(totalconcargo),0),2) AS total_con_cargo,
        ROUND(ISNULL(SUM(propina),0),2) AS propinas,
        ROUND(ISNULL(SUM(propinatarjeta),0),2) AS propina_tarjeta,
        ROUND(ISNULL(SUM(totaldescuentos),0),2) AS descuentos,
        ROUND(ISNULL(SUM(totalcortesias),0),2) AS cortesias,
        ROUND(ISNULL(SUM(descuentomonedero),0),2) AS descuento_monedero,
        ROUND(ISNULL(SUM(nopersonas),0),0) AS personas,
        ROUND(ISNULL(SUM(totalarticulos),0),2) AS articulos
      FROM ${reportSalesUnion('c')}
      WHERE CAST(fecha AS DATE) BETWEEN @ini AND @fin AND cancelado=0 AND pagado=1 AND total>0
    `);
    resumenOperativo = r.recordset[0] || resumenOperativo;
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  let folios = {};
  try {
    const r = await mkReq().query(`
      SELECT
        MIN(folio) AS folio_inicial,
        MAX(folio) AS folio_final,
        MIN(numcheque) AS numcheque_inicial,
        MAX(numcheque) AS numcheque_final,
        COUNT(CASE WHEN cancelado=0 AND pagado=1 AND total>0 THEN 1 END) AS cuentas_pagadas,
        COUNT(CASE WHEN cancelado=1 THEN 1 END) AS cuentas_canceladas,
        COUNT(CASE WHEN cancelado=0 AND totaldescuentos>0 THEN 1 END) AS cuentas_con_descuento,
        COUNT(CASE WHEN cancelado=0 AND totalcortesias>0 THEN 1 END) AS cuentas_con_cortesia,
        ROUND(ISNULL(SUM(CASE WHEN cancelado=1 THEN total ELSE 0 END),0),2) AS importe_cancelado,
        ROUND(ISNULL(SUM(CASE WHEN cancelado=0 THEN impresiones ELSE 0 END),0),0) AS reimpresiones,
        ROUND(ISNULL(SUM(CASE WHEN cancelado=0 THEN reabiertas ELSE 0 END),0),0) AS reabiertas
      FROM ${reportSalesUnion('c')}
      WHERE CAST(fecha AS DATE) BETWEEN @ini AND @fin
    `);
    folios = r.recordset[0] || {};
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  let turnos = [];
  try {
    const r = await mkReq().query(`
      SELECT
        c.idturno,
        MIN(t.idturnointerno) AS idturnointerno,
        MIN(t.apertura) AS apertura,
        MAX(t.cierre) AS cierre,
        ISNULL(MAX(t.cajero),'') AS cajero,
        ISNULL(MAX(t.idestacion),'') AS estacion,
        ROUND(ISNULL(MAX(t.fondo),0),2) AS fondo,
        COUNT(*) AS tickets,
        ROUND(ISNULL(SUM(c.total),0),2) AS venta,
        ROUND(ISNULL(SUM(c.propina),0),2) AS propina,
        ROUND(ISNULL(SUM(c.totaldescuentos),0),2) AS descuentos,
        ROUND(ISNULL(SUM(c.totalcortesias),0),2) AS cortesias
      FROM ${reportSalesUnion('c')}
      LEFT JOIN ${db()}turnos t ON t.idturno=c.idturno
      WHERE CAST(c.fecha AS DATE) BETWEEN @ini AND @fin AND c.cancelado=0 AND c.pagado=1 AND c.total>0
      GROUP BY c.idturno
      ORDER BY MIN(t.apertura), c.idturno
    `);
    turnos = r.recordset.map(row => ({
      ...row,
      apertura: safeDate(row.apertura),
      cierre: safeDate(row.cierre),
    }));
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  let detalleTickets = [];
  try {
    const r = await mkReq().query(`
      SELECT TOP 500
        c.folio, c.numcheque, c.fecha, c.cierre, c.mesa, c.nopersonas,
        c.idturno, c.idmesero,
        LTRIM(RTRIM(ISNULL(m.nombre, CAST(c.idmesero AS NVARCHAR(80))))) AS mesero,
        ISNULL(ar.descripcion, CAST(c.idarearestaurant AS NVARCHAR(80))) AS servicio,
        ROUND(ISNULL(c.subtotal,0),2) AS subtotal,
        ROUND(ISNULL(c.totalimpuesto1,0),2) AS impuestos,
        ROUND(ISNULL(c.totaldescuentos,0),2) AS descuentos,
        ROUND(ISNULL(c.totalcortesias,0),2) AS cortesias,
        ROUND(ISNULL(c.propina,0),2) AS propina,
        ROUND(ISNULL(c.total,0),2) AS total,
        ROUND(ISNULL(c.efectivo,0),2) AS efectivo,
        ROUND(ISNULL(c.tarjeta,0),2) AS tarjeta,
        ROUND(ISNULL(c.vales,0),2) AS vales,
        ROUND(ISNULL(c.otros,0),2) AS otros
      FROM ${reportSalesUnion('c')}
      LEFT JOIN ${db()}meseros m ON CAST(m.idmesero AS NVARCHAR(80)) = CAST(c.idmesero AS NVARCHAR(80))
      LEFT JOIN ${db()}areasrestaurant ar ON ar.idarearestaurant=c.idarearestaurant
      WHERE CAST(c.fecha AS DATE) BETWEEN @ini AND @fin AND c.cancelado=0 AND c.pagado=1 AND c.total>0
      ORDER BY c.fecha, c.folio
    `);
    detalleTickets = r.recordset.map(row => ({
      ...row,
      fecha: safeDate(row.fecha),
      cierre: safeDate(row.cierre),
    }));
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  let pagosDetalle = [];
  try {
    const r = await mkReq().query(`
      SELECT cp.folio, cp.idformadepago, cp.forma_pago,
        ROUND(ISNULL(cp.importe,0),2) AS importe,
        ROUND(ISNULL(cp.propina,0),2) AS propina,
        cp.referencia, cp.cardBrand
      FROM ${reportPaymentsUnion('cp')}
      WHERE CAST(cp.fecha AS DATE) BETWEEN @ini AND @fin AND cp.cancelado=0 AND cp.pagado=1 AND cp.total>0
      ORDER BY cp.folio, cp.forma_pago
    `);
    pagosDetalle = r.recordset;
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  let declaracionCaja = [];
  try {
    const r = await mkReq().query(`
      SELECT dc.idturno, dc.idformadepago, fp.descripcion AS forma_pago,
        ROUND(ISNULL(dc.importedeclarado,0),2) AS importe_declarado,
        dc.tipo, dc.descripcion
      FROM ${db()}declaracioncajero dc
      LEFT JOIN ${db()}formasdepago fp ON fp.idformadepago=dc.idformadepago
      WHERE dc.idturno IN (
        SELECT DISTINCT idturno FROM ${reportSalesUnion('c')}
        WHERE CAST(fecha AS DATE) BETWEEN @ini AND @fin AND idturno IS NOT NULL
      )
      ORDER BY dc.idturno, fp.descripcion
    `);
    declaracionCaja = r.recordset;
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  let movimientosCaja = [];
  try {
    const r = await mkReq().query(`
      SELECT m.tipo, LTRIM(RTRIM(ISNULL(m.concepto,'Sin concepto'))) AS concepto,
        COUNT(*) AS movimientos,
        ROUND(ISNULL(SUM(m.importe),0),2) AS importe_total
      FROM ${db()}movtoscaja m
      WHERE CAST(m.fecha AS DATE) BETWEEN @ini AND @fin AND ISNULL(m.cancelado,0)=0
      GROUP BY m.tipo, LTRIM(RTRIM(ISNULL(m.concepto,'Sin concepto')))
      ORDER BY m.tipo, importe_total DESC
    `);
    movimientosCaja = r.recordset;
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  let cancelaciones = { cuentas: [], productos: [], resumen: {} };
  try {
    const cuentas = await mkReq().query(`
      SELECT c.folio, c.numcheque, c.fecha, c.mesa, c.idturno, c.idmesero,
        LTRIM(RTRIM(ISNULL(m.nombre, CAST(c.idmesero AS NVARCHAR(80))))) AS mesero,
        ROUND(ISNULL(c.total,0),2) AS total,
        c.razoncancelado, c.usuariocancelo, c.fechacancelado
      FROM ${db()}cheques c
      LEFT JOIN ${db()}meseros m ON CAST(m.idmesero AS NVARCHAR(80)) = CAST(c.idmesero AS NVARCHAR(80))
      WHERE CAST(c.fecha AS DATE) BETWEEN @ini AND @fin AND c.cancelado=1
      ORDER BY c.fechacancelado DESC, c.fecha DESC
    `);
    const productos = await mkReq().query(`
      SELECT TOP 500 ca.foliocheque AS folio, ca.fecha, ca.usuario,
        ca.clave AS idproducto, ISNULL(p.descripcion, ca.clave) AS producto,
        ca.razon, ca.cantidad, ROUND(ISNULL(ca.precio,0),2) AS precio,
        ROUND(ISNULL(ca.cantidad,0)*ISNULL(ca.precio,0),2) AS importe
      FROM ${db()}cancela ca
      LEFT JOIN ${db()}productos p ON p.idproducto=ca.clave
      WHERE CAST(ca.fecha AS DATE) BETWEEN @ini AND @fin
      ORDER BY ca.fecha DESC
    `);
    const cuentaRows = cuentas.recordset.map(row => ({
      ...row,
      fecha: safeDate(row.fecha),
      fechacancelado: safeDate(row.fechacancelado),
    }));
    const productoRows = productos.recordset.map(row => ({ ...row, fecha: safeDate(row.fecha) }));
    cancelaciones = {
      cuentas: cuentaRows,
      productos: productoRows,
      resumen: {
        cuentas_canceladas: cuentaRows.length,
        importe_cuentas: cuentaRows.reduce((acc, row) => acc + Number(row.total || 0), 0),
        productos_cancelados: productoRows.length,
        importe_productos: productoRows.reduce((acc, row) => acc + Number(row.importe || 0), 0),
      },
    };
  } catch(_e) { logger.debug('[query fallback] ' + (_e.message || '').substring(0,80)); }

  return {
    periodo: { inicio, fin, dias: dias+1 },
    periodoAnterior: { inicio: inicioAnterior, fin: finAnterior },
    totales:      totRec,
    anterior:     anteriorRec,
    ventasPorDia,
    ventasPorTipo,
    formasPago,
    ventasServicio,
    ventasHora,
    meseros,
    top_productos,
    resumenOperativo,
    folios,
    turnos,
    detalleTickets,
    pagosDetalle,
    declaracionCaja,
    movimientosCaja,
    cancelaciones,
  };
}

async function getCorteX(idturno = null) {
  try {

    const pool = await getPool();
    let turno;

    // SQL ya entrega fechas locales de SoftRestaurant; no aplicar offset manual
    const turnoQuery = `
      SELECT TOP 1 t.idturno,t.idturnointerno,
        t.apertura AS apertura,
        CASE WHEN t.cierre IS NULL THEN NULL ELSE t.cierre END AS cierre,
        t.cajero,t.idestacion,t.fondo,t.efectivo,t.tarjeta,t.vales,t.credito
      FROM ${db()}turnos t`;

    if (idturno) {
      const rt = await pool.request().input('tid', sql.Int, parseInt(idturno))
        .query(`${turnoQuery} WHERE t.idturno=@tid`);
      turno = rt.recordset[0] || null;
    } else {
      const rt = await pool.request().query(`${turnoQuery} WHERE t.cierre IS NULL ORDER BY t.apertura DESC`);
      turno = rt.recordset[0] || null;
      if (!turno) {
        const rf = await pool.request().query(`${turnoQuery} ORDER BY t.apertura DESC`);
        turno = rf.recordset[0] || null;
      }
    }

    if (!turno) return null;
    const tid = turno.idturno;

    const mkR = () => pool.request().input('tid', sql.Int, tid);

    const rVentas = await mkR().query(`
      SELECT COUNT(*) AS tickets,
        ROUND(ISNULL(SUM(total),0),2)           AS venta_total,
        ROUND(ISNULL(SUM(subtotal),0),2)        AS subtotal,
        ROUND(ISNULL(SUM(totalimpuesto1),0),2)  AS impuestos,
        ROUND(ISNULL(SUM(totaldescuentos),0),2) AS descuentos,
        ROUND(ISNULL(SUM(totalcortesias),0),2)  AS cortesias,
        ROUND(ISNULL(SUM(propina),0),2)         AS propinas,
        ROUND(ISNULL(SUM(totalalimentos),0),2)  AS alimentos,
        ROUND(ISNULL(SUM(totalbebidas),0),2)    AS bebidas,
        ROUND(ISNULL(SUM(totalotros),0),2)      AS otros,
        ROUND(ISNULL(AVG(total),0),2)           AS ticket_promedio,
        MIN(fecha) AS primera_venta, MAX(fecha) AS ultima_venta
      FROM ${db()}cheques
      WHERE idturno=@tid AND cancelado=0 AND pagado=1 AND total>0
    `);

    const rCancelados = await mkR().query(`
      SELECT COUNT(*) AS cantidad, ROUND(ISNULL(SUM(total),0),2) AS importe
      FROM (
        SELECT total FROM ${db()}cheques    WHERE idturno=@tid AND cancelado=1
        UNION ALL
        SELECT total FROM ${db()}tempcheques WHERE idturno=@tid AND cancelado=1
      ) AS c
    `);

    const rPagos = await mkR().query(`
      SELECT fp.descripcion AS forma_pago, COUNT(cp.folio) AS tickets,
        ROUND(SUM(cp.importe),2) AS total
      FROM ${db()}chequespagos cp
      INNER JOIN ${db()}formasdepago fp ON cp.idformadepago=fp.idformadepago
      INNER JOIN ${db()}cheques c ON cp.folio=c.folio
      WHERE c.idturno=@tid AND c.cancelado=0 AND c.pagado=1
      GROUP BY fp.descripcion ORDER BY total DESC
    `);

    const rHoras = await mkR().query(`
      SELECT DATEPART(HOUR,fecha) AS hora, COUNT(*) AS tickets, ROUND(SUM(total),2) AS venta
      FROM ${db()}cheques
      WHERE idturno=@tid AND cancelado=0 AND pagado=1 AND total>0
      GROUP BY DATEPART(HOUR,fecha) ORDER BY hora
    `);

    const rServicio = await mkR().query(`
      SELECT ar.descripcion AS servicio, COUNT(c.folio) AS tickets,
        ROUND(ISNULL(SUM(c.total),0),2) AS total
      FROM ${db()}areasrestaurant ar
      LEFT JOIN ${db()}cheques c
        ON c.idarearestaurant=ar.idarearestaurant AND c.idturno=@tid AND c.cancelado=0 AND c.pagado=1 AND c.total>0
      GROUP BY ar.idarearestaurant,ar.descripcion ORDER BY ar.idarearestaurant
    `);

    const rMeseros = await mkR().query(`
      SELECT c.idmesero,
        LTRIM(RTRIM(ISNULL(m.nombre, CAST(c.idmesero AS NVARCHAR(80))))) AS mesero,
        COUNT(*) AS tickets, ROUND(SUM(c.total),2) AS venta,
        ROUND(AVG(c.total),2) AS ticket_promedio
      FROM ${db()}cheques c
      LEFT JOIN ${db()}meseros m ON CAST(m.idmesero AS NVARCHAR(80)) = CAST(c.idmesero AS NVARCHAR(80))
      WHERE c.idturno=@tid AND c.cancelado=0 AND c.pagado=1 AND c.total>0
      GROUP BY c.idmesero, m.nombre ORDER BY venta DESC
    `);

    const rFolios = await mkR().query(`
      SELECT MIN(c.folio) AS folio_inicial, MAX(c.folio) AS folio_final,
        COUNT(CASE WHEN c.cancelado=0 AND c.pagado=1 THEN 1 END) AS total_cuentas,
        COUNT(CASE WHEN c.cancelado=1 THEN 1 END) AS cuentas_canceladas,
        COUNT(CASE WHEN c.totaldescuentos>0 AND c.cancelado=0 THEN 1 END) AS descuento_a_cuenta
      FROM (
        SELECT folio, cancelado, pagado, totaldescuentos
        FROM ${db()}cheques    WHERE idturno=@tid
        UNION ALL
        SELECT folio, cancelado, pagado, totaldescuentos
        FROM ${db()}tempcheques WHERE idturno=@tid
      ) AS c
    `);

  
    let top_productos = [];
    try {
      const rTopProd = await mkR().query(`
        SELECT TOP 5
          LTRIM(RTRIM(ISNULL(p.descripcion, CAST(cd.idproducto AS VARCHAR)))) AS producto,
          SUM(cd.cantidad)                        AS cantidad,
          ROUND(SUM(cd.cantidad * cd.precio), 2)  AS total_venta
        FROM ${db()}cheqdet cd
        INNER JOIN ${db()}cheques c ON cd.foliodet = c.folio
        LEFT  JOIN ${db()}productos p ON cd.idproducto = p.idproducto
        WHERE c.idturno=@tid AND c.cancelado=0 AND c.pagado=1 AND cd.cantidad>0
        GROUP BY cd.idproducto, p.descripcion
        ORDER BY total_venta DESC
      `);
      top_productos = rTopProd.recordset;
    } catch (_) {  }

  
    let movimientos_caja = {};
    try {
      const rMovCaja = await mkR().query(`
        SELECT
          ISNULL(SUM(CASE WHEN m.tipo=1 THEN m.importe ELSE 0 END),0) AS retiros,
          ISNULL(SUM(CASE WHEN m.tipo=2 THEN m.importe ELSE 0 END),0) AS depositos,
          ISNULL(SUM(CASE WHEN m.tipo=3 THEN m.importe ELSE 0 END),0) AS propinas_pagadas
        FROM ${db()}movtoscaja m
        WHERE m.idturno=@tid AND ISNULL(m.cancelado,0)=0
      `);
      movimientos_caja = rMovCaja.recordset[0] || {};
    } catch (_) {  }

    return {
      turno: {
        ...turno, estado: turno.cierre===null ? 'abierto' : 'cerrado',
        apertura: turno.apertura ? toLocalISOStr(turno.apertura) : null,
        cierre:   turno.cierre   ? toLocalISOStr(turno.cierre)   : null,
      },
      ventas:         rVentas.recordset[0],
      cancelados:     rCancelados.recordset[0],
      formasPago:     rPagos.recordset,
      ventasPorHora:  rHoras.recordset,
      ventasServicio: rServicio.recordset,
      meseros:        rMeseros.recordset,
      folios:         rFolios.recordset[0] || {},
      top_productos,
      movimientos_caja,
      generadoEn:     toLocalISOStr(new Date()),
    };
  } catch(_) { return null; }
}

async function getProductosCancelados(inicio, fin, idturno = null, page = 1, pageSize = 50) {
  try {

    const pool   = await getPool();
    const offset = (Math.max(1, page) - 1) * pageSize;
    const limit  = Math.min(pageSize, 200);

    const mkReq = () => {
      const r = pool.request();
      r.input('pc_offset', sql.Int, offset);
      r.input('pc_limit',  sql.Int, limit);
      if (idturno) {
        r.input('pc_turno', sql.Int, parseInt(idturno));
      } else {
        r.input('pc_ini', sql.Date, inicio);
        r.input('pc_fin', sql.Date, fin);
      }
      return r;
    };

    const UNIONcheques = idturno
      ? `(
          SELECT folio, fecha, mesa, idmesero, total, razoncancelado, usuariocancelo, fechacancelado
          FROM ${db()}cheques    WHERE cancelado=1 AND idturno=@pc_turno
          UNION ALL
          SELECT folio, fecha, mesa, idmesero, total, razoncancelado, usuariocancelo, fechacancelado
          FROM ${db()}tempcheques WHERE cancelado=1 AND idturno=@pc_turno
        ) AS c`
      : `(
          SELECT folio, fecha, mesa, idmesero, total, razoncancelado, usuariocancelo, fechacancelado
          FROM ${db()}cheques
          WHERE cancelado=1 AND CAST(fecha AS DATE) BETWEEN @pc_ini AND @pc_fin
          UNION ALL
          SELECT folio, fecha, mesa, idmesero, total, razoncancelado, usuariocancelo, fechacancelado
          FROM ${db()}tempcheques
          WHERE cancelado=1 AND CAST(fecha AS DATE) BETWEEN @pc_ini AND @pc_fin
        ) AS c`;

    const whereBase = '1=1';

    const rResumen = await mkReq().query(`
      SELECT COUNT(*) AS cuentas_canceladas, ROUND(ISNULL(SUM(c.total),0),2) AS importe_cancelado,
        COUNT(DISTINCT c.idmesero) AS meseros_involucrados,
        COUNT(DISTINCT CAST(c.fecha AS DATE)) AS dias
      FROM ${UNIONcheques} WHERE ${whereBase}
    `);

    const rCuentas = await mkReq().query(`
      SELECT c.folio, c.fecha, c.mesa, c.idmesero, ROUND(c.total,2) AS total,
        c.razoncancelado, c.usuariocancelo, c.fechacancelado
      FROM ${UNIONcheques}
      WHERE ${whereBase}
      ORDER BY c.fecha DESC
      OFFSET @pc_offset ROWS FETCH NEXT @pc_limit ROWS ONLY
    `);

    const rTotal = await mkReq().query(`
      SELECT COUNT(*) AS total FROM ${UNIONcheques}
      WHERE ${whereBase}
    `);

    const rPorUsuario = await mkReq().query(`
      SELECT ISNULL(c.usuariocancelo,'Sin registro') AS usuario,
        COUNT(*) AS cantidad, ROUND(ISNULL(SUM(c.total),0),2) AS importe
      FROM ${UNIONcheques}
      WHERE ${whereBase}
      GROUP BY c.usuariocancelo ORDER BY cantidad DESC
    `);

    const rMotivos = await mkReq().query(`
      SELECT ISNULL(NULLIF(LTRIM(RTRIM(c.razoncancelado)),''),'Sin motivo') AS motivo,
        COUNT(*) AS cantidad
      FROM ${UNIONcheques}
      WHERE ${whereBase}
      GROUP BY LTRIM(RTRIM(c.razoncancelado)) ORDER BY cantidad DESC
    `);

    const totalRegistros = rTotal.recordset[0].total;
    return {
      resumen: rResumen.recordset[0], cuentas: rCuentas.recordset,
      porUsuario: rPorUsuario.recordset, motivos: rMotivos.recordset,
      paginacion: { page, pageSize: limit, total: totalRegistros, paginas: Math.ceil(totalRegistros / limit) },
      periodo: idturno ? { tipo: 'turno', idturno } : { tipo: 'fechas', inicio, fin },
      generadoEn: new Date().toISOString(),
    };
  } catch(_) { return { data: [], total: 0, pagina: 1, totalPaginas: 1 }; }
}

async function getProductosIndCancelados(idturno = null, page = 1, pageSize = 200, inicio = null, fin = null) {
  try {

    const pool   = await getPool();
    const offset = (Math.max(1, page) - 1) * pageSize;
    const limit  = Math.min(pageSize, 500);
    const CA  = `${db()}cancela`;
    const PRD = `${db()}productos`;

    // JOIN contra cheques + tempcheques: cancelaciones de cuentas abiertas (turno activo)
    // viven en tempcheques; las cerradas/pagadas en cheques.
    // Cuando filtra por fecha, acotar también el UNION para evitar full scan
    const UNION_CHQ = idturno
      ? `(SELECT folio, mesa, idmesero, idturno FROM ${db()}cheques    WHERE idturno=@tid
         UNION ALL
         SELECT folio, mesa, idmesero, idturno FROM ${db()}tempcheques WHERE idturno=@tid) AS c`
      : `(SELECT folio, mesa, idmesero, idturno FROM ${db()}cheques
           WHERE CAST(fecha AS DATE) BETWEEN @ini AND @fin
         UNION ALL
         SELECT folio, mesa, idmesero, idturno FROM ${db()}tempcheques
           WHERE CAST(fecha AS DATE) BETWEEN @ini AND @fin) AS c`;

    const mkReq = () => {
      const r = pool.request()
        .input('offset', sql.Int, offset)
        .input('limit',  sql.Int, limit);
      if (idturno) {
        r.input('tid', sql.Int, parseInt(idturno));
      } else {
        r.input('ini', sql.Date, inicio);
        r.input('fin', sql.Date, fin);
      }
      return r;
    };
    const WHERE = idturno
      ? 'c.idturno = @tid'
      : 'CAST(ca.fecha AS DATE) BETWEEN @ini AND @fin';

    const rResumen = await mkReq().query(`
      SELECT COUNT(*) AS productos_cancelados,
        ROUND(ISNULL(SUM(ca.precio*ca.cantidad),0),2) AS importe_cancelado,
        COUNT(DISTINCT ca.foliocheque) AS cuentas_afectadas,
        COUNT(DISTINCT c.idmesero) AS meseros_involucrados
      FROM ${CA} ca INNER JOIN ${UNION_CHQ} ON ca.foliocheque=c.folio
      WHERE ${WHERE}
    `);

    const rTopProductos = await mkReq().query(`
      SELECT TOP 50 ISNULL(p.descripcion, ca.clave) AS producto,
        SUM(ca.cantidad)                         AS cantidad_total,
        ROUND(AVG(ca.precio),2)                  AS precio_unitario,
        ROUND(SUM(ca.precio*ca.cantidad),2)      AS importe_total
      FROM ${CA} ca INNER JOIN ${UNION_CHQ} ON ca.foliocheque=c.folio
      LEFT JOIN ${PRD} p ON ca.clave=p.idproducto
      WHERE ${WHERE}
      GROUP BY ca.clave, p.descripcion
      ORDER BY cantidad_total DESC
    `);

    const rPorUsuario = await mkReq().query(`
      SELECT ISNULL(ca.usuario,'Sin registro') AS usuario,
        COUNT(*) AS cantidad, ROUND(ISNULL(SUM(ca.precio*ca.cantidad),0),2) AS importe
      FROM ${CA} ca INNER JOIN ${UNION_CHQ} ON ca.foliocheque=c.folio
      WHERE ${WHERE}
      GROUP BY ca.usuario ORDER BY cantidad DESC
    `);

    const rMotivos = await mkReq().query(`
      SELECT ISNULL(NULLIF(LTRIM(RTRIM(ca.razon)),''),'Sin motivo') AS motivo, COUNT(*) AS cantidad
      FROM ${CA} ca INNER JOIN ${UNION_CHQ} ON ca.foliocheque=c.folio
      WHERE ${WHERE}
      GROUP BY LTRIM(RTRIM(ca.razon)) ORDER BY cantidad DESC
    `);

    const rTotal = await mkReq().query(`
      SELECT COUNT(*) AS total
      FROM ${CA} ca INNER JOIN ${UNION_CHQ} ON ca.foliocheque=c.folio
      WHERE ${WHERE}
    `);

    const rDetalle = await mkReq().query(`
      SELECT ca.foliocheque AS folio, ISNULL(p.descripcion, ca.clave) AS producto,
        ca.cantidad, ROUND(ca.precio,2) AS precio_unitario,
        ROUND(ca.precio*ca.cantidad,2)  AS importe_total,
        c.mesa, c.idmesero, ca.fecha,
        ISNULL(NULLIF(LTRIM(RTRIM(ca.razon)),''),'Sin motivo') AS motivo,
        ISNULL(ca.usuario,'Sin registro') AS usuario_cancelo
      FROM ${CA} ca INNER JOIN ${UNION_CHQ} ON ca.foliocheque=c.folio
      LEFT JOIN ${PRD} p ON ca.clave=p.idproducto
      WHERE ${WHERE}
      ORDER BY ca.fecha DESC, ca.foliocheque
      OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY
    `);

    const totalRegistros = rTotal.recordset[0].total;
    return {
      resumen:    rResumen.recordset[0],
      topProductos: rTopProductos.recordset,
      porUsuario: rPorUsuario.recordset,
      motivos:    rMotivos.recordset,
      detalle:    rDetalle.recordset,
      paginacion: { page, pageSize: limit, total: totalRegistros, paginas: Math.ceil(totalRegistros / limit) },
      periodo:    idturno ? { tipo: 'turno', idturno: parseInt(idturno) } : { tipo: 'fechas', inicio, fin },
      generadoEn: new Date().toISOString(),
    };
  } catch(_) { return { data: [], total: 0, pagina: 1, totalPaginas: 1 }; }
}

async function getCancelacionesTurnoActual() {
  try {

    const pool = await getPool();
    const hoy  = localDateStr();
    const mkR  = () => pool.request().input('rs_hoy', sql.Date, hoy);

    const rResumen = await mkR().query(`
      SELECT COUNT(*) AS total_cancelaciones,
        ROUND(ISNULL(SUM(ca.precio*ca.cantidad),0),2) AS importe_cancelado,
        COUNT(DISTINCT ca.foliocheque) AS cuentas_afectadas
      FROM ${db()}cancela ca WHERE CAST(ca.fecha AS DATE)=@rs_hoy
    `);

    // Contar cuentas canceladas (cheques + tempcheques) independientemente de la tabla cancela
    const rCuentasCount = await mkR().query(`
      SELECT COUNT(*) AS n FROM (
        SELECT folio FROM ${db()}cheques
          WHERE CAST(fecha AS DATE)=@rs_hoy AND cancelado=1
        UNION ALL
        SELECT folio FROM ${db()}tempcheques
          WHERE CAST(fecha AS DATE)=@rs_hoy AND cancelado=1
      ) AS c
    `);
    const totalCuentasCanceladas = rCuentasCount.recordset[0].n || 0;

    const total = rResumen.recordset[0].total_cancelaciones;

    // Solo retornar vacío si no hay productos cancelados Y no hay cuentas canceladas
    if (total === 0 && totalCuentasCanceladas === 0) return {
      total_cancelaciones:0, importe_cancelado:0, cuentas_afectadas:0,
      top_productos:[], por_usuario:[], motivos:[],
      detalle_productos:[], cuentas_canceladas:[],
      usuario_top:null, ultima_cancelacion:null,
    };

    // Todos los queries en paralelo para mínima latencia
    const [rTop, rUsuario, rUltima, rMotivos, rDetalleProductos, rCuentas] = await Promise.all([
      mkR().query(`
        SELECT TOP 10 ISNULL(p.descripcion, CAST(ca.clave AS VARCHAR)) AS producto,
          SUM(ca.cantidad) AS cantidad,
          ROUND(SUM(ca.precio*ca.cantidad),2) AS importe_total
        FROM ${db()}cancela ca
        LEFT JOIN ${db()}productos p ON ca.clave=p.idproducto
        WHERE CAST(ca.fecha AS DATE)=@rs_hoy
        GROUP BY ca.clave, p.descripcion ORDER BY cantidad DESC
      `),
      mkR().query(`
        SELECT ISNULL(ca.usuario,'Sin registro') AS usuario,
          COUNT(*) AS cantidad,
          ROUND(ISNULL(SUM(ca.precio*ca.cantidad),0),2) AS importe
        FROM ${db()}cancela ca WHERE CAST(ca.fecha AS DATE)=@rs_hoy
        GROUP BY ca.usuario ORDER BY cantidad DESC
      `),
      mkR().query(`
        SELECT TOP 1 ISNULL(p.descripcion, CAST(ca.clave AS VARCHAR)) AS producto, ca.cantidad,
          ROUND(ca.precio*ca.cantidad,2) AS importe,
          ISNULL(ca.usuario,'Sin registro') AS usuario,
          ISNULL(NULLIF(LTRIM(RTRIM(ca.razon)),''),'Sin motivo') AS razon,
          ca.fecha AS fecha
        FROM ${db()}cancela ca
        LEFT JOIN ${db()}productos p ON ca.clave=p.idproducto
        WHERE CAST(ca.fecha AS DATE)=@rs_hoy ORDER BY ca.fecha DESC
      `),
      mkR().query(`
        SELECT ISNULL(NULLIF(LTRIM(RTRIM(ca.razon)),''),'Sin motivo') AS motivo,
          COUNT(*) AS cantidad
        FROM ${db()}cancela ca WHERE CAST(ca.fecha AS DATE)=@rs_hoy
        GROUP BY LTRIM(RTRIM(ca.razon)) ORDER BY cantidad DESC
      `),
      mkR().query(`
        SELECT TOP 200
          ca.foliocheque AS folio,
          ISNULL(p.descripcion, CAST(ca.clave AS VARCHAR)) AS producto,
          ca.cantidad,
          ROUND(ca.precio,2) AS precio_unitario,
          ROUND(ca.precio*ca.cantidad,2) AS importe_total,
          c.mesa,
          c.idmesero,
          ca.fecha AS fecha,
          ISNULL(NULLIF(LTRIM(RTRIM(ca.razon)),''),'Sin motivo') AS motivo,
          ISNULL(ca.usuario,'Sin registro') AS usuario_cancelo
        FROM ${db()}cancela ca
        INNER JOIN ${db()}cheques c ON ca.foliocheque=c.folio
        LEFT JOIN ${db()}productos p ON ca.clave=p.idproducto
        WHERE CAST(ca.fecha AS DATE)=@rs_hoy
        ORDER BY ca.fecha DESC
      `),
      mkR().query(`
        SELECT TOP 200
          c.folio, c.mesa, c.idmesero,
          c.fechacancelado AS fechacancelado,
          c.fecha AS fecha,
          ROUND(c.total,2) AS total,
          c.razoncancelado, c.usuariocancelo
        FROM (
          SELECT folio, mesa, idmesero, fechacancelado, fecha, total, razoncancelado, usuariocancelo
          FROM ${db()}cheques
          WHERE CAST(fecha AS DATE)=@rs_hoy AND cancelado=1
          UNION ALL
          SELECT folio, mesa, idmesero, fechacancelado, fecha, total, razoncancelado, usuariocancelo
          FROM ${db()}tempcheques
          WHERE CAST(fecha AS DATE)=@rs_hoy AND cancelado=1
        ) AS c
        ORDER BY c.fechacancelado DESC, c.fecha DESC
      `),
    ]);

    return {
      ...rResumen.recordset[0],
      top_productos:      rTop.recordset,
      por_usuario:        rUsuario.recordset,
      motivos:            rMotivos.recordset,
      detalle_productos:  rDetalleProductos.recordset,
      cuentas_canceladas: rCuentas.recordset,
      usuario_top:        rUsuario.recordset[0] || null,
      ultima_cancelacion: rUltima.recordset[0]  || null,
    };
  } catch(_) { return []; }
}

async function columnExists(pool, table, column) {
  try {
    const dbName = process.env.DB_NAME || 'softrestaurant11';
    const r = await pool.request()
      .input('table', sql.NVarChar(128), table)
      .input('column', sql.NVarChar(128), column)
      .query(`
        SELECT 1 AS ok
        FROM ${dbName}.INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA='dbo' AND TABLE_NAME=@table AND COLUMN_NAME=@column
      `);
    return r.recordset.length > 0;
  } catch(_) {
    return false;
  }
}

async function tableExists(pool, table) {
  try {
    const dbName = process.env.DB_NAME || 'softrestaurant11';
    const r = await pool.request()
      .input('table', sql.NVarChar(128), table)
      .query(`
        SELECT 1 AS ok
        FROM ${dbName}.INFORMATION_SCHEMA.TABLES
        WHERE TABLE_SCHEMA='dbo' AND TABLE_NAME=@table
      `);
    return r.recordset.length > 0;
  } catch(_) {
    return false;
  }
}


async function getCortesiasCatalogos(inicio = null, fin = null) {
  const pool = await getPool();
  const data = { meseros: [], productos: [], tiposDescuento: [], usuarios: [], turnos: [] };

  try {
    if (await tableExists(pool, 'meseros')) {
      const r = await pool.request().query(`
        SELECT
          CAST(idmesero AS NVARCHAR(80)) AS id,
          LTRIM(RTRIM(ISNULL(nombre, CAST(idmesero AS NVARCHAR(80))))) AS nombre
        FROM ${db()}meseros
        ORDER BY nombre
      `);
      data.meseros = r.recordset;
    }
  } catch(_) {}

  try {
    if (await tableExists(pool, 'productos')) {
      const r = await pool.request().query(`
        SELECT TOP 5000
          CAST(idproducto AS NVARCHAR(80)) AS id,
          LTRIM(RTRIM(ISNULL(descripcion, CAST(idproducto AS NVARCHAR(80))))) AS nombre
        FROM ${db()}productos
        ORDER BY nombre
      `);
      data.productos = r.recordset;
    }
  } catch(_) {}

  try {
    if (await tableExists(pool, 'tiposdescuento')) {
      const r = await pool.request().query(`
        SELECT
          CAST(idtipodescuento AS NVARCHAR(80)) AS id,
          LTRIM(RTRIM(ISNULL(descripcion, CAST(idtipodescuento AS NVARCHAR(80))))) AS nombre
        FROM ${db()}tiposdescuento
        ORDER BY nombre
      `);
      data.tiposDescuento = r.recordset;
    } else if (await tableExists(pool, 'tipos_descuento')) {
      const r = await pool.request().query(`
        SELECT
          CAST(idtipodescuento AS NVARCHAR(80)) AS id,
          LTRIM(RTRIM(ISNULL(desc_tipodescuento, CAST(idtipodescuento AS NVARCHAR(80))))) AS nombre
        FROM ${db()}tipos_descuento
        ORDER BY nombre
      `);
      data.tiposDescuento = r.recordset;
    }
  } catch(_) {}

  try {
    const cdUsuario = await columnExists(pool, 'cheqdet', 'usuario');
    const chUsuario = await columnExists(pool, 'cheques', 'usuario');
    const tcdUsuario = await columnExists(pool, 'tempcheqdet', 'usuario');
    const tchUsuario = await columnExists(pool, 'tempcheques', 'usuario');
    const cdtUsuarioDesc = await columnExists(pool, 'cheqdet', 'usuariodescuento');
    const tcdtUsuarioDesc = await columnExists(pool, 'tempcheqdet', 'usuariodescuento');
    const partes = [];
    if (cdUsuario) partes.push(`SELECT DISTINCT LTRIM(RTRIM(usuario)) AS usuario FROM ${db()}cheqdet WHERE NULLIF(LTRIM(RTRIM(usuario)), '') IS NOT NULL`);
    if (chUsuario) partes.push(`SELECT DISTINCT LTRIM(RTRIM(usuario)) AS usuario FROM ${db()}cheques WHERE NULLIF(LTRIM(RTRIM(usuario)), '') IS NOT NULL`);
    if (tcdUsuario) partes.push(`SELECT DISTINCT LTRIM(RTRIM(usuario)) AS usuario FROM ${db()}tempcheqdet WHERE NULLIF(LTRIM(RTRIM(usuario)), '') IS NOT NULL`);
    if (tchUsuario) partes.push(`SELECT DISTINCT LTRIM(RTRIM(usuario)) AS usuario FROM ${db()}tempcheques WHERE NULLIF(LTRIM(RTRIM(usuario)), '') IS NOT NULL`);
    if (cdtUsuarioDesc) partes.push(`SELECT DISTINCT LTRIM(RTRIM(usuariodescuento)) AS usuario FROM ${db()}cheqdet WHERE NULLIF(LTRIM(RTRIM(usuariodescuento)), '') IS NOT NULL`);
    if (tcdtUsuarioDesc) partes.push(`SELECT DISTINCT LTRIM(RTRIM(usuariodescuento)) AS usuario FROM ${db()}tempcheqdet WHERE NULLIF(LTRIM(RTRIM(usuariodescuento)), '') IS NOT NULL`);

    if (partes.length) {
      const r = await pool.request().query(`
        SELECT TOP 500 usuario AS id, usuario AS nombre
        FROM (${partes.join(' UNION ')}) u
        ORDER BY usuario
      `);
      data.usuarios = r.recordset;
    }
  } catch(_) {}

  try {
    if (await tableExists(pool, 'turnos')) {
      const req = pool.request();
      const where = [];
      if (inicio && fin) {
        req.input('cat_ini', sql.VarChar(10), inicio);
        req.input('cat_fin', sql.VarChar(10), fin);
        where.push('CAST(apertura AS DATE) BETWEEN CONVERT(date,@cat_ini,23) AND CONVERT(date,@cat_fin,23)');
      }
      const r = await req.query(`
        SELECT TOP 300
          CAST(idturno AS NVARCHAR(80)) AS id,
          CONCAT('Turno ', idturno, ' - ', CONVERT(VARCHAR(10), apertura, 120)) AS nombre
        FROM ${db()}turnos
        ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
        ORDER BY apertura DESC
      `);
      data.turnos = r.recordset;
    }
  } catch(_) {}

  return data;
}

async function getCortesias(params) {
  const pool = await getPool();
  const inicio = params.inicio;
  const fin = params.fin;
  const idturno = params.idturno || null;

  const req = pool.request();
  if (inicio && fin) {
    req.input('ini', sql.VarChar(10), inicio);
    req.input('fin', sql.VarChar(10), fin);
  }
  if (idturno) {
    req.input('tid', sql.Int, parseInt(idturno, 10));
  }
  
  // Variables booleanas de existencia de tablas/columnas
  const tCheq = await tableExists(pool, 'cheques');
  const tTemp = await tableExists(pool, 'tempcheques');
  const tMeseros = await tableExists(pool, 'meseros');
  const tTurnos = await tableExists(pool, 'turnos');
  const tTiposDesc1 = await tableExists(pool, 'tipodescuento');
  const tTiposDesc2 = await tableExists(pool, 'tiposdescuento');
  const tAreas = await tableExists(pool, 'areasrestaurant');
  const td1Descripcion = tTiposDesc1 ? await columnExists(pool, 'tipodescuento', 'descripcion') : false;
  const td2Descripcion = tTiposDesc2 ? await columnExists(pool, 'tiposdescuento', 'descripcion') : false;
  
  const cdPrecioSin = await columnExists(pool, 'cheqdet', 'preciosinimpuestos');
  const cdPrecio = await columnExists(pool, 'cheqdet', 'precio');
  const cdDescuento = await columnExists(pool, 'cheqdet', 'descuento');
  const cdIdCortesia = await columnExists(pool, 'cheqdet', 'idcortesia');
  const cdComentario = await columnExists(pool, 'cheqdet', 'comentario');
  const cdComentarioDesc = await columnExists(pool, 'cheqdet', 'comentariodescuento');
  const cdTipoDesc = await columnExists(pool, 'cheqdet', 'idtipodescuento');
  const cdUsuario = await columnExists(pool, 'cheqdet', 'usuario');
  const cdUsuarioDesc = await columnExists(pool, 'cheqdet', 'usuariodescuento');

  const chNumCheque = await columnExists(pool, 'cheques', 'numcheque');
  const chTotal = await columnExists(pool, 'cheques', 'total');
  const chSubTotal = await columnExists(pool, 'cheques', 'subtotal');
  const chTotalCortesias = await columnExists(pool, 'cheques', 'totalcortesias');
  const chTotalDescuentos = await columnExists(pool, 'cheques', 'totaldescuentos');
  const chIdMesero = await columnExists(pool, 'cheques', 'idmesero');
  const chIdTurno = await columnExists(pool, 'cheques', 'idturno');
  const chUsuario = await columnExists(pool, 'cheques', 'usuario');
  const chComentario = await columnExists(pool, 'cheques', 'comentario');

  const firstNonEmptySql = (expressions) => {
    if (!expressions.length) return "CAST(NULL AS NVARCHAR(255))";
    return `COALESCE(${expressions.map(expr => `NULLIF(LTRIM(RTRIM(${expr})), '')`).join(', ')}, '')`;
  };
  const usuarioAutorizoExpr = firstNonEmptySql([
    ...(cdUsuarioDesc ? ['cd.usuariodescuento'] : []),
    ...(cdUsuario ? ['cd.usuario'] : []),
    ...(chUsuario ? ['ch.usuario'] : []),
  ]);
  const motivoExpr = firstNonEmptySql([
    ...(cdComentarioDesc ? ['cd.comentariodescuento'] : []),
    ...(cdComentario ? ['cd.comentario'] : []),
    ...(chComentario ? ['ch.comentario'] : []),
  ]);

  const selectCols = `
    ch.fecha AS fecha,
    ch.folio AS folio,
    ${chNumCheque ? 'ISNULL(ch.numcheque, ch.folio)' : 'ch.folio'} AS numcheque,
    ${chIdMesero ? 'ch.idmesero' : 'NULL'} AS idmesero,
    ${tMeseros && chIdMesero ? 'ISNULL(m.nombre, CAST(ch.idmesero AS NVARCHAR(80)))' : (chIdMesero ? 'CAST(ch.idmesero AS NVARCHAR(80))' : "'Sin mesero'")} AS meseroNombre,
    ${chIdTurno ? 'ch.idturno' : 'NULL'} AS idturno,
    ${tTurnos && chIdTurno ? 'CONCAT(\'Turno \', ch.idturno, \' - \', ISNULL(t.cajero,\'\'))' : 'CAST(ch.idturno AS NVARCHAR(80))'} AS turnoNombre,
    ch.mesa AS mesa,
    ${tAreas ? 'ch.idarearestaurant' : 'NULL'} AS idarearestaurant,
    cd.idproducto AS idproducto,
    ISNULL(p.descripcion, CAST(cd.idproducto AS NVARCHAR(80))) AS productoNombre,
    ISNULL(cd.cantidad, 0) AS cantidad,
    ${cdPrecioSin ? 'ISNULL(cd.preciosinimpuestos, ISNULL(cd.precio, 0))' : (cdPrecio ? 'ISNULL(cd.precio, 0)' : '0')} AS precioUnitario,
    ${cdDescuento ? 'ISNULL(cd.descuento, 0)' : '0'} AS porcentajeDescuento,
    ${cdIdCortesia ? 'ISNULL(cd.idcortesia, 0)' : '0'} AS idcortesia,
    ${cdTipoDesc ? 'cd.idtipodescuento' : 'NULL'} AS idtipodescuento,
    ${usuarioAutorizoExpr} AS usuarioAutorizo,
    ${motivoExpr} AS motivo,
    ${chTotal ? 'ISNULL(ch.total, 0)' : '0'} AS totalCuenta,
    ${chTotalDescuentos ? 'ISNULL(ch.totaldescuentos, 0)' : '0'} AS totalDescuentosCuenta,
    ${chTotalCortesias ? 'ISNULL(ch.totalcortesias, 0)' : '0'} AS totalCortesiasCuenta
  `;

  const joins = `
    LEFT JOIN ${db()}productos p ON cd.idproducto = p.idproducto
    ${tMeseros && chIdMesero ? `LEFT JOIN ${db()}meseros m ON CAST(m.idmesero AS NVARCHAR(80)) = CAST(ch.idmesero AS NVARCHAR(80))` : ''}
    ${tTurnos && chIdTurno ? `LEFT JOIN ${db()}turnos t ON t.idturno = ch.idturno` : ''}
  `;

  const whereBase = [];
  whereBase.push('ISNULL(ch.cancelado,0)=0');
  if (inicio && fin) {
    whereBase.push('CAST(ch.fecha AS DATE) BETWEEN CONVERT(date,@ini,23) AND CONVERT(date,@fin,23)');
  }
  if (idturno && chIdTurno) {
    whereBase.push('ch.idturno=@tid');
  }

  // Filtrado interno para NO traer tooooda la base si no es necesario,
  // solo traemos lo que podría ser beneficio.
  const condicionBeneficio = `
    (${cdDescuento ? 'ISNULL(cd.descuento, 0) > 0' : '1=0'} 
     OR ${cdIdCortesia ? 'ISNULL(cd.idcortesia, 0) > 0' : '1=0'}
     OR ${chTotalCortesias ? 'ISNULL(ch.totalcortesias, 0) > 0' : '1=0'}
     OR ${chTotalDescuentos ? 'ISNULL(ch.totaldescuentos, 0) > 0' : '1=0'}
     OR (${cdPrecioSin ? 'ISNULL(cd.preciosinimpuestos, ISNULL(cd.precio, 0))' : (cdPrecio ? 'ISNULL(cd.precio, 0)' : '0')} > 0 AND ${chTotal ? 'ISNULL(ch.total, 0)' : '0'} = 0))
  `;
  whereBase.push(condicionBeneficio);

  const whereStr = whereBase.join(' AND ');

  const sqlUnion = [];
  if (tCheq) {
    sqlUnion.push(`
      SELECT 'CERRADA' AS origenCuenta, ${selectCols}
      FROM ${db()}cheqdet cd
      INNER JOIN ${db()}cheques ch ON cd.foliodet = ch.folio
      ${joins}
      WHERE ${whereStr}
    `);
  }
  if (tTemp) {
    sqlUnion.push(`
      SELECT 'ABIERTA_TEMPORAL' AS origenCuenta, ${selectCols}
      FROM ${db()}tempcheqdet cd
      INNER JOIN ${db()}tempcheques ch ON cd.foliodet = ch.folio
      ${joins}
      WHERE ${whereStr}
    `);
  }

  if (sqlUnion.length === 0) return { detalle: [], resumen: {} }; // No hay tablas

  const unionAll = sqlUnion.join(' UNION ALL ');

  // Aplicar filtros externos
  const externalWheres = [];
  if (params.mesero) {
    req.input('p_mesero', sql.NVarChar(80), params.mesero);
    req.input('p_meseroLike', sql.NVarChar(82), `%${params.mesero}%`);
    externalWheres.push('(CAST(u.idmesero AS NVARCHAR(80)) = @p_mesero OR ISNULL(u.meseroNombre, \'\') LIKE @p_meseroLike)');
  }
  if (params.producto) {
    req.input('p_producto', sql.NVarChar(120), `%${params.producto}%`);
    externalWheres.push('(CAST(u.idproducto AS NVARCHAR(80)) LIKE @p_producto OR ISNULL(u.productoNombre, \'\') LIKE @p_producto)');
  }
  if (params.usuario) {
    req.input('p_usuario', sql.NVarChar(80), `%${params.usuario}%`);
    externalWheres.push('u.usuarioAutorizo LIKE @p_usuario');
  }
  if (params.soloSinMotivo) externalWheres.push(`NULLIF(LTRIM(RTRIM(u.motivo)), '') IS NULL`);
  if (params.soloSinAutorizacion) externalWheres.push(`NULLIF(LTRIM(RTRIM(u.usuarioAutorizo)), '') IS NULL`);
  if (params.tipoDescuento) {
    req.input('p_tipoDesc', sql.NVarChar(80), `%${params.tipoDescuento}%`);
    externalWheres.push('(CAST(u.idtipodescuento AS NVARCHAR(80)) LIKE @p_tipoDesc OR ISNULL(td.descripcion, \'\') LIKE @p_tipoDesc)');
  }
  if (params.origenCuenta) {
    req.input('p_origen', sql.NVarChar(80), params.origenCuenta);
    externalWheres.push('u.origenCuenta = @p_origen');
  }

  // Costeo CTE
  const query = `
    WITH CostoProducto AS (
      SELECT
        c.idproducto,
        SUM(
          ISNULL(c.cantidad, 0) *
          COALESCE(
            idet.costopromedio,
            idet.costo,
            base.costopromedio,
            base.costo,
            0
          )
        ) AS costo_unitario
      FROM ${db()}costos c
      LEFT JOIN ${db()}insumosdetalle idet
        ON idet.idinsumo = c.idinsumo
      OUTER APPLY (
        SELECT TOP 1
          ipd.costopromedio,
          ipd.costo
        FROM ${db()}insumospresentaciones ip
        LEFT JOIN ${db()}insumospresentacionesdetalle ipd
          ON ipd.idinsumospresentaciones = ip.idinsumospresentaciones
        WHERE ip.idinsumo = c.idinsumo
      ) base
      GROUP BY c.idproducto
    ),
    UnionData AS (
      ${unionAll}
    )
    SELECT TOP 5000
      u.*,
      ISNULL(cp.costo_unitario, 0) AS costoUnitarioEstimado,
      ${td1Descripcion ? 'td1.descripcion' : (td2Descripcion ? 'td2.descripcion' : 'NULL')} AS tipoDescuentoNombre
    FROM UnionData u
    LEFT JOIN CostoProducto cp ON cp.idproducto = u.idproducto
    ${tTiposDesc1 ? `LEFT JOIN ${db()}tipodescuento td1 ON td1.idtipodescuento = u.idtipodescuento` : ''}
    ${tTiposDesc2 ? `LEFT JOIN ${db()}tiposdescuento td2 ON td2.idtipodescuento = u.idtipodescuento` : ''}
    ${externalWheres.length ? `WHERE ${externalWheres.join(' AND ')}` : ''}
    ORDER BY u.fecha DESC, u.folio DESC
  `;

  const result = await req.query(query);

  let rawData = result.recordset.map(r => {
    const ventaOriginal = r.precioUnitario * r.cantidad;
    let importeDescuento = 0;
    
    // Calcular importe de descuento/cortesía
    if (r.porcentajeDescuento >= 100) {
      importeDescuento = ventaOriginal;
    } else if (r.porcentajeDescuento > 0 && r.porcentajeDescuento < 100) {
      importeDescuento = ventaOriginal * (r.porcentajeDescuento / 100);
    } else if (r.idcortesia > 0 || r.totalCortesiasCuenta > 0 || (r.totalCuenta === 0 && ventaOriginal > 0)) {
       // Si no hay porcentaje pero está marcado como cortesía o cuenta en 0, asumimos 100% de ese producto
       importeDescuento = ventaOriginal;
       r.porcentajeDescuento = 100;
    }

    // Tipo de Auditoría
    let tipoAuditoria = 'DESCONOCIDO';
    if (r.totalCortesiasCuenta > 0 || r.idcortesia > 0) {
      tipoAuditoria = 'CORTESIA_FORMAL';
    } else if (r.porcentajeDescuento >= 100 || (ventaOriginal > 0 && r.totalCuenta === 0)) {
      tipoAuditoria = 'CORTESIA_OPERATIVA';
    } else if (r.porcentajeDescuento > 0 && r.porcentajeDescuento < 100) {
      tipoAuditoria = 'DESCUENTO_AUDITABLE';
    } else if (r.totalDescuentosCuenta > 0) {
      tipoAuditoria = 'DESCUENTO_AUDITABLE';
    }

    // Estatus de Auditoría
    let estatusAuditoria = 'COMPLETO';
    if (!r.usuarioAutorizo || String(r.usuarioAutorizo).trim() === '') estatusAuditoria = 'SIN_USUARIO_AUTORIZADOR';
    else if (!r.motivo || String(r.motivo).trim() === '') estatusAuditoria = 'SIN_MOTIVO';
    else if (r.costoUnitarioEstimado === 0) estatusAuditoria = 'SIN_COSTO';

    const costoTotalEstimado = r.costoUnitarioEstimado * r.cantidad;
    const margenPerdidoEstimado = importeDescuento - costoTotalEstimado;

    return {
      ...r,
      fecha: r.fecha instanceof Date ? toLocalISOStr(r.fecha) : r.fecha,
      ventaOriginal: Math.round(ventaOriginal * 100) / 100,
      importeDescuento: Math.round(importeDescuento * 100) / 100,
      costoTotalEstimado: Math.round(costoTotalEstimado * 100) / 100,
      margenPerdidoEstimado: Math.round(margenPerdidoEstimado * 100) / 100,
      tipoAuditoria,
      estatusAuditoria,
      usuarioAutorizo: r.usuarioAutorizo || 'Sin usuario autorizador',
      motivo: r.motivo || 'Sin motivo',
    };
  });

  // Filtros en JS para los que requieren cálculo de tipoAuditoria
  if (params.tipoAuditoria) {
    rawData = rawData.filter(x => x.tipoAuditoria === params.tipoAuditoria);
  }
  if (params.soloSinCosto) {
    rawData = rawData.filter(x => x.estatusAuditoria === 'SIN_COSTO' || x.costoUnitarioEstimado === 0);
  }

  // Calcular KPIs
  const resumen = {
    totalBeneficiosAuditables: 0,
    totalRegalado: 0, // 100% discount
    totalDescuentosParciales: 0,
    totalDescuento100: 0,
    totalCortesiasFormales: 0,
    totalCortesiasOperativas: 0,
    totalImporteDescontado: 0,
    costoTotalEstimado: 0,
    margenPerdidoEstimado: 0,
    productosRegalados: 0,
    cuentasAfectadas: 0,
    cuentasAbiertasAfectadas: 0,
    cuentasCerradasAfectadas: 0,
    cortesiasSinUsuario: 0,
    cortesiasSinMotivo: 0,
    productosSinCosto: 0,
    porcentajeBeneficiosSobreVentas: 0
  };

  const cuentasSet = new Set();
  const cuentasAbiertasSet = new Set();
  const cuentasCerradasSet = new Set();
  let proxyVentaGlobal = 0; // Para sacar un % base si se requiere, sumamos ventaOriginal - importeDescuento

  rawData.forEach(r => {
    resumen.totalBeneficiosAuditables++;
    resumen.totalImporteDescontado += r.importeDescuento;
    resumen.costoTotalEstimado += r.costoTotalEstimado;
    resumen.margenPerdidoEstimado += r.margenPerdidoEstimado;
    proxyVentaGlobal += (r.ventaOriginal - r.importeDescuento);

    if (r.porcentajeDescuento >= 100) {
      resumen.totalRegalado += r.importeDescuento;
      resumen.totalDescuento100++;
      resumen.productosRegalados += r.cantidad;
    } else {
      resumen.totalDescuentosParciales += r.importeDescuento;
    }

    if (r.tipoAuditoria === 'CORTESIA_FORMAL') resumen.totalCortesiasFormales++;
    if (r.tipoAuditoria === 'CORTESIA_OPERATIVA') resumen.totalCortesiasOperativas++;

    if (r.estatusAuditoria === 'SIN_USUARIO_AUTORIZADOR') resumen.cortesiasSinUsuario++;
    if (r.estatusAuditoria === 'SIN_MOTIVO') resumen.cortesiasSinMotivo++;
    if (r.estatusAuditoria === 'SIN_COSTO' || r.costoUnitarioEstimado === 0) resumen.productosSinCosto++;

    const key = r.origenCuenta + '_' + r.folio;
    if (!cuentasSet.has(key)) {
      cuentasSet.add(key);
      if (r.origenCuenta === 'ABIERTA_TEMPORAL') cuentasAbiertasSet.add(key);
      else cuentasCerradasSet.add(key);
    }
  });

  resumen.cuentasAfectadas = cuentasSet.size;
  resumen.cuentasAbiertasAfectadas = cuentasAbiertasSet.size;
  resumen.cuentasCerradasAfectadas = cuentasCerradasSet.size;
  
  if (proxyVentaGlobal > 0) {
    resumen.porcentajeBeneficiosSobreVentas = (resumen.totalImporteDescontado / proxyVentaGlobal) * 100;
  }

  // Redondeos
  resumen.totalImporteDescontado = Math.round(resumen.totalImporteDescontado * 100) / 100;
  resumen.totalRegalado = Math.round(resumen.totalRegalado * 100) / 100;
  resumen.totalDescuentosParciales = Math.round(resumen.totalDescuentosParciales * 100) / 100;
  resumen.costoTotalEstimado = Math.round(resumen.costoTotalEstimado * 100) / 100;
  resumen.margenPerdidoEstimado = Math.round(resumen.margenPerdidoEstimado * 100) / 100;
  resumen.porcentajeBeneficiosSobreVentas = Math.round(resumen.porcentajeBeneficiosSobreVentas * 100) / 100;

  const groupBy = (field, nameField) => {
    const map = new Map();
    rawData.forEach(r => {
      const key = String(r[field] || 'Sin dato').trim() || 'Sin dato';
      const item = map.get(key) || { nombre: r[nameField] || key, cantidad: 0, importe: 0, costo: 0 };
      item.cantidad += Number(r.cantidad) || 0;
      item.importe += Number(r.importeDescuento) || 0;
      item.costo += Number(r.costoTotalEstimado) || 0;
      map.set(key, item);
    });
    return [...map.values()]
      .sort((a,b) => b.importe - a.importe)
      .slice(0, 15)
      .map(x => ({
        ...x,
        cantidad: Math.round(x.cantidad * 100) / 100,
        importe: Math.round(x.importe * 100) / 100,
        costo: Math.round(x.costo * 100) / 100,
      }));
  };

  return {
    resumen,
    rankings: {
      porProducto: groupBy('idproducto', 'productoNombre'),
      porMesero: groupBy('idmesero', 'meseroNombre'),
      porTurno: groupBy('idturno', 'turnoNombre'),
      porUsuarioAutorizador: groupBy('usuarioAutorizo', 'usuarioAutorizo'),
      porTipoAuditoria: groupBy('tipoAuditoria', 'tipoAuditoria'),
      porOrigenCuenta: groupBy('origenCuenta', 'origenCuenta'),
    },
    detalle: rawData,
    periodo: idturno ? { tipo:'turno', idturno } : { tipo:'fechas', inicio, fin },
    generadoEn: new Date().toISOString(),
  };
}

module.exports = {
  getReporteData,
  getCorteX: operationalCutService.getCorteX,
  getCorteZ: operationalCutService.getCorteZ,
  getOperationalCut: operationalCutService.getOperationalCut,
  getProductosCancelados,
  getProductosIndCancelados, getCancelacionesTurnoActual,
  getCortesias, getCortesiasCatalogos,
  _private: { reportSalesUnion, reportPaymentsUnion, reportDetailsUnion },
};
