import Notifications from "../models/Notifications";
import { sendCampaign } from "./notificationService";
import { syncPendingDebts } from "./operationService";
import { checkAndNotifyUpcomingPayments } from "./upcomingPaymentService";
import { checkAndNotifyOverduePayments } from "./overduePaymentService";
import { generateMonthlyHistory } from "./customerHistoryService";

/**
 * Módulo de tareas programadas (schedulers) del servidor.
 *
 * Contiene tres schedulers independientes:
 *
 * 1. **Notification Scheduler** (`startNotificationScheduler`):
 *    Corre cada 60 segundos. Procesa hasta 10 campañas de notificación con
 *    estado `scheduled` cuya fecha de envío ya pasó. Además, el día 1 de cada
 *    mes genera el historial mensual de clientes.
 *
 * 2. **Debt Scheduler** (`startDebtScheduler`):
 *    Corre cada 5 minutos. Sincroniza operaciones de crédito pendientes con
 *    el sistema externo LA Sistemas (`syncPendingDebts`).
 *
 * 3. **Daily Payment Notifications Scheduler** (`startDailyPaymentNotificationsScheduler`):
 *    Corre una vez por día. Notifica a usuarios sobre pagos próximos y vencidos.
 *    Soporta programarse a una hora específica del día con `scheduleAt`.
 *
 * Todos los schedulers son idempotentes: llamarlos cuando ya están corriendo
 * no tiene efecto. Se pueden detener individualmente con sus funciones `stop*`.
 */

// Referencias a los timers activos para poder detenerlos
let notificationTimer: NodeJS.Timeout | null = null;
let debtTimer: NodeJS.Timeout | null = null;
let upcomingPaymentTimer: NodeJS.Timeout | null = null;
let upcomingPaymentInitialTimeout: NodeJS.Timeout | null = null;

/**
 * Ejecuta la verificación de pagos próximos y vencidos.
 * Función interna compartida por el scheduler diario.
 */
async function runDailyPaymentNotifications() {
  try {
    await checkAndNotifyUpcomingPayments();
    await checkAndNotifyOverduePayments();
  } catch (error) {
    // eslint-disable-next-line no-console
    console.error("Daily Payment Notifications Scheduler error:", error);
  }
}

// Control para evitar que el historial mensual se genere más de una vez por mes
let lastRunMonth: number | null = null;

/**
 * Inicia el scheduler de notificaciones push/campañas.
 *
 * En cada ciclo:
 * - Si es el día 1 del mes y aún no se generó el historial mensual, lo genera.
 * - Busca hasta 10 campañas en estado `scheduled` con `sendAt <= ahora` y las envía.
 *
 * Es idempotente: si ya está corriendo, retorna sin hacer nada.
 *
 * @param intervalMs - Intervalo entre ejecuciones en ms (default: 60.000 = 1 minuto)
 */
export function startNotificationScheduler(intervalMs = 60_000) {
  if (notificationTimer) return; // ya está corriendo
  notificationTimer = setInterval(async () => {
    try {
      const now = new Date();

      // Lógica de historial mensual: ejecutar el día 1 de cada mes a las 00:00 (aprox)
      if (now.getDate() === 1 && now.getMonth() !== lastRunMonth) {
        generateMonthlyHistory(now).catch((err) =>
          console.error("Monthly history failed:", err),
        );
        lastRunMonth = now.getMonth();
      }

      // Obtener campañas programadas cuya fecha de envío ya llegó
      const due = await Notifications.find({
        status: "scheduled",
        sendAt: { $lte: now },
      })
        .sort({ sendAt: 1 })
        .limit(10);

      for (const campaign of due) {
        try {
          await sendCampaign(String(campaign._id));
        } catch (err) {
          // eslint-disable-next-line no-console
          console.error("Scheduled campaign failed:", campaign._id, err);
        }
      }
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error("Notification Scheduler error:", error);
    }
  }, intervalMs);
}

/**
 * Detiene el scheduler de notificaciones y libera el timer.
 */
export function stopNotificationScheduler() {
  if (notificationTimer) clearInterval(notificationTimer as any);
  notificationTimer = null;
}

/**
 * Inicia el scheduler de sincronización de deudas pendientes con LA Sistemas.
 *
 * En cada ciclo llama a `syncPendingDebts` para actualizar el estado de
 * operaciones de crédito que aún no han sido confirmadas.
 * Es idempotente: si ya está corriendo, retorna sin hacer nada.
 *
 * @param intervalMs - Intervalo entre ejecuciones en ms (default: 300.000 = 5 minutos)
 */
export function startDebtScheduler(intervalMs = 300_000) {
  if (debtTimer) return;
  debtTimer = setInterval(async () => {
    try {
      await syncPendingDebts();
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error("Debt Scheduler error:", error);
    }
  }, intervalMs);
}

/**
 * Detiene el scheduler de sincronización de deudas y libera el timer.
 */
export function stopDebtScheduler() {
  if (debtTimer) clearInterval(debtTimer as any);
  debtTimer = null;
}

/** Opciones de programación diaria con hora y minuto específicos */
type DailyScheduleOptions = {
  hour: number;
  minute?: number;
};

/**
 * Calcula los milisegundos que faltan hasta la próxima ejecución a la hora indicada.
 * Si la hora ya pasó hoy, programa para el mismo horario del día siguiente.
 *
 * @param hour - Hora del día (0-23)
 * @param minute - Minuto (default: 0)
 * @returns Milisegundos hasta la próxima ejecución
 */
function getDelayUntilNextRun(hour: number, minute = 0) {
  const now = new Date();
  const nextRun = new Date(now);
  nextRun.setHours(hour, minute, 0, 0);

  // Si el horario ya pasó hoy, mover al día siguiente
  if (nextRun.getTime() <= now.getTime()) {
    nextRun.setDate(nextRun.getDate() + 1);
  }

  return nextRun.getTime() - now.getTime();
}

/**
 * Inicia el scheduler diario de notificaciones de pagos próximos y vencidos.
 *
 * Soporta dos modos:
 * - Sin `scheduleAt`: corre inmediatamente al intervalo indicado.
 * - Con `scheduleAt`: espera hasta la hora configurada del día y luego
 *   corre cada `intervalMs` (ideal para ejecutar exactamente a las 08:00, por ejemplo).
 *
 * Es idempotente: si ya está corriendo, retorna sin hacer nada.
 *
 * @param intervalMs - Intervalo entre ejecuciones en ms (default: 86.400.000 = 24 horas)
 * @param scheduleAt - Hora exacta de ejecución diaria (opcional)
 */
export function startDailyPaymentNotificationsScheduler(
  intervalMs = 86400000,
  scheduleAt?: DailyScheduleOptions,
) {
  if (upcomingPaymentTimer || upcomingPaymentInitialTimeout) return;

  if (!scheduleAt) {
    // Sin hora específica: iniciar el intervalo inmediatamente
    upcomingPaymentTimer = setInterval(
      runDailyPaymentNotifications,
      intervalMs,
    );
    return;
  }

  // Con hora específica: esperar hasta ese momento y luego iniciar el intervalo
  const delayMs = getDelayUntilNextRun(scheduleAt.hour, scheduleAt.minute ?? 0);
  upcomingPaymentInitialTimeout = setTimeout(() => {
    runDailyPaymentNotifications();
    upcomingPaymentTimer = setInterval(
      runDailyPaymentNotifications,
      intervalMs,
    );

    if (upcomingPaymentInitialTimeout) {
      clearTimeout(upcomingPaymentInitialTimeout as any);
      upcomingPaymentInitialTimeout = null;
    }
  }, delayMs);
}

/**
 * Detiene el scheduler diario de pagos, cancelando tanto el timeout inicial
 * como el intervalo recurrente.
 */
export function stopDailyPaymentNotificationsScheduler() {
  if (upcomingPaymentTimer) clearInterval(upcomingPaymentTimer as any);
  if (upcomingPaymentInitialTimeout)
    clearTimeout(upcomingPaymentInitialTimeout as any);
  upcomingPaymentTimer = null;
  upcomingPaymentInitialTimeout = null;
}
