// src/server/features/notifications/notifications.service.ts
// Business logic for notifications management with aggregation

import { db } from '@serverShared/db/client';
import { pendingNotifications } from './db/pending_notifications.table';
import { shifts } from '../schedule/db/shifts.table';
import { eq, and, isNull, gte, lt, or } from 'drizzle-orm';
import { randomUUID } from 'node:crypto';
import { notifyUser } from '@serverShared/plugins/socket';
import { loggerService } from '@serverShared/lib/logger';
import { EVENTS } from '@shared/contracts/events';
import { getNotificationMessage, prepareNotificationFields } from '@shared/utils/formatters';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import timezone from 'dayjs/plugin/timezone.js';

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault('Europe/Moscow');

const notificationsLogger = loggerService.get('DB').child({ module: 'notifications' });

function extractNotificationNoteId(data: unknown): string | null {
  try {
    const parsed = typeof data === 'string' ? JSON.parse(data) : data;
    const d = (parsed || {}) as any;
    return d?.noteId || d?.id || d?.targetId || d?.link?.query?.noteId || null;
  } catch {
    return null;
  }
}

/**
 * Подсчитать количество несогласованных смен за месяц
 * Несогласованная смена: isApproved = false (любой статус, включая Blank)
 */
async function getUnapprovedShiftsCount(month: number, year: number): Promise<number> {
  try {
    // Используем dayjs для создания дат, чтобы избежать проблем с локальным временем сервера
    const monthDate = dayjs().tz('Europe/Moscow').year(year).month(month - 1).date(1);
    const startDate = monthDate.startOf('month').utc().toDate();
    const endDate = monthDate.add(1, 'month').startOf('month').utc().toDate();

    const result = await db
      .select()
      .from(shifts)
      .where(
        and(
          eq(shifts.isApproved, false),
          // isApproved = false (несогласованная смена, любой статус включая Blank)
          gte(shifts.date, startDate),
          lt(shifts.date, endDate)
          // Дата смены в указанном месяце
        )
      );

    return result.length;
  } catch (error) {
    notificationsLogger.error('Error getting unapproved shifts count:', error);
    return 0;
  }
}

/**
 * Подготовить данные уведомления для отправки через сокет
 * Добавляет поля isGrouped, monthName и link для approval:created событий
 * Подсчитывает реальное количество несогласованных смен за месяц
 * ИСПОЛЬЗУЕТ getNotificationMessage ИЗ @shared/utils/formatters (Единый источник правды)
 */
async function prepareNotificationData(event: string, data: any): Promise<any> {
  // Используем getNotificationMessage из @shared/utils/formatters для формирования текста уведомления
  const message = getNotificationMessage(event, data);

  // Получаем дополнительные поля из @shared/utils/formatters
  const additionalFields = prepareNotificationFields(event, data);

  // Для approval:created событий подсчитываем реальное количество несогласованных смен
  let count = data.count;
  if (event === 'approval:created' && data.month && data.year) {
    count = await getUnapprovedShiftsCount(data.month, data.year);
  }

  // Возвращаем данные с обновленным сообщением и дополнительными полями
  return {
    ...data,
    message,
    ...additionalFields,
    count,
  };
}

/**
 * Notify a user with aggregation support
 * If an unread notification with the same event and targetId exists, it will be updated (count incremented)
 * Otherwise, a new notification will be created
 */
export async function notify({
  userId,
  event,
  data,
  targetId,
  tx,
}: {
  userId: string;
  event: string;
  data: any;
  targetId?: string;
  tx?: any;
}): Promise<void> {
  try {
    const client = tx || db;
    // Debug log: Notify call
    notificationsLogger.debug(`[Debug] Notify call: user=${userId}, event=${event}, targetId=${targetId}`);

    // For mention:notification events, always create a new notification (no aggregation)
    // Each mention should be a separate alert
    if (event === EVENTS.MENTION_NOTIFICATION) {
      const notificationId = randomUUID();
      const roomId = `room:user_${userId}`;

      notificationsLogger.debug(`[Debug] Creating new mention notification ${notificationId}, roomId=${roomId}, userId=${userId}`);
      notificationsLogger.debug(`[Debug] Mention notification data:`, JSON.stringify(data, null, 2));

      // Подготавливаем данные для mention:notification
      const preparedData = await prepareNotificationData(event, {
        ...data,
        targetId: targetId,
      });

      notificationsLogger.debug(`[Debug] Prepared notification data:`, JSON.stringify(preparedData, null, 2));

      // Создаем новое уведомление
      await client.insert(pendingNotifications).values({
        id: notificationId,
        userId,
        roomId,
        event,
        data: preparedData,
        createdAt: new Date(),
      });

      notificationsLogger.info(`Mention notification created: ${event} for user ${userId}`);
      notificationsLogger.debug(`[Debug] Mention notification successfully inserted into DB for user ${userId}`);

      notificationsLogger.debug(`[Service Debug] Calling notifyUser for ${userId}`);

      // Send real-time notification
      try {
        await notifyUser(userId, 'notification:new', {
          id: notificationId,
          event,
          data: preparedData,
          readAt: null,
          createdAt: new Date().toISOString(),
        });
        notificationsLogger.debug(`[Debug] Socket notifyUser called for ${userId}`);
      } catch (socketError) {
        notificationsLogger.warn(`Socket notification failed for user ${userId} (mention:notification):`, socketError);
        // Do not throw - DB operation was successful
      }
      return;
    }

    // Check for existing unread notification with same event, userId, and targetId
    // For approval events, aggregate by event + month + year to prevent merging different months
    const allExistingNotifications = await client
      .select()
      .from(pendingNotifications)
      .where(
        and(
          eq(pendingNotifications.userId, userId),
          eq(pendingNotifications.event, event),
          isNull(pendingNotifications.readAt)
        )
      )
      .orderBy(pendingNotifications.createdAt);

    // Filter by targetId for non-approval events, or by month/year for approval events
    // Для approval:created событий игнорируем targetId и ищем строго по userId, event и month/year в data
    let existingNotifications = allExistingNotifications;
    if (event === 'approval:created') {
      // Для approval:created событий агрегируем по месяцу и году, игнорируя targetId
      const month = data.month;
      const year = data.year;

      notificationsLogger.info(`[Debug] Filtering approval:created notifications for month=${month}, year=${year}`);
      notificationsLogger.info(`[Debug] All existing notifications:`, allExistingNotifications.map((n: any) => ({
        id: n.id,
        month: (n.data as any)?.month,
        year: (n.data as any)?.year,
      })));

      if (month !== undefined && year !== undefined) {
        existingNotifications = allExistingNotifications.filter((n: any) => {
          const notificationMonth = (n.data as any)?.month;
          const notificationYear = (n.data as any)?.year;
          return notificationMonth === month && notificationYear === year;
        });

        notificationsLogger.info(`[Debug] Filtered notifications:`, existingNotifications.length);
      }
    } else {
      // Для остальных событий используем targetId
      if (targetId !== undefined) {
        existingNotifications = allExistingNotifications.filter((n: any) => {
          const notificationTargetId = (n.data as any)?.targetId;
          return notificationTargetId === targetId;
        });
      }
    }

    if (existingNotifications.length > 0) {
      // Update existing notification
      const existing = existingNotifications[0];

      notificationsLogger.debug(`[Debug] Updating existing notification ${existing.id}`);

      // Safe JSON Parse: если existing.data является строкой, применяем JSON.parse()
      const existingData = typeof existing.data === 'string' ? JSON.parse(existing.data) : existing.data;

      // Подготавливаем данные с полями isGrouped, monthName и link ДО сохранения в БД
      // Для approval:created событий prepareNotificationData сам пересчитает count через getUnapprovedShiftsCount
      const preparedData = await prepareNotificationData(event, {
        ...existingData,
        ...data,
      });

      // Если для approval:created нет несогласованных смен, удаляем уведомление
      if (event === 'approval:created' && preparedData.count === 0) {
        await client
          .delete(pendingNotifications)
          .where(eq(pendingNotifications.id, existing.id));
        
        notificationsLogger.info(`Notification deleted (no unapproved shifts): ${event} for user ${userId}`);
        notificationsLogger.debug(`[Debug] Notification ${existing.id} deleted (no unapproved shifts)`);

        // Отправляем сокет-событие с обновленным объектом, чтобы фронтенд мгновенно убрал уведомление из списка
        try {
          await notifyUser(userId, 'notification:new', {
            id: existing.id,
            event,
            data: preparedData,
            readAt: null,
            createdAt: existing.createdAt.toISOString(),
          });
        } catch (socketError) {
          notificationsLogger.warn(`Socket notification failed for user ${userId} (approval:created - delete):`, socketError);
          // Do not throw - DB operation was successful
        }

        return;
      }

      await client
        .update(pendingNotifications)
        .set({
          data: preparedData, // Используем подготовленные данные с isGrouped, monthName, link
          readAt: null, // Сбрасываем readAt, чтобы пометить как непрочитанное при обновлении
        })
        .where(eq(pendingNotifications.id, existing.id));

      notificationsLogger.info(`Notification aggregated: ${event} for user ${userId}, count: ${preparedData.count}`);

      notificationsLogger.debug(`[Service Debug] Calling notifyUser for ${userId}`);

      // Send real-time notification with updated count
      try {
        await notifyUser(userId, 'notification:new', {
          id: existing.id,
          event,
          data: preparedData,
          readAt: null,
          createdAt: existing.createdAt.toISOString(),
        });
        notificationsLogger.debug(`[Debug] Socket notifyUser called for ${userId}`);
      } catch (socketError) {
        notificationsLogger.warn(`Socket notification failed for user ${userId} (approval:created - update):`, socketError);
        // Do not throw - DB operation was successful
      }
    } else {
      // Create new notification
      // Для approval:created используем виртуальный ID group_${month}_${year} для согласованности с API
      const month = data.month;
      const year = data.year;
      const notificationId = (event === 'approval:created' && month && year)
        ? `group_${month}_${year}`
        : randomUUID();
      const roomId = `room:user_${userId}`;

      notificationsLogger.debug(`[Debug] Creating new notification ${notificationId}, roomId=${roomId}, userId=${userId}`);
      
      // Подготавливаем данные с полями isGrouped, monthName и link ДО сохранения в БД
      // Для approval:created событий prepareNotificationData сам пересчитает count через getUnapprovedShiftsCount
      const preparedData = await prepareNotificationData(event, {
        ...data,
        targetId: targetId, // Сохраняем для будущей фильтрации
      });
      
      // Если для approval:created нет несогласованных смен, удаляем из БД и отправляем сокет с count: 0
      if (event === 'approval:created' && preparedData.count === 0) {
        notificationsLogger.info(`Notification not created (no unapproved shifts): ${event} for user ${userId}`);
        notificationsLogger.debug(`[Debug] Notification ${notificationId} not created (no unapproved shifts)`);

        // Превентивная очистка базы по составному ID
        await client
          .delete(pendingNotifications)
          .where(eq(pendingNotifications.id, notificationId));

        notificationsLogger.info(`Notification deleted from DB (no unapproved shifts): ${notificationId}`);
        notificationsLogger.debug(`[Debug] Notification ${notificationId} deleted from DB`);

        // Отправляем сокет-событие с count: 0, чтобы фронтенд мог удалить уведомление
        try {
          await notifyUser(userId, 'notification:new', {
            id: notificationId,
            event,
            data: preparedData,
            readAt: null,
            createdAt: new Date().toISOString(),
          });
        } catch (socketError) {
          notificationsLogger.warn(`Socket notification failed for user ${userId} (approval:created - no shifts):`, socketError);
          // Do not throw - DB operation was successful
        }

        return;
      }
      
      // Атомарный upsert: insert или update если существует
      // Используем onDuplicateKeyUpdate для гарантии отсутствия дублей
      notificationsLogger.debug(`[Debug] Upserting notification ${notificationId}`);
      await client.insert(pendingNotifications)
        .values({
          id: notificationId,
          userId,
          roomId,
          event,
          data: preparedData, // Используем подготовленные данные с isGrouped, monthName, link
          createdAt: new Date(),
        })
        .onDuplicateKeyUpdate({
          set: {
            data: preparedData,
            readAt: null, // Сбрасываем readAt при upsert, чтобы пометить как непрочитанное
            createdAt: new Date(),
          },
        });

      notificationsLogger.info(`Notification upserted: ${event} for user ${userId}`);
      
      notificationsLogger.debug(`[Service Debug] Calling notifyUser for ${userId}`);

      // Send real-time notification
      try {
        await notifyUser(userId, 'notification:new', {
          id: notificationId,
          event,
          data: preparedData,
          readAt: null,
          createdAt: new Date().toISOString(),
        });
        notificationsLogger.debug(`[Debug] Socket notifyUser called for ${userId}`);
      } catch (socketError) {
        notificationsLogger.warn(`Socket notification failed for user ${userId} (general notification):`, socketError);
        // Do not throw - DB operation was successful
      }
    }
  } catch (error) {
    notificationsLogger.error('Error in notify:', error);
    throw error;
  }
}

/**
 * Notify multiple users with the same notification
 */
export async function notifyMultipleUsers({
  userIds,
  event,
  data,
  targetId,
  tx,
}: {
  userIds: string[];
  event: string;
  data: any;
  targetId?: string;
  tx?: any;
}): Promise<void> {
  await Promise.all(
    userIds.map((userId) => notify({ userId, event, data, targetId, tx }))
  );
}

/**
 * Get unread notifications count for a user
 */
export async function getUnreadCount(userId: string): Promise<number> {
  try {
    const result = await db
      .select({ count: pendingNotifications.id })
      .from(pendingNotifications)
      .where(
        and(
          eq(pendingNotifications.userId, userId),
          isNull(pendingNotifications.readAt)
        )
      );
    
    return result.length;
  } catch (error) {
    notificationsLogger.error('Error getting unread count:', error);
    return 0;
  }
}

/**
 * Get list of user IDs who already received a notification for a specific target and event
 * This method is used to avoid duplicate notifications (e.g., personal mention + admin notification)
 * 
 * @param targetId - The ID of the target (e.g., noteId for comments)
 * @param event - The event type (e.g., 'mention:notification')
 * @returns Set of user IDs who already received the notification
 */
export async function getNotifiedUsersForTarget(
  targetId: string,
  event: string
): Promise<Set<string>> {
  try {
    const existingNotifications = await db
      .select()
      .from(pendingNotifications)
      .where(
        and(
          eq(pendingNotifications.event, event),
          isNull(pendingNotifications.readAt)
        )
      )
      .orderBy(pendingNotifications.createdAt);
    
    // Filter by targetId in data
    const filteredNotifications = existingNotifications.filter(n => {
      const notificationTargetId = (n.data as any)?.targetId;
      return notificationTargetId === targetId;
    });
    
    // Return Set of user IDs
    return new Set(filteredNotifications.map(n => n.userId));
  } catch (error) {
    notificationsLogger.error('Error getting notified users for target:', error);
    return new Set();
  }
}

/**
 * Get unapproved shifts count for a month (exported for use in notifications.routes.ts)
 */
export { getUnapprovedShiftsCount };

/**
 * Delete notifications by noteId and eventType
 * Used when a note is hidden (isPublic: true -> false) to revoke creation notifications
 *
 * @param noteId - The ID of the note
 * @param eventType - The event type to filter (e.g., 'note:created')
 * @param excludeUserId - Optional user ID to exclude from deletion (usually the author)
 */
export async function deleteNotificationsByNoteId(
  noteId: string,
  eventType: string,
  excludeUserId?: string
): Promise<void> {
  try {
    const conditions = [
      eq(pendingNotifications.event, eventType),
      isNull(pendingNotifications.readAt),
    ];

    const allNotifications = await db
      .select()
      .from(pendingNotifications)
      .where(and(...conditions));

    // Filter by noteId in data
    const notificationsToDelete = allNotifications.filter((n: any) => {
      const notificationNoteId = extractNotificationNoteId(n.data);
      return notificationNoteId === noteId && (!excludeUserId || n.userId !== excludeUserId);
    });

    if (notificationsToDelete.length === 0) {
      notificationsLogger.info(`No notifications to delete for note ${noteId} with event ${eventType}`);
      return;
    }

    // Delete all matching notifications
    for (const notification of notificationsToDelete) {
      await db
        .delete(pendingNotifications)
        .where(eq(pendingNotifications.id, notification.id));

      // Send socket event to notify frontend to remove the notification
      // Using 'notification:new' with the same ID to trigger removal on frontend
      try {
        await notifyUser(notification.userId, 'notification:new', {
          id: notification.id,
          event: eventType,
          data: notification.data,
          readAt: new Date().toISOString(), // Mark as read to trigger removal
          createdAt: notification.createdAt.toISOString(),
        });
      } catch (socketError) {
        notificationsLogger.warn(`Socket notification failed for user ${notification.userId} (deleteNotificationsByNoteId):`, socketError);
        // Do not throw - DB operation was successful
      }
    }

    notificationsLogger.info(`Deleted ${notificationsToDelete.length} notifications for note ${noteId} with event ${eventType}`);
  } catch (error) {
    notificationsLogger.error('Error deleting notifications by noteId:', error);
    throw error;
  }
}

/**
 * Delete ALL notifications by noteId (all event types)
 * Used when a note is hard deleted to remove all related notifications
 *
 * @param noteId - The ID of the note
 */
export async function deleteAllNotificationsByNoteId(
  noteId: string
): Promise<void> {
  try {
    const allNotifications = await db
      .select()
      .from(pendingNotifications);

    // Filter by noteId in data
    const notificationsToDelete = allNotifications.filter((n: any) => {
      const notificationNoteId = extractNotificationNoteId(n.data);
      return notificationNoteId === noteId;
    });

    if (notificationsToDelete.length === 0) {
      notificationsLogger.info(`No notifications to delete for note ${noteId}`);
      return;
    }

    // Delete all matching notifications
    for (const notification of notificationsToDelete) {
      await db
        .delete(pendingNotifications)
        .where(eq(pendingNotifications.id, notification.id));

      // Send socket event to notify frontend to remove the notification
      try {
        await notifyUser(notification.userId, 'notification:new', {
          id: notification.id,
          event: notification.event,
          data: notification.data,
          readAt: new Date().toISOString(), // Mark as read to trigger removal
          createdAt: notification.createdAt.toISOString(),
        });
      } catch (socketError) {
        notificationsLogger.warn(`Socket notification failed for user ${notification.userId} (deleteAllNotificationsByNoteId):`, socketError);
        // Do not throw - DB operation was successful
      }
    }

    notificationsLogger.info(`Deleted ${notificationsToDelete.length} notifications for note ${noteId}`);
  } catch (error) {
    notificationsLogger.error('Error deleting all notifications by noteId:', error);
    throw error;
  }
}
