/**
 * Notification Service
 * Core notification logic: CRUD, sending, and delivery tracking.
 *
 * @module Notifications
 */

import { Repository, In, LessThan } from 'typeorm';
import {
  Notification,
  NotificationStatus,
  NotificationPriority,
} from '../entities/notification.entity';
import { NotificationTemplate } from '../entities/template.entity';
import { NotificationPreference } from '../entities/preference.entity';
import { Channel, ChannelType } from '../entities/channel.entity';
import { InAppNotification, InAppCategory } from '../entities/in-app-notification.entity';

export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

export interface CreateNotificationDto {
  userId?: string;
  recipientEmail?: string;
  recipientPhone?: string;
  recipientDeviceId?: string;
  templateId?: string;
  templateCode?: string;
  channelType: ChannelType;
  channelId?: string;
  subject?: string;
  body?: string;
  bodyHtml?: string;
  variables?: Record<string, any>;
  contextType?: string;
  contextId?: string;
  priority?: NotificationPriority;
  expiresAt?: Date;
  metadata?: Record<string, any>;
}

export interface SendNotificationDto {
  userId?: string;
  recipientEmail?: string;
  recipientPhone?: string;
  recipientDeviceId?: string;
  templateCode: string;
  channelType: ChannelType;
  variables?: Record<string, any>;
  contextType?: string;
  contextId?: string;
  priority?: NotificationPriority;
  metadata?: Record<string, any>;
}

export interface NotificationFilters {
  userId?: string;
  channelType?: ChannelType;
  status?: NotificationStatus;
  priority?: NotificationPriority;
  contextType?: string;
  contextId?: string;
  fromDate?: Date;
  toDate?: Date;
  page?: number;
  limit?: number;
}

export interface NotificationStats {
  total: number;
  byStatus: Record<NotificationStatus, number>;
  byChannel: Record<ChannelType, number>;
  deliveryRate: number;
  readRate: number;
}

export class NotificationService {
  constructor(
    private readonly notificationRepository: Repository<Notification>,
    private readonly templateRepository: Repository<NotificationTemplate>,
    private readonly preferenceRepository: Repository<NotificationPreference>,
    private readonly channelRepository: Repository<Channel>,
    private readonly inAppNotificationRepository: Repository<InAppNotification>,
  ) {}

  /**
   * Create a new notification (queued for delivery)
   */
  async create(
    ctx: ServiceContext,
    dto: CreateNotificationDto,
  ): Promise<Notification> {
    const notification = this.notificationRepository.create({
      tenantId: ctx.tenantId,
      ...dto,
      status: 'pending',
      queuedAt: new Date(),
    });

    return this.notificationRepository.save(notification);
  }

  /**
   * Send a notification using a template
   */
  async send(
    ctx: ServiceContext,
    dto: SendNotificationDto,
  ): Promise<Notification> {
    // Get the template
    const template = await this.templateRepository.findOne({
      where: [
        { code: dto.templateCode, tenantId: ctx.tenantId, channelType: dto.channelType, isActive: true },
        { code: dto.templateCode, tenantId: null as any, channelType: dto.channelType, isActive: true },
      ],
    });

    if (!template) {
      throw new Error(`Template not found: ${dto.templateCode}`);
    }

    // Check user preferences if userId is provided
    if (dto.userId) {
      const preferences = await this.preferenceRepository.findOne({
        where: { userId: dto.userId, tenantId: ctx.tenantId },
      });

      if (preferences) {
        // Check if notifications are globally disabled
        if (!preferences.globalEnabled) {
          throw new Error('User has disabled all notifications');
        }

        // Check channel-specific preferences
        const channelEnabled = this.isChannelEnabled(preferences, dto.channelType);
        if (!channelEnabled) {
          throw new Error(`User has disabled ${dto.channelType} notifications`);
        }

        // Check quiet hours
        if (this.isInQuietHours(preferences)) {
          // Queue for later instead of immediate send
          const notification = await this.create(ctx, {
            ...dto,
            templateId: template.id,
            templateCode: template.code,
            subject: this.renderTemplate(template.subject || '', dto.variables || {}),
            body: this.renderTemplate(template.bodyTemplate || '', dto.variables || {}),
            bodyHtml: template.bodyHtml ? this.renderTemplate(template.bodyHtml, dto.variables || {}) : undefined,
          });
          return notification;
        }
      }
    }

    // Get the default channel for this type
    const channel = await this.channelRepository.findOne({
      where: { channelType: dto.channelType, isActive: true, isDefault: true },
    });

    // Render the template with variables
    const subject = this.renderTemplate(template.subject || '', dto.variables || {});
    const body = this.renderTemplate(template.bodyTemplate || '', dto.variables || {});
    const bodyHtml = template.bodyHtml ? this.renderTemplate(template.bodyHtml, dto.variables || {}) : undefined;

    // Create the notification
    const notification = await this.create(ctx, {
      userId: dto.userId,
      recipientEmail: dto.recipientEmail,
      recipientPhone: dto.recipientPhone,
      recipientDeviceId: dto.recipientDeviceId,
      templateId: template.id,
      templateCode: template.code,
      channelType: dto.channelType,
      channelId: channel?.id,
      subject,
      body,
      bodyHtml,
      variables: dto.variables,
      contextType: dto.contextType,
      contextId: dto.contextId,
      priority: dto.priority || 'normal',
      metadata: dto.metadata,
    });

    // For in-app notifications, also create an InAppNotification record
    if (dto.channelType === 'in_app' && dto.userId) {
      await this.createInAppNotification(ctx, {
        userId: dto.userId,
        title: subject,
        message: body,
        category: 'info',
        contextType: dto.contextType,
        contextId: dto.contextId,
        priority: dto.priority || 'normal',
      });
    }

    // Simulate sending (in production, this would be queued to a message broker)
    await this.processNotification(notification);

    return notification;
  }

  /**
   * Process a notification (send via the appropriate channel)
   */
  private async processNotification(notification: Notification): Promise<void> {
    try {
      notification.status = 'sending';
      await this.notificationRepository.save(notification);

      // Simulate channel-specific sending
      switch (notification.channelType) {
        case 'email':
          // In production: send via email provider (SendGrid, SES, etc.)
          break;
        case 'sms':
          // In production: send via SMS provider (Twilio, etc.)
          break;
        case 'push':
          // In production: send via push notification service (FCM, APNS, etc.)
          break;
        case 'whatsapp':
          // In production: send via WhatsApp Business API
          break;
        case 'in_app':
          // Already handled via InAppNotification
          break;
        case 'webhook':
          // In production: call webhook URL
          break;
      }

      notification.status = 'sent';
      notification.sentAt = new Date();
      await this.notificationRepository.save(notification);
    } catch (error) {
      notification.status = 'failed';
      notification.failedAt = new Date();
      notification.errorMessage = error instanceof Error ? error.message : 'Unknown error';
      notification.retryCount += 1;

      if (notification.retryCount < notification.maxRetries) {
        notification.status = 'pending';
        notification.nextRetryAt = new Date(Date.now() + this.getRetryDelay(notification.retryCount));
      }

      await this.notificationRepository.save(notification);
    }
  }

  /**
   * Get retry delay based on retry count (exponential backoff)
   */
  private getRetryDelay(retryCount: number): number {
    return Math.min(1000 * Math.pow(2, retryCount), 3600000); // Max 1 hour
  }

  /**
   * Render a template with variables
   */
  private renderTemplate(template: string, variables: Record<string, any>): string {
    return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
      return variables[key] !== undefined ? String(variables[key]) : match;
    });
  }

  /**
   * Check if a channel is enabled for a user
   */
  private isChannelEnabled(preferences: NotificationPreference, channelType: ChannelType): boolean {
    switch (channelType) {
      case 'email':
        return preferences.emailEnabled;
      case 'sms':
        return preferences.smsEnabled;
      case 'push':
        return preferences.pushEnabled;
      case 'whatsapp':
        return preferences.whatsappEnabled;
      case 'in_app':
        return preferences.inAppEnabled;
      default:
        return true;
    }
  }

  /**
   * Check if current time is within quiet hours
   */
  private isInQuietHours(preferences: NotificationPreference): boolean {
    if (!preferences.quietHoursStart || !preferences.quietHoursEnd) {
      return false;
    }

    const now = new Date();
    const currentTime = now.toTimeString().slice(0, 5);
    const start = preferences.quietHoursStart;
    const end = preferences.quietHoursEnd;

    if (start <= end) {
      return currentTime >= start && currentTime <= end;
    } else {
      // Quiet hours span midnight
      return currentTime >= start || currentTime <= end;
    }
  }

  /**
   * Create an in-app notification
   */
  private async createInAppNotification(
    ctx: ServiceContext,
    data: {
      userId: string;
      title: string;
      message: string;
      category?: InAppCategory;
      contextType?: string;
      contextId?: string;
      priority?: NotificationPriority;
      actionUrl?: string;
      actionType?: string;
    },
  ): Promise<InAppNotification> {
    const inAppNotification = this.inAppNotificationRepository.create({
      tenantId: ctx.tenantId,
      userId: data.userId,
      title: data.title,
      message: data.message,
      category: data.category || 'info',
      contextType: data.contextType,
      contextId: data.contextId,
      priority: data.priority || 'normal',
      actionUrl: data.actionUrl,
      actionType: data.actionType as any,
    });

    return this.inAppNotificationRepository.save(inAppNotification);
  }

  /**
   * Find notification by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<Notification | null> {
    return this.notificationRepository.findOne({
      where: { id, tenantId: ctx.tenantId },
      relations: ['template', 'channel'],
    });
  }

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

    const queryBuilder = this.notificationRepository
      .createQueryBuilder('n')
      .leftJoinAndSelect('n.template', 'template')
      .leftJoinAndSelect('n.channel', 'channel')
      .where('n.tenant_id = :tenantId', { tenantId: ctx.tenantId });

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

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

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

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

    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 delivered
   */
  async markAsDelivered(ctx: ServiceContext, id: string): Promise<Notification | null> {
    const notification = await this.findById(ctx, id);
    if (!notification) {
      return null;
    }

    notification.status = 'delivered';
    notification.deliveredAt = new Date();
    return this.notificationRepository.save(notification);
  }

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

    notification.status = 'read';
    notification.readAt = new Date();
    return this.notificationRepository.save(notification);
  }

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

    // Can only mark as unread if previously read
    if (notification.status === 'read') {
      notification.status = 'delivered';
      notification.readAt = null as unknown as Date;
      return this.notificationRepository.save(notification);
    }

    return notification;
  }

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

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

    if (notification.status !== 'pending' && notification.status !== 'queued') {
      throw new Error('Only pending or queued notifications can be cancelled');
    }

    notification.status = 'cancelled';
    return this.notificationRepository.save(notification);
  }

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

    if (notification.status !== 'failed') {
      throw new Error('Only failed notifications can be retried');
    }

    notification.status = 'pending';
    notification.retryCount = 0;
    notification.errorMessage = null as unknown as string;
    notification.failedAt = null as unknown as Date;
    notification.nextRetryAt = null as unknown as Date;

    await this.notificationRepository.save(notification);
    await this.processNotification(notification);

    return notification;
  }

  /**
   * Get notification statistics
   */
  async getStatistics(
    ctx: ServiceContext,
    options?: { fromDate?: Date; toDate?: Date; userId?: string },
  ): Promise<NotificationStats> {
    const queryBuilder = this.notificationRepository
      .createQueryBuilder('n')
      .where('n.tenant_id = :tenantId', { tenantId: ctx.tenantId });

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

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

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

    const [total, byStatusRaw, byChannelRaw, deliveredCount, readCount] = await Promise.all([
      queryBuilder.getCount(),
      this.notificationRepository
        .createQueryBuilder('n')
        .select('n.status', 'status')
        .addSelect('COUNT(*)', 'count')
        .where('n.tenant_id = :tenantId', { tenantId: ctx.tenantId })
        .groupBy('n.status')
        .getRawMany(),
      this.notificationRepository
        .createQueryBuilder('n')
        .select('n.channel_type', 'channelType')
        .addSelect('COUNT(*)', 'count')
        .where('n.tenant_id = :tenantId', { tenantId: ctx.tenantId })
        .groupBy('n.channel_type')
        .getRawMany(),
      this.notificationRepository.count({
        where: { tenantId: ctx.tenantId, status: In(['delivered', 'read']) },
      }),
      this.notificationRepository.count({
        where: { tenantId: ctx.tenantId, status: 'read' },
      }),
    ]);

    const byStatus: Record<string, number> = {};
    byStatusRaw.forEach((row: { status: string; count: string }) => {
      byStatus[row.status] = parseInt(row.count, 10);
    });

    const byChannel: Record<string, number> = {};
    byChannelRaw.forEach((row: { channelType: string; count: string }) => {
      byChannel[row.channelType] = parseInt(row.count, 10);
    });

    const sentCount = total - (byStatus['pending'] || 0) - (byStatus['queued'] || 0) - (byStatus['cancelled'] || 0);

    return {
      total,
      byStatus: byStatus as Record<NotificationStatus, number>,
      byChannel: byChannel as Record<ChannelType, number>,
      deliveryRate: sentCount > 0 ? deliveredCount / sentCount : 0,
      readRate: deliveredCount > 0 ? readCount / deliveredCount : 0,
    };
  }

  /**
   * Get pending notifications for processing
   */
  async getPendingNotifications(limit: number = 100): Promise<Notification[]> {
    return this.notificationRepository.find({
      where: [
        { status: 'pending' },
        { status: 'queued', nextRetryAt: LessThan(new Date()) },
      ],
      order: { priority: 'DESC', createdAt: 'ASC' },
      take: limit,
    });
  }

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

    const result = await this.notificationRepository.delete({
      createdAt: LessThan(cutoffDate),
      status: In(['sent', 'delivered', 'read', 'cancelled', 'failed']),
    });

    return result.affected || 0;
  }
}
