'use strict';

const logger = require('../utils/logger');
const liveNotifications = require('./liveNotificationService');
const { localHour } = require('../utils/localDate');
const { getSnapshotCompleto, invalidarCache, invalidarMesas } = require('./dashboardCacheService');

let io           = null;
let lastHash     = '';
let lastPayload  = null;
let pollTimer    = null;
let isRefreshing = false;
let lastNotifyState = null;
let turnoFondoColumnLoaded = false;
let turnoFondoColumn = '';

// En modo bajo consumo el polling se hace cada 10s en lugar de 5s.
// Reduce la carga de queries SQL en ~50% sin impacto notable en la experiencia.
const LOW_MEMORY = process.env.LOW_MEMORY === 'true' ||
  (() => { try { return require('os').totalmem() / (1024**3) <= 4.5; } catch(_) { return false; } })();
const POLL_INTERVAL_MS = LOW_MEMORY ? 10000 : 5000;

async function getTurnoFondoColumn(pool) {
  if (turnoFondoColumnLoaded) return turnoFondoColumn;
  turnoFondoColumnLoaded = true;
  try {
    const dbName = (process.env.DB_NAME || 'softrestaurant11').replace(/]/g, ']]');
    const r = await pool.request().query(`
      SELECT TOP 1 COLUMN_NAME
      FROM [${dbName}].INFORMATION_SCHEMA.COLUMNS
      WHERE TABLE_SCHEMA='dbo'
        AND TABLE_NAME='turnos'
        AND (
          LOWER(COLUMN_NAME) IN ('fondo','fondocaja','fondo_caja','fondoinicial','fondo_inicial','importeapertura')
          OR LOWER(COLUMN_NAME) LIKE '%fondo%'
        )
      ORDER BY CASE
        WHEN LOWER(COLUMN_NAME) IN ('fondo','fondocaja','fondo_caja','fondoinicial','fondo_inicial','importeapertura') THEN 0
        ELSE 1
      END, COLUMN_NAME
    `);
    const col = r.recordset[0]?.COLUMN_NAME;
    if (col) turnoFondoColumn = `[${String(col).replace(/]/g, ']]')}]`;
  } catch (_) {
    turnoFondoColumn = '';
  }
  return turnoFondoColumn;
}

function init(socketIoInstance) {
  io = socketIoInstance;

  io.on('connection', (socket) => {
    const clientsNow = io.engine.clientsCount;
    logger.info(`✅ WS conectado: ${socket.id} (total: ${clientsNow})`);

    // Enviar último estado conocido inmediatamente al cliente recién conectado
    if (lastPayload) socket.emit('actualizacion', lastPayload);

    // Reanudar polling si estaba pausado por falta de clientes
    if (!pollTimer) {
      logger.info('▶️  Reanudando polling (primer cliente conectado)');
      startPolling();
    }

    socket.on('disconnect', () => {
      // clientsCount ya decrementó en este punto
      const remaining = io.engine.clientsCount;
      logger.info(`❌ WS desconectado: ${socket.id} (quedan: ${remaining})`);

      // Pausar polling en horas valle para no hacer queries inútiles a SQL Server
      if (remaining <= 0) {
        logger.info('⏸️  Pausando polling (sin clientes conectados)');
        stopPolling();
      }
    });
  });

  // El snapshot completo es costoso y no debe competir con el agente Cloud
  // cuando nadie tiene abierto el Monitor local. El primer cliente WebSocket
  // lo reactiva inmediatamente desde el handler de connection.
  if (process.env.ARKHON_BACKGROUND_SOCKET_POLLING === 'true') {
    startPolling();
  } else {
    logger.info('Polling WebSocket en pausa hasta que se conecte el primer cliente local');
  }
}

async function pollDB(forceEmit = false) {
  if (isRefreshing) {
    if (forceEmit && lastPayload && io) io.emit('actualizacion', lastPayload);
    return lastPayload;
  }

  isRefreshing = true;
  // Safety de observabilidad: no liberar isRefreshing mientras las consultas
  // anteriores siguen vivas; Promise.race no puede cancelarlas.
  const _safetyTimer = setTimeout(() => {
    if (isRefreshing) {
      logger.warn('⚠️  pollDB lento (>20s) — se mantiene el ciclo único hasta finalizar');
    }
  }, 20000);

  try {
    if (forceEmit) invalidarCache();

    // mssql ya aplica requestTimeout. Esperar el lote real evita promesas SQL
    // huérfanas y garantiza que sólo exista un snapshot activo.
    const snapshot = await getSnapshotCompleto();
    const { resumen, mesas, ventas_por_hora, cancelaciones_turno, domicilios_activos, movimientos_caja,
            top_productos, top_productos_curso, ventas_servicio, formas_pago } = snapshot;

    // ── Hash de cambios ───────────────────────────────────────────────────────
    const ventaHoraActual = (ventas_por_hora || []).find(h => h.hora === localHour())?.venta ?? 0;
    const mesasHash       = (mesas || []).map(m => `${m.mesa}:${m.foliocuenta}:${m.total_actual||m.total}:${Number(m.impreso||0)}:${Number(m.pagado||0)}`).join('|');
    const domHash         = (domicilios_activos || []).map(d => [
      d.folio, d.status_domicilio, d.total, Number(d.impreso || 0),
      Number(d.impresiones || 0), Number(d.pagado || 0), d.idcliente || '',
      d.repartidor_id || '', d.repartidor || '', d.iddireccion || '',
    ].join(':')).join('|');
    const movHash         = movimientos_caja?.resumen_hoy
      ? `${movimientos_caja.resumen_hoy.total_retiros_hoy}|${movimientos_caja.resumen_hoy.total_depositos_hoy}`
      : '0|0';
    const cancelHash      = cancelaciones_turno
      ? `${cancelaciones_turno.total_cancelaciones}|${cancelaciones_turno.importe_cancelado}|${cancelaciones_turno.ultima_cancelacion?.fecha ?? ''}|${(cancelaciones_turno.detalle_productos||[]).length}|${(cancelaciones_turno.cuentas_canceladas||[]).length}`
      : '0|0||0|0';

    // BUG-FIX: top_productos y top_productos_curso NO estaban en el hash.
    // Escenario roto: se vende producto nuevo pero venta_pagada no cambia aún
    // (mesa abierta) → hash idéntico → no se emite → pantalla "En curso" nunca actualiza.
    // Solución: incluir el top-5 de cada lista + ventas_servicio + formas_pago en el hash.
    const prodHash     = (top_productos      || []).slice(0,10).map(p => `${p.nombre||p.descripcion}:${p.cantidad}:${p.total}`).join('|');
    const cursoHash    = (top_productos_curso || []).slice(0,10).map(p => `${p.nombre||p.descripcion}:${p.cantidad}:${p.total}`).join('|');
    const servicioHash = (ventas_servicio     || []).map(s => `${s.servicio}:${s.total}`).join('|');
    const formasHash   = (formas_pago         || []).map(f => `${f.forma_pago}:${f.total}`).join('|');

    // Detectar turnos cerrados — query ligera
    let turnoHash = '';
    let turnoOpenHash = '';
    let turnoOpenFondo = 0;
    try {
      const { getPool: gp } = require('../db');
      const { db: dbFn } = require('../utils/dbName');
      const pool2 = await gp();
      const fondoColumn = await getTurnoFondoColumn(pool2);
      const fondoSelect = fondoColumn ? `, ISNULL(TRY_CONVERT(float, ${fondoColumn}), 0) AS fondo` : ', 0 AS fondo';
      const open = await pool2.request().query(
        `SELECT TOP 1 idturno, apertura ${fondoSelect} FROM ${dbFn()}turnos WHERE apertura IS NOT NULL ORDER BY apertura DESC`
      );
      if (open.recordset[0]) {
        turnoOpenHash = `${open.recordset[0].idturno}:${open.recordset[0].apertura}`;
        turnoOpenFondo = Number(open.recordset[0].fondo || 0);
      }
      const trn = await pool2.request().query(
        `SELECT TOP 1 idturno, cierre FROM ${dbFn()}turnos WHERE cierre IS NOT NULL ORDER BY cierre DESC`
      );
      turnoHash = trn.recordset[0] ? `${trn.recordset[0].idturno}:${trn.recordset[0].cierre}` : '';
    } catch(_) {}

    // Layout: [0]=venta_pagada [1]=venta_en_curso [2]=cuentas_pagadas [3]=mesas...
    const currentHash = [
      resumen.venta_pagada,
      resumen.venta_en_curso,
      resumen.cuentas_pagadas,
      mesas.length,
      mesasHash,
      ventaHoraActual,
      cancelHash,
      domHash,
      movHash,
      prodHash,      // BUG-FIX: productos vendidos (cuentas cerradas)
      cursoHash,     // BUG-FIX: productos en curso (mesas abiertas)
      servicioHash,  // BUG-FIX: ventas por tipo de servicio
      formasHash,    // BUG-FIX: formas de pago
      turnoHash,     // Detectar cierre de turno
      turnoOpenHash, // Detectar apertura de turno
    ].join('|');

    // Invalidar caché si cambiaron ventas o cuentas cerradas
    if (lastHash) {
      const prev = lastHash.split('|');
      if (String(resumen.venta_pagada) !== prev[0] || String(resumen.cuentas_pagadas) !== prev[2]) {
        invalidarCache();
      }
      // P3-E FIX: Invalidar caché de mesas cuando solo cambia el estado de mesas
      // (sin que cambien venta_pagada o cuentas_pagadas — ej: nueva mesa abierta)
      if (String(mesas.length) !== prev[3] || mesasHash !== prev[4]) {
        invalidarMesas();
      }
      // BUG-FIX: Invalidar caché completo cuando cambia venta_en_curso o productos en curso.
      // Escenario: se agrega producto a mesa abierta → venta_en_curso sube pero venta_pagada
      // no cambió → la caché de top_productos_curso quedaba stale → pantalla no actualizaba.
      if (String(resumen.venta_en_curso) !== prev[1] || cursoHash !== prev[10]) {
        invalidarCache();
      }
    }

    const hayCambio = currentHash !== lastHash;
    const pico      = snapshot.ventas_por_hora_metricas?.horaPico;
    const cancelPartsForNotify = cancelHash.split('|');
    const movPartsForNotify = movHash.split('|');
    const currentNotifyState = {
      ventaPagada: Number(resumen.venta_pagada || 0),
      cuentasPagadas: Number(resumen.cuentas_pagadas || 0),
      totalCortesias: Number(resumen.total_cortesias || 0),
      cancelProductos: Number(cancelPartsForNotify[0] || 0),
      cancelImporte: Number(cancelPartsForNotify[1] || 0),
      cancelCuentas: Number(cancelPartsForNotify[4] || 0),
      retiros: Number(movPartsForNotify[0] || 0),
      depositos: Number(movPartsForNotify[1] || 0),
      turnoHash,
      turnoOpenHash,
      turnoOpenFondo,
    };

    // ── NOTIFICACIONES EN VIVO ──────────────────────────────────────────
    // Se emiten por Socket.IO al instante del polling y, si hay Web Push
    // configurado, también llegan al sistema operativo/navegador con permiso
    // del usuario. No reemplaza WhatsApp ni rompe el flujo existente.
    if (lastNotifyState && hayCambio && process.env.NOTIFICATIONS_ENABLED !== 'false') {
      try {
        const prevState = lastNotifyState;
        const currState = currentNotifyState;
        if (currState.cuentasPagadas > prevState.cuentasPagadas) {
          liveNotifications.notify({
            type: 'cuenta_cerrada',
            amount: currState.ventaPagada - prevState.ventaPagada,
            sourceTable: 'cheques',
            data: { nuevas: currState.cuentasPagadas - prevState.cuentasPagadas },
          }).catch(_ => {});
        }
        if (currState.cancelProductos > prevState.cancelProductos) {
          const ultima = cancelaciones_turno?.ultima_cancelacion;
          liveNotifications.notify({
            type: 'producto_cancelado',
            amount: ultima?.importe || cancelaciones_turno?.importe_cancelado || 0,
            folio: ultima?.folio,
            numcheque: ultima?.numcheque,
            fecha: ultima?.fecha,
            usuario: ultima?.usuario,
            sourceTable: 'cancela',
            data: { producto: ultima?.producto || '', total_cancelaciones: currState.cancelProductos },
          }).catch(_ => {});
        }
        if (currState.cancelCuentas > prevState.cancelCuentas) {
          const ultimaCuenta = (cancelaciones_turno?.cuentas_canceladas || [])[0];
          liveNotifications.notify({
            type: 'cancelacion_cuenta',
            amount: ultimaCuenta?.total || 0,
            folio: ultimaCuenta?.folio,
            numcheque: ultimaCuenta?.numcheque,
            fecha: ultimaCuenta?.fechacancelado || ultimaCuenta?.fecha,
            motivo: ultimaCuenta?.razoncancelado || 'Sin motivo',
            sourceTable: 'cheques',
            data: { cuentas_canceladas: currState.cancelCuentas },
          }).catch(_ => {});
        }
        const ultimoMov = movimientos_caja?.movimientos?.[0];
        if (currState.retiros > prevState.retiros) {
          liveNotifications.notify({
            type: 'retiro_caja',
            amount: ultimoMov?.tipo_label === 'retiro' ? ultimoMov.importe : currState.retiros - prevState.retiros,
            idmovto: ultimoMov?.folio,
            idturno: ultimoMov?.idturno,
            fecha: ultimoMov?.fecha,
            usuario: ultimoMov?.usuario,
            motivo: ultimoMov?.concepto || 'Sin concepto',
            sourceTable: 'movtoscaja',
            data: { total_retiros_hoy: currState.retiros, movimiento: ultimoMov || null },
          }).catch(_ => {});
        }
        if (currState.depositos > prevState.depositos) {
          liveNotifications.notify({
            type: 'deposito_caja',
            amount: ultimoMov?.tipo_label === 'deposito' ? ultimoMov.importe : currState.depositos - prevState.depositos,
            idmovto: ultimoMov?.folio,
            idturno: ultimoMov?.idturno,
            fecha: ultimoMov?.fecha,
            usuario: ultimoMov?.usuario,
            motivo: ultimoMov?.concepto || 'Sin concepto',
            sourceTable: 'movtoscaja',
            data: { total_depositos_hoy: currState.depositos, movimiento: ultimoMov || null },
          }).catch(_ => {});
        }
        if (currState.totalCortesias > prevState.totalCortesias) {
          liveNotifications.notify({
            type: 'cortesia',
            amount: currState.totalCortesias - prevState.totalCortesias,
            sourceTable: 'cheqdet',
            data: { total_cortesias: currState.totalCortesias },
          }).catch(_ => {});
        }
        if (currState.turnoOpenHash && currState.turnoOpenHash !== prevState.turnoOpenHash) {
          const turnoId = currState.turnoOpenHash.split(':')[0];
          if (turnoId) {
            liveNotifications.notify({
              type: 'turno_abierto',
              idturno: turnoId,
              fecha: currState.turnoOpenHash.split(':').slice(1).join(':'),
              fondo: currState.turnoOpenFondo,
              sourceTable: 'turnos',
              data: { idturno: turnoId, fondo: currState.turnoOpenFondo },
            }).catch(_ => {});
          }
        }
        if (currState.turnoHash && currState.turnoHash !== prevState.turnoHash) {
          const turnoId = currState.turnoHash.split(':')[0];
          if (turnoId) {
            liveNotifications.notify({
              type: 'turno_cerrado',
              amount: currState.ventaPagada,
              idturno: turnoId,
              fecha: currState.turnoHash.split(':').slice(1).join(':'),
              sourceTable: 'turnos',
              data: { idturno: turnoId, venta_pagada: currState.ventaPagada },
            }).catch(_ => {});
          }
        }

        try {
          const wa = require('./whatsappService');
          if (wa.isConfigured()) {
            if (currState.cancelCuentas > prevState.cancelCuentas || currState.cancelProductos > prevState.cancelProductos) {
              wa.alertaCancelaciones().catch(_ => {});
              wa.alertaProductosCancelados().catch(_ => {});
            }
            if (currState.retiros !== prevState.retiros || currState.depositos !== prevState.depositos) {
              wa.alertaRetiros().catch(_ => {});
              wa.alertaDepositos().catch(_ => {});
            }
            if (currState.turnoHash && currState.turnoHash !== prevState.turnoHash) {
              const turnoId = currState.turnoHash.split(':')[0];
              if (turnoId) wa.notificarCierreTurno(turnoId).catch(_ => {});
            }
          }
        } catch (_) {}
      } catch(err) {
        logger.warn({ event: 'live_notification_detect_error', message: err.message });
      }
    }

    if (false && lastHash && hayCambio && process.env.NOTIFICATIONS_ENABLED !== 'false') {
      try {
        const prev = lastHash.split('|');
        const money = n => '$' + Number(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

        const prevCuentas = Number(prev[2] || 0);
        const currCuentas = Number(resumen.cuentas_pagadas || 0);
        if (currCuentas > prevCuentas) {
          const nuevas = currCuentas - prevCuentas;
          liveNotifications.notify({
            type: 'cuenta_cerrada',
            title: nuevas === 1 ? 'Cuenta cerrada' : `${nuevas} cuentas cerradas`,
            body: `Venta pagada acumulada: ${money(resumen.venta_pagada)}`,
            url: '/',
            priority: 'normal',
            data: { nuevas, cuentas_pagadas: currCuentas, venta_pagada: resumen.venta_pagada },
          }).catch(_ => {});
        }

        const prevCancelParts = (prev[6] || '0|0||0|0').split('|');
        const currCancelParts = cancelHash.split('|');
        const prevCancelProductos = Number(prevCancelParts[0] || 0);
        const currCancelProductos = Number(currCancelParts[0] || 0);
        const prevCancelCuentas = Number(prevCancelParts[4] || 0);
        const currCancelCuentas = Number(currCancelParts[4] || 0);

        if (currCancelProductos > prevCancelProductos) {
          const ultima = cancelaciones_turno?.ultima_cancelacion;
          liveNotifications.notify({
            type: 'cancelacion_producto',
            title: 'Cancelación registrada',
            body: ultima
              ? `${ultima.producto || 'Producto'} · ${money(ultima.importe || 0)} · ${ultima.usuario || 'Sin usuario'}`
              : `Importe cancelado acumulado: ${money(cancelaciones_turno?.importe_cancelado || 0)}`,
            url: '/cancelaciones',
            priority: 'alta',
            data: { total_cancelaciones: currCancelProductos },
          }).catch(_ => {});
        }

        if (currCancelCuentas > prevCancelCuentas) {
          const ultimaCuenta = (cancelaciones_turno?.cuentas_canceladas || [])[0];
          liveNotifications.notify({
            type: 'cancelacion_cuenta',
            title: 'Cuenta cancelada',
            body: ultimaCuenta
              ? `Folio ${ultimaCuenta.folio || ''} · Mesa ${ultimaCuenta.mesa || ''} · ${money(ultimaCuenta.total || 0)}`
              : `Cuentas canceladas en turno: ${currCancelCuentas}`,
            url: '/cancelaciones',
            priority: 'alta',
            data: { cuentas_canceladas: currCancelCuentas },
          }).catch(_ => {});
        }

        const prevMov = (prev[8] || '0|0').split('|');
        const currMov = movHash.split('|');
        const prevRetiros = Number(prevMov[0] || 0);
        const prevDepositos = Number(prevMov[1] || 0);
        const currRetiros = Number(currMov[0] || 0);
        const currDepositos = Number(currMov[1] || 0);
        const ultimoMov = movimientos_caja?.movimientos?.[0];

        if (currRetiros > prevRetiros) {
          liveNotifications.notify({
            type: 'retiro_efectivo',
            title: 'Retiro de efectivo registrado',
            body: ultimoMov?.tipo_label === 'retiro'
              ? `${money(ultimoMov.importe)} · ${ultimoMov.concepto || 'Sin concepto'}`
              : `Total retiros hoy: ${money(currRetiros)}`,
            url: '/movimientos-caja',
            priority: 'alta',
            data: { total_retiros_hoy: currRetiros, movimiento: ultimoMov || null },
          }).catch(_ => {});
        }

        if (currDepositos > prevDepositos) {
          liveNotifications.notify({
            type: 'deposito_efectivo',
            title: 'Depósito de efectivo registrado',
            body: ultimoMov?.tipo_label === 'deposito'
              ? `${money(ultimoMov.importe)} · ${ultimoMov.concepto || 'Sin concepto'}`
              : `Total depósitos hoy: ${money(currDepositos)}`,
            url: '/movimientos-caja',
            priority: 'normal',
            data: { total_depositos_hoy: currDepositos, movimiento: ultimoMov || null },
          }).catch(_ => {});
        }

        const prevTurno = prev[13] || '';
        if (turnoHash && turnoHash !== prevTurno) {
          const turnoId = turnoHash.split(':')[0];
          if (turnoId) {
            liveNotifications.notify({
              type: 'turno_cerrado',
              title: 'Turno cerrado',
              body: `Turno ${turnoId} cerrado. Venta pagada: ${money(resumen.venta_pagada)}`,
              url: '/corte-z',
              priority: 'alta',
              data: { idturno: turnoId, venta_pagada: resumen.venta_pagada },
            }).catch(_ => {});
          }
        }

        try {
          const wa = require('./whatsappService');
          if (wa.isConfigured()) {
            if (currCancelCuentas > prevCancelCuentas || currCancelProductos > prevCancelProductos) {
              wa.alertaCancelaciones().catch(_ => {});
              wa.alertaProductosCancelados().catch(_ => {});
            }
            if (currRetiros !== prevRetiros || currDepositos !== prevDepositos) {
              wa.alertaRetiros().catch(_ => {});
              wa.alertaDepositos().catch(_ => {});
            }
            if (turnoHash && turnoHash !== prevTurno) {
              const turnoId = turnoHash.split(':')[0];
              if (turnoId) wa.notificarCierreTurno(turnoId).catch(_ => {});
            }
          }
        } catch (_) {}
      } catch(err) {
        logger.warn({ event: 'live_notification_detect_error', message: err.message });
      }
    }
    logger.debug(
      `⏱ poll [${new Date().toLocaleTimeString('es-MX')}] ` +
      `Pagado:$${resumen.venta_pagada} Curso:$${resumen.venta_en_curso} ` +
      `Mesas:${mesas.length} ` +
      (pico ? `Pico:${String(pico.hora).padStart(2,'0')}h $${pico.venta} ` : '') +
      `Cambio:${hayCambio} Clientes:${io?.engine?.clientsCount ?? 0}`
    );

    if (hayCambio || forceEmit) {
      lastHash    = currentHash;
      lastPayload = snapshot;
      lastNotifyState = currentNotifyState;
      if (io) io.emit('actualizacion', snapshot);
      try {
        require('../agent/liveSnapshotService').pushLiveSnapshot(snapshot);
      } catch (_) {}
      logger.debug(`📡 Emit → ${io?.engine?.clientsCount ?? 0} cliente(s)`);
    }

    return lastPayload;

  } catch (err) {
    logger.error('❌ Error en polling', { message: err.message, stack: err.stack });
    if (io) io.emit('dashboard-error', { mensaje: 'Error interno al actualizar datos' });
    return null;
  } finally {
    clearTimeout(_safetyTimer);
    isRefreshing = false;
  }
}

function startPolling() {
  if (process.env.ARKHON_SOCKET_POLLING === 'false') {
    stopPolling();
    logger.info('Polling WebSocket desactivado por configuracion; los modulos usan sus APIs bajo demanda');
    return;
  }
  if (pollTimer) clearInterval(pollTimer);
  pollTimer = setInterval(() => pollDB(false), POLL_INTERVAL_MS);
  pollDB(false);
  logger.info(`📡 Polling cada ${POLL_INTERVAL_MS / 1000}s`);
}

function stopPolling() {
  if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
}

function getLastPayload() { return lastPayload; }

module.exports = { init, pollDB, getLastPayload, startPolling, stopPolling };
