import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { EntityManager, FilterQuery } from '@mikro-orm/postgresql';
import { Notification } from '../entities/notification.entity';
import { NotificationPreferenceService } from './notification-preference.service';
import { NotificationQueueService } from './notification-queue.service';
import { NotificationsGateway } from '../notifications.gateway';
import { NotifChannelEnum, NotifRequest, NotifTitleEnum } from '../interfaces/notification.interface';
import { SmsLog } from '../entities/smsLogs.entity';
import { SmsLogRepository } from '../repositories/sms-log.repository';
import { PaginatedResult } from '../../../common/interfaces/pagination.interface';

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

  constructor(
    private readonly em: EntityManager,
    private readonly preferenceService: NotificationPreferenceService,
    private readonly queueService: NotificationQueueService,
    private readonly notificationGateway: NotificationsGateway,
    private readonly smsLogRepository: SmsLogRepository,
  ) { }

  async sendNotification(params: NotifRequest): Promise<Notification[]> {
    const { recipients, message, metadata, restaurantId } = params;

    // create Database notifications
    const notifications = await this.createAdminBulkNotifications(
      recipients.map(recipient => ({
        restaurantId,
        title: message.title,
        content: message.content,
        adminId: 'adminId' in recipient ? recipient.adminId : null,
        userId: 'userId' in recipient ? recipient.userId : null,
      })),
    );

    // get admin prefrences
    const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, message.title);

    if (preference?.channels?.length === 0) {
      this.logger.warn(`Notification type is NONE for restaurant ${restaurantId}, title ${message.title}`);
      return notifications;
    }

    // send in app notification
    if (preference?.channels?.includes(NotifChannelEnum.IN_APP)) {
      await this.queueService.addBulkInAppNotifications(
        notifications.map(notification => ({
          recipient: { adminId: notification.admin?.id || '', restaurantId },
          subject: message.title,
          body: message.content,
          notificationId: notification.id,
        })),
      );
    }

    // add sms notifications to queue
    if (preference?.channels?.includes(NotifChannelEnum.SMS)) {
      await this.queueService.addBulkSmsNotifications(
        recipients.map(recipient => ({
          recipient,
          templateId: message.sms.templateId,
          parameters: message.sms.parameters,
          restaurantId,
        })),
      );
    }

    this.logger.log(`Queued notification for restaurant ${restaurantId}, title ${message.title}`);

    return notifications;
  }

  async createAdminBulkNotifications(
    params: {
      restaurantId: string;
      title: NotifTitleEnum;
      content: string;
      adminId: string | null;
      userId: string | null;
    }[],
  ): Promise<Notification[]> {
    const notifications = params.map(param => {
      return this.em.create(Notification, {
        restaurant: param.restaurantId,
        admin: param.adminId,
        user: param.userId && param.userId.trim() !== '' ? param.userId : null,
        title: param.title,
        content: param.content,
      });
    });
    await this.em.persistAndFlush(notifications);
    return notifications;
  }

  async findOne(id: string): Promise<Notification> {
    const notification = await this.em.findOne(Notification, { id }, { populate: ['restaurant', 'user'] });
    if (!notification) {
      throw new NotFoundException('Notification not found');
    }
    return notification;
  }

  async findByRestaurant(
    restaurantId: string,
    adminId: string,
    limit = 50,
    cursor?: string,
    status?: 'seen' | 'unseen',
  ): Promise<{ data: Notification[]; nextCursor: string | null }> {
    const where: FilterQuery<Notification> = {
      restaurant: { id: restaurantId },
      admin: { id: adminId },
    };

    // Filter by status (seen/unseen)
    if (status === 'seen') {
      where.seenAt = { $ne: null };
    } else if (status === 'unseen') {
      where.seenAt = null;
    }

    // Cursor-based pagination: if cursor is provided, get items with id < cursor
    if (cursor) {
      where.id = { $lt: cursor };
    }

    const notifications = await this.em.find(Notification, where, {
      orderBy: { createdAt: 'DESC', id: 'DESC' },
      limit: limit + 1, // fetch one extra to determine next page
      populate: ['user'],
    });

    const hasNextPage = notifications.length > limit;
    const data = hasNextPage ? notifications.slice(0, limit) : notifications;
    const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null;

    return {
      data,
      nextCursor: nextCursor ?? null,
    };
  }

  async findByUserAndRestaurant(
    userId: string,
    restaurantId: string,
    limit = 50,
    cursor?: string,
    status?: 'seen' | 'unseen',
  ): Promise<{ data: Notification[]; nextCursor: string | null }> {
    const where: FilterQuery<Notification> = {
      user: { id: userId },
      restaurant: { id: restaurantId },
    };

    // Filter by status (seen/unseen)
    if (status === 'seen') {
      where.seenAt = { $ne: null };
    } else if (status === 'unseen') {
      where.seenAt = null;
    }

    // Cursor-based pagination: if cursor is provided, get items with id < cursor (since ULIDs are time-ordered)
    if (cursor) {
      where.id = { $lt: cursor };
    }

    const notifications = await this.em.find(Notification, where, {
      orderBy: { createdAt: 'DESC', id: 'DESC' },
      limit: limit + 1, // Fetch one extra to determine if there's a next page
    });

    // Check if there's a next page
    const hasNextPage = notifications.length > limit;
    const data = hasNextPage ? notifications.slice(0, limit) : notifications;
    const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null;

    return {
      data,
      nextCursor: nextCursor ?? null,
    };
  }

  async readNotificationAdmin(id: string, adminId: string, restaurantId: string): Promise<void> {
    const notification = await this.em.findOne(Notification, {
      id,
      admin: { id: adminId },
      restaurant: { id: restaurantId },
    });
    if (!notification) {
      throw new NotFoundException('Notification not found');
    }
    notification.seenAt = new Date();
    await this.em.persistAndFlush(notification);
  }
  async readNotificationAsUser(id: string, userId: string, restaurantId: string): Promise<void> {
    const notification = await this.em.findOne(Notification, {
      id,
      user: { id: userId },
      restaurant: { id: restaurantId },
    });
    if (!notification) {
      throw new NotFoundException('Notification not found');
    }
    notification.seenAt = new Date();
    await this.em.persistAndFlush(notification);
  }

  async findByRestaurantAndType(restaurantId: string, title: NotifTitleEnum, limit = 50): Promise<Notification[]> {
    return this.em.find(
      Notification,
      {
        restaurant: { id: restaurantId },
        title,
      },
      {
        orderBy: { createdAt: 'DESC' },
        limit,
        populate: ['user'],
      },
    );
  }

  async findByAdminAndRestaurant(adminId: string, restaurantId: string, limit = 50): Promise<Notification[]> {
    return this.em.find(
      Notification,
      { admin: { id: adminId }, restaurant: { id: restaurantId } },
      {
        orderBy: { createdAt: 'DESC' },
        limit,
      },
    );
  }

  async countUnseenByUserAndRestaurant(userId: string, restaurantId: string): Promise<number> {
    const where: FilterQuery<Notification> = {
      user: { id: userId },
      restaurant: { id: restaurantId },
      seenAt: null,
    };
    return this.em.count(Notification, where);
  }

  async countUnseenByRestaurant(adminId: string, restaurantId: string): Promise<number> {
    const where: FilterQuery<Notification> = {
      admin: { id: adminId },
      restaurant: { id: restaurantId },
      seenAt: null,
    };
    return this.em.count(Notification, where);
  }

  readAllNotifsAsUser(userId: string, restaurantId: string): Promise<number> {
    const where: FilterQuery<Notification> = {
      user: { id: userId },
      restaurant: { id: restaurantId },
      seenAt: null,
    };
    return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
  }

  readAllNotifsAsAdmin(adminId: string, restaurantId: string): Promise<number> {
    const where: FilterQuery<Notification> = {
      admin: { id: adminId },
      restaurant: { id: restaurantId },
      seenAt: null,
    };
    return this.em.nativeUpdate(Notification, where, { seenAt: new Date() });
  }

  async getSmsCountByRestaurant(
    page: number = 1,
    limit: number = 10,
  ): Promise<PaginatedResult<{ restaurantId: string; restaurantName: string; smsCount: number }>> {
    return this.smsLogRepository.getSmsCountByRestaurant(page, limit);
  }

  async getSmsCountByRestaurantId(restaurantId: string): Promise<number> {
    return this.smsLogRepository.getSmsCountByRestaurantId(restaurantId);
  }
}
