/**
 * In-App Notification Service
 * In-app notification management with read/archive tracking.
 *
 * @module Notifications
 */

import { Repository, In, LessThan, MoreThan } from 'typeorm';
import {
  InAppNotification,
  InAppCategory,
  InAppPriority,
  InAppActionType,
} from '../entities/in-app-notification.entity';
import { ServiceContext } from './notification.service';

export interface CreateInAppNotificationDto {
  userId: string;
  title: string;
  message: string;
  icon?: string;
  color?: string;
  actionType?: InAppActionType;
  actionUrl?: string;
  actionData?: Record<string, any>;
  category?: InAppCategory;
  contextType?: string;
  contextId?: string;
  priority?: InAppPriority;
  expiresAt?: Date;
}

export interface InAppNotificationFilters {
  userId?: string;
  category?: InAppCategory;
  priority?: InAppPriority;
  isRead?: boolean;
  isArchived?: boolean;
  contextType?: string;
  contextId?: string;
  fromDate?: Date;
  toDate?: Date;
  page?: number;
  limit?: number;
}

export interface InAppNotificationCounts {
  total: number;
  unread: number;
  byCategory: Record<InAppCategory, number>;
}

export class InAppNotificationService {
  constructor(
    private readonly inAppNotificationRepository: Repository<InAppNotification>,
  ) {}

  /**
   * Create a new in-app notification
   */
  async create(
    ctx: ServiceContext,
    dto: CreateInAppNotificationDto,
  ): Promise<InAppNotification> {
    const notification = this.inAppNotificationRepository.create({
      tenantId: ctx.tenantId,
      ...dto,
    });

    return this.inAppNotificationRepository.save(notification);
  }

  /**
   * Create multiple in-app notifications (broadcast)
   */
  async createMany(
    ctx: ServiceContext,
    userIds: string[],
    data: Omit<CreateInAppNotificationDto, 'userId'>,
  ): Promise<InAppNotification[]> {
    const notifications = userIds.map((userId) =>
      this.inAppNotificationRepository.create({
        tenantId: ctx.tenantId,
        userId,
        ...data,
      }),
    );

    return this.inAppNotificationRepository.save(notifications);
  }

  /**
   * Find notification by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<InAppNotification | null> {
    return this.inAppNotificationRepository.findOne({
      where: { id, tenantId: ctx.tenantId },
    });
  }

  /**
   * Find all notifications for a user
   */
  async findByUser(
    ctx: ServiceContext,
    userId: string,
    options?: {
      includeRead?: boolean;
      includeArchived?: boolean;
      limit?: number;
    },
  ): Promise<InAppNotification[]> {
    const queryBuilder = this.inAppNotificationRepository
      .createQueryBuilder('n')
      .where('n.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('n.user_id = :userId', { userId });

    if (!options?.includeRead) {
      queryBuilder.andWhere('n.is_read = false');
    }

    if (!options?.includeArchived) {
      queryBuilder.andWhere('n.is_archived = false');
    }

    // Exclude expired notifications
    queryBuilder.andWhere('(n.expires_at IS NULL OR n.expires_at > :now)', { now: new Date() });

    return queryBuilder
      .orderBy('n.priority', 'DESC')
      .addOrderBy('n.created_at', 'DESC')
      .take(options?.limit || 50)
      .getMany();
  }

  /**
   * Find all notifications with filters
   */
  async findAll(
    ctx: ServiceContext,
    filters: InAppNotificationFilters,
  ): Promise<{ data: InAppNotification[]; total: number; page: number; limit: number }> {
    const page = filters.page || 1;
    const limit = filters.limit || 20;
    const skip = (page - 1) * limit;

    const queryBuilder = this.inAppNotificationRepository
      .createQueryBuilder('n')
      .where('n.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (filters.userId) {
      queryBuilder.andWhere('n.user_id = :userId', { userId: filters.userId });
    }

    if (filters.category) {
      queryBuilder.andWhere('n.category = :category', { category: filters.category });
    }

    if (filters.priority) {
      queryBuilder.andWhere('n.priority = :priority', { priority: filters.priority });
    }

    if (filters.isRead !== undefined) {
      queryBuilder.andWhere('n.is_read = :isRead', { isRead: filters.isRead });
    }

    if (filters.isArchived !== undefined) {
      queryBuilder.andWhere('n.is_archived = :isArchived', { isArchived: filters.isArchived });
    }

    if (filters.contextType) {
      queryBuilder.andWhere('n.context_type = :contextType', { contextType: filters.contextType });
    }

    if (filters.contextId) {
      queryBuilder.andWhere('n.context_id = :contextId', { contextId: filters.contextId });
    }

    if (filters.fromDate) {
      queryBuilder.andWhere('n.created_at >= :fromDate', { fromDate: filters.fromDate });
    }

    if (filters.toDate) {
      queryBuilder.andWhere('n.created_at <= :toDate', { toDate: filters.toDate });
    }

    const [data, total] = await queryBuilder
      .orderBy('n.created_at', 'DESC')
      .skip(skip)
      .take(limit)
      .getManyAndCount();

    return { data, total, page, limit };
  }

  /**
   * Mark notification as read
   */
  async markAsRead(ctx: ServiceContext, id: string): Promise<InAppNotification | null> {
    const notification = await this.findById(ctx, id);
    if (!notification) {
      return null;
    }

    if (!notification.isRead) {
      notification.isRead = true;
      notification.readAt = new Date();
      return this.inAppNotificationRepository.save(notification);
    }

    return notification;
  }

  /**
   * Mark notification as unread
   */
  async markAsUnread(ctx: ServiceContext, id: string): Promise<InAppNotification | null> {
    const notification = await this.findById(ctx, id);
    if (!notification) {
      return null;
    }

    if (notification.isRead) {
      notification.isRead = false;
      notification.readAt = null as unknown as Date;
      return this.inAppNotificationRepository.save(notification);
    }

    return notification;
  }

  /**
   * Mark all notifications as read for a user
   */
  async markAllAsRead(ctx: ServiceContext, userId: string): Promise<number> {
    const result = await this.inAppNotificationRepository.update(
      { tenantId: ctx.tenantId, userId, isRead: false },
      { isRead: true, readAt: new Date() },
    );
    return result.affected || 0;
  }

  /**
   * Mark multiple notifications as read
   */
  async markMultipleAsRead(ctx: ServiceContext, ids: string[]): Promise<number> {
    const result = await this.inAppNotificationRepository.update(
      { id: In(ids), tenantId: ctx.tenantId, isRead: false },
      { isRead: true, readAt: new Date() },
    );
    return result.affected || 0;
  }

  /**
   * Archive a notification
   */
  async archive(ctx: ServiceContext, id: string): Promise<InAppNotification | null> {
    const notification = await this.findById(ctx, id);
    if (!notification) {
      return null;
    }

    if (!notification.isArchived) {
      notification.isArchived = true;
      notification.archivedAt = new Date();
      return this.inAppNotificationRepository.save(notification);
    }

    return notification;
  }

  /**
   * Unarchive a notification
   */
  async unarchive(ctx: ServiceContext, id: string): Promise<InAppNotification | null> {
    const notification = await this.findById(ctx, id);
    if (!notification) {
      return null;
    }

    if (notification.isArchived) {
      notification.isArchived = false;
      notification.archivedAt = null as unknown as Date;
      return this.inAppNotificationRepository.save(notification);
    }

    return notification;
  }

  /**
   * Archive all notifications for a user
   */
  async archiveAll(ctx: ServiceContext, userId: string): Promise<number> {
    const result = await this.inAppNotificationRepository.update(
      { tenantId: ctx.tenantId, userId, isArchived: false },
      { isArchived: true, archivedAt: new Date() },
    );
    return result.affected || 0;
  }

  /**
   * Archive multiple notifications
   */
  async archiveMultiple(ctx: ServiceContext, ids: string[]): Promise<number> {
    const result = await this.inAppNotificationRepository.update(
      { id: In(ids), tenantId: ctx.tenantId, isArchived: false },
      { isArchived: true, archivedAt: new Date() },
    );
    return result.affected || 0;
  }

  /**
   * Delete a notification
   */
  async delete(ctx: ServiceContext, id: string): Promise<boolean> {
    const result = await this.inAppNotificationRepository.delete({
      id,
      tenantId: ctx.tenantId,
    });
    return (result.affected ?? 0) > 0;
  }

  /**
   * Delete all archived notifications for a user
   */
  async deleteArchived(ctx: ServiceContext, userId: string): Promise<number> {
    const result = await this.inAppNotificationRepository.delete({
      tenantId: ctx.tenantId,
      userId,
      isArchived: true,
    });
    return result.affected || 0;
  }

  /**
   * Get notification counts for a user
   */
  async getCounts(ctx: ServiceContext, userId: string): Promise<InAppNotificationCounts> {
    const [total, unread, byCategoryRaw] = await Promise.all([
      this.inAppNotificationRepository.count({
        where: { tenantId: ctx.tenantId, userId, isArchived: false },
      }),
      this.inAppNotificationRepository.count({
        where: { tenantId: ctx.tenantId, userId, isRead: false, isArchived: false },
      }),
      this.inAppNotificationRepository
        .createQueryBuilder('n')
        .select('n.category', 'category')
        .addSelect('COUNT(*)', 'count')
        .where('n.tenant_id = :tenantId', { tenantId: ctx.tenantId })
        .andWhere('n.user_id = :userId', { userId })
        .andWhere('n.is_read = false')
        .andWhere('n.is_archived = false')
        .groupBy('n.category')
        .getRawMany(),
    ]);

    const byCategory: Record<string, number> = {};
    byCategoryRaw.forEach((row: { category: string; count: string }) => {
      byCategory[row.category] = parseInt(row.count, 10);
    });

    return {
      total,
      unread,
      byCategory: byCategory as Record<InAppCategory, number>,
    };
  }

  /**
   * Get recent notifications for a user (for real-time updates)
   */
  async getRecent(
    ctx: ServiceContext,
    userId: string,
    sinceDate: Date,
  ): Promise<InAppNotification[]> {
    return this.inAppNotificationRepository.find({
      where: {
        tenantId: ctx.tenantId,
        userId,
        createdAt: MoreThan(sinceDate),
      },
      order: { createdAt: 'DESC' },
    });
  }

  /**
   * Clean up expired notifications
   */
  async cleanupExpired(): Promise<number> {
    const result = await this.inAppNotificationRepository.delete({
      expiresAt: LessThan(new Date()),
    });
    return result.affected || 0;
  }

  /**
   * Clean up old archived notifications
   */
  async cleanupOldArchived(daysOld: number = 30): Promise<number> {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - daysOld);

    const result = await this.inAppNotificationRepository.delete({
      isArchived: true,
      archivedAt: LessThan(cutoffDate),
    });

    return result.affected || 0;
  }

  /**
   * Get notifications by context (e.g., for a specific project or task)
   */
  async findByContext(
    ctx: ServiceContext,
    contextType: string,
    contextId: string,
  ): Promise<InAppNotification[]> {
    return this.inAppNotificationRepository.find({
      where: {
        tenantId: ctx.tenantId,
        contextType,
        contextId,
      },
      order: { createdAt: 'DESC' },
    });
  }

  /**
   * Delete notifications by context (e.g., when entity is deleted)
   */
  async deleteByContext(
    ctx: ServiceContext,
    contextType: string,
    contextId: string,
  ): Promise<number> {
    const result = await this.inAppNotificationRepository.delete({
      tenantId: ctx.tenantId,
      contextType,
      contextId,
    });
    return result.affected || 0;
  }

  /**
   * Send a system notification to all users in a tenant
   */
  async broadcastToTenant(
    ctx: ServiceContext,
    userIds: string[],
    data: {
      title: string;
      message: string;
      category?: InAppCategory;
      priority?: InAppPriority;
      actionUrl?: string;
    },
  ): Promise<number> {
    const notifications = await this.createMany(ctx, userIds, {
      title: data.title,
      message: data.message,
      category: data.category || 'info',
      priority: data.priority || 'normal',
      actionUrl: data.actionUrl,
    });

    return notifications.length;
  }
}
