import { Injectable, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import { CreateNotificationDto } from './dto/create-notification.dto';
import { NotificationCategory, NotificationQueryDto } from './dto/notification-query.dto';
import { NotificationsGateway } from './notifications.gateway';
import { NotificationsRepository } from './notifications.repository';
import { NotificationType } from './schemas/notification.schema';

const NOTIFICATION_CATEGORY_TYPES: Record<NotificationCategory, NotificationType[]> = {
  interactions: [
    'like',
    'comment',
    'reply',
    'mention',
    'save',
    'share',
    'collaboration_request',
    'collaboration_request_approved',
    'collaboration_request_rejected',
    'system',
  ],
  messages: ['message'],
  follows: ['follow'],
  follow_requests: ['follow_request', 'follow_request_approved', 'follow_request_rejected'],
  collaboration: [
    'collaboration_request',
    'collaboration_request_approved',
    'collaboration_request_rejected',
    'collaboration_request_cancelled',
  ],
  system: ['system'],
};

@Injectable()
export class NotificationsService {
  constructor(
    private readonly notificationsRepository: NotificationsRepository,
    private readonly notificationsGateway: NotificationsGateway,
  ) {}

  async create(dto: CreateNotificationDto) {
    if (dto.recipientId === dto.actorId) {
      return null;
    }

    if (dto.eventId) {
      const existing = await this.notificationsRepository.findByEventId(dto.eventId);
      if (existing) return existing;
    }

    const resourceType = (dto.resourceType ?? this.resolveResourceType(dto.type)).trim();
    const deepLink = (dto.deepLink ?? this.buildDeepLink(dto.type, dto.referenceId, resourceType)).trim();
    const title = (dto.title ?? this.buildTitle(dto.type)).trim();
    const previewText = (dto.previewText ?? '').trim();
    let notification;
    try {
      notification = await this.notificationsRepository.create({
      eventId: dto.eventId,
      recipientId: new Types.ObjectId(dto.recipientId),
      actorId: new Types.ObjectId(dto.actorId),
      type: dto.type,
      referenceId: dto.referenceId ? new Types.ObjectId(dto.referenceId) : undefined,
      title,
      previewText,
      resourceType,
      deepLink,
      metadata: dto.metadata ?? {},
      read: false,
      readAt: null,
      });
    } catch (error) {
      if (dto.eventId && this.isDuplicateKeyError(error)) {
        const existing = await this.notificationsRepository.findByEventId(dto.eventId);
        if (existing) return existing;
      }
      throw error;
    }

    const unreadCounts = await this.getUnreadCounts(dto.recipientId);
    const unreadCount = unreadCounts.total;
    this.notificationsGateway.emitCreated(
      dto.recipientId,
      notification.toJSON(),
      unreadCount,
      unreadCounts,
    );

    return notification;
  }

  async createFollowNotification(actorId: string, recipientId: string, referenceId?: string) {
    return this.create({
      actorId,
      recipientId,
      type: 'follow',
      referenceId: referenceId || actorId,
      resourceType: 'user',
      deepLink: `/users/${actorId}`,
    });
  }

  async createLikeNotification(
    actorId: string,
    recipientId: string,
    referenceId: string,
    options?: { resourceType?: string; previewText?: string },
  ) {
    return this.create({
      actorId,
      recipientId,
      type: 'like',
      referenceId,
      resourceType: options?.resourceType ?? 'post',
      previewText: options?.previewText ?? '',
    });
  }

  async createCommentNotification(
    actorId: string,
    recipientId: string,
    referenceId: string,
    options?: { resourceType?: string; previewText?: string },
  ) {
    return this.create({
      actorId,
      recipientId,
      type: 'comment',
      referenceId,
      resourceType: options?.resourceType ?? 'post',
      previewText: options?.previewText ?? '',
    });
  }

  async createSaveNotification(
    actorId: string,
    recipientId: string,
    referenceId: string,
    options?: { resourceType?: string; previewText?: string },
  ) {
    return this.create({
      actorId,
      recipientId,
      type: 'save',
      referenceId,
      resourceType: options?.resourceType ?? 'post',
      previewText: options?.previewText ?? '',
    });
  }

  async createShareNotification(
    actorId: string,
    recipientId: string,
    referenceId: string,
    options?: { resourceType?: string; previewText?: string },
  ) {
    return this.create({
      actorId,
      recipientId,
      type: 'share',
      referenceId,
      resourceType: options?.resourceType ?? 'post',
      previewText: options?.previewText ?? '',
    });
  }

  async createMentionNotification(
    actorId: string,
    recipientId: string,
    referenceId: string,
    options?: { resourceType?: string; previewText?: string; deepLink?: string },
  ) {
    return this.create({
      actorId,
      recipientId,
      type: 'mention',
      referenceId,
      resourceType: options?.resourceType ?? 'post',
      previewText: options?.previewText ?? '',
      deepLink: options?.deepLink,
    });
  }

  async createMessageNotification(
    actorId: string,
    recipientId: string,
    conversationId: string,
    previewText = '',
    messageId?: string,
  ) {
    return this.create({
      actorId,
      recipientId,
      type: 'message',
      referenceId: conversationId,
      resourceType: 'conversation',
      deepLink: `/chat/conversations/${conversationId}`,
      previewText,
      metadata: {
        type: 'message',
        conversationId: String(conversationId),
        ...(messageId ? { messageId: String(messageId) } : {}),
      },
    });
  }

  async createFollowRequestApprovedNotification(input: {
    actorId: string;
    recipientId: string;
    requestId: string;
    eventId?: string;
  }) {
    const actorId = String(input.actorId);
    const recipientId = String(input.recipientId);
    const requestId = String(input.requestId);
    return this.create({
      eventId: input.eventId,
      actorId,
      recipientId,
      type: 'follow_request_approved',
      referenceId: requestId,
      resourceType: 'user',
      deepLink: `/users/${actorId}`,
      metadata: {
        type: 'follow_request_approved',
        userId: actorId,
        requestId,
      },
    });
  }

  async getMine(recipientId: string, query: NotificationQueryDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;
    const filter: Record<string, unknown> = {};

    if (typeof query.read === 'boolean') {
      filter.read = query.read;
    }
    this.applyTypeOrCategoryFilter(filter, query);
    if (query.resourceType) {
      filter.resourceType = query.resourceType.trim();
    }

    const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;

    const [items, total, unreadCount] = await Promise.all([
      this.notificationsRepository.findMine(recipientId, filter, skip, limit, sort),
      this.notificationsRepository.countMine(recipientId, filter),
      this.notificationsRepository.countUnread(recipientId),
    ]);

    return {
      ...buildPaginatedResponse(items, {
        page,
        limit,
        total,
        offset: skip,
      }),
      unreadCount,
    };
  }

  async getForSuperAdmin(query: NotificationQueryDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;
    const filter: Record<string, unknown> = {};

    if (typeof query.read === 'boolean') {
      filter.read = query.read;
    }
    this.applyTypeOrCategoryFilter(filter, query);
    if (query.resourceType) {
      filter.resourceType = query.resourceType.trim();
    }

    const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;

    const unreadFilter = { ...filter };
    delete unreadFilter.read;

    const [items, total, unreadCount] = await Promise.all([
      this.notificationsRepository.findMany(filter, skip, limit, sort),
      this.notificationsRepository.count(filter),
      this.notificationsRepository.countUnreadAll(unreadFilter),
    ]);

    return {
      ...buildPaginatedResponse(items, {
        page,
        limit,
        total,
        offset: skip,
      }),
      unreadCount,
    };
  }

  async getUnreadCount(recipientId: string) {
    return {
      unreadCount: await this.notificationsRepository.countUnread(recipientId),
    };
  }

  async getUnreadCountByCategory(recipientId: string, category?: NotificationCategory) {
    if (!category) {
      return this.getUnreadCount(recipientId);
    }

    return {
      unreadCount: await this.notificationsRepository.countUnreadByFilter(recipientId, {
        type: { $in: NOTIFICATION_CATEGORY_TYPES[category] },
      }),
      category,
    };
  }

  async getUnreadCounts(recipientId: string) {
    const [
      total,
      interactions,
      messages,
      follows,
      followRequests,
      collaboration,
      system,
    ] = await Promise.all([
      this.notificationsRepository.countUnread(recipientId),
      this.countUnreadForCategory(recipientId, 'interactions'),
      this.countUnreadForCategory(recipientId, 'messages'),
      this.countUnreadForCategory(recipientId, 'follows'),
      this.countUnreadForCategory(recipientId, 'follow_requests'),
      this.countUnreadForCategory(recipientId, 'collaboration'),
      this.countUnreadForCategory(recipientId, 'system'),
    ]);

    return {
      total,
      interactions,
      messages,
      follows,
      followRequests,
      collaboration,
      system,
    };
  }

  async markRead(recipientId: string, notificationId: string) {
    if (!Types.ObjectId.isValid(notificationId)) {
      throw new NotFoundException('Notification not found');
    }

    const notification = await this.notificationsRepository.markRead(recipientId, notificationId);
    if (!notification) {
      throw new NotFoundException('Notification not found');
    }

    const unreadCounts = await this.getUnreadCounts(recipientId);
    const unreadCount = unreadCounts.total;
    this.notificationsGateway.emitUnreadCount(recipientId, unreadCount, unreadCounts);

    return {
      message: 'Notification marked as read',
      unreadCount,
      item: notification,
    };
  }

  async markAllRead(recipientId: string) {
    const modifiedCount = await this.notificationsRepository.markAllRead(recipientId);
    const unreadCounts = await this.getUnreadCounts(recipientId);
    const unreadCount = unreadCounts.total;
    this.notificationsGateway.emitUnreadCount(recipientId, unreadCount, unreadCounts);

    return {
      message: 'All notifications marked as read',
      modifiedCount,
      updatedCount: modifiedCount,
      unreadCount,
    };
  }

  private buildTitle(type: NotificationType): string {
    switch (type) {
      case 'like':
        return 'New like';
      case 'comment':
        return 'New comment';
      case 'follow':
        return 'New follower';
      case 'message':
        return 'New message';
      case 'save':
        return 'Post saved';
      case 'share':
        return 'Post shared';
      case 'mention':
        return 'New mention';
      case 'reply':
        return 'New reply';
      case 'system':
        return 'Notification';
      case 'collaboration_request':
        return 'Collaboration request';
      case 'collaboration_request_approved':
        return 'Collaboration request approved';
      case 'collaboration_request_rejected':
        return 'Collaboration request rejected';
      case 'collaboration_request_cancelled':
        return 'Collaboration request cancelled';
      case 'follow_request':
        return 'Follow request';
      case 'follow_request_approved':
        return 'Follow request approved';
      case 'follow_request_rejected':
        return 'Follow request rejected';
      case 'support_reply':
        return 'Support reply';
      case 'support_ticket_status':
        return 'Support ticket updated';
      default:
        return 'Notification';
    }
  }

  private resolveResourceType(type: NotificationType): string {
    switch (type) {
      case 'follow':
      case 'follow_request':
      case 'follow_request_approved':
      case 'follow_request_rejected':
        return 'user';
      case 'message':
        return 'conversation';
      case 'system':
        return 'system';
      case 'collaboration_request':
      case 'collaboration_request_approved':
      case 'collaboration_request_rejected':
      case 'collaboration_request_cancelled':
        return 'post';
      case 'support_reply':
      case 'support_ticket_status':
        return 'support_ticket';
      default:
        return 'post';
    }
  }

  private buildDeepLink(
    type: NotificationType,
    referenceId?: string,
    resourceType?: string,
  ): string {
    if (type === 'message' && referenceId) {
      return `/chat/conversations/${referenceId}`;
    }

    if (resourceType === 'user' && referenceId) {
      return `/users/${referenceId}`;
    }

    if (resourceType === 'system') {
      return '';
    }

    if (resourceType === 'support_ticket' && referenceId) {
      return `/support/tickets/${referenceId}`;
    }

    if (referenceId) {
      return `/posts/${referenceId}`;
    }

    return '';
  }

  private applyTypeOrCategoryFilter(
    filter: Record<string, unknown>,
    query: NotificationQueryDto,
  ): void {
    if (query.type) {
      filter.type = query.type;
      return;
    }

    if (query.category) {
      filter.type = { $in: NOTIFICATION_CATEGORY_TYPES[query.category] };
    }
  }

  private countUnreadForCategory(recipientId: string, category: NotificationCategory) {
    return this.notificationsRepository.countUnreadByFilter(recipientId, {
      type: { $in: NOTIFICATION_CATEGORY_TYPES[category] },
    });
  }

  private isDuplicateKeyError(error: unknown): boolean {
    return typeof error === 'object' && error !== null && 'code' in error
      && (error as { code?: number }).code === 11000;
  }
}
