/**
 * Push Notification Service
 *
 * Service for managing push tokens and sending push notifications
 *
 * @module Mobile
 */

import { Repository, DataSource, In } from 'typeorm';
import { PushToken } from '../entities/push-token.entity';
import { PushNotificationLog, PushNotificationStatus } from '../entities/push-notification-log.entity';
import {
  RegisterPushTokenDto,
  UpdatePushTokenDto,
  SubscribeTopicsDto,
  SendPushNotificationDto,
  SendBulkNotificationDto,
  PushTokenResponseDto,
  PushNotificationLogResponseDto,
  NotificationFilterDto,
  NotificationStatsDto,
} from '../dto/push-notification.dto';
import { ServiceContext } from './mobile-session.service';

export class PushNotificationService {
  private tokenRepository: Repository<PushToken>;
  private logRepository: Repository<PushNotificationLog>;

  constructor(dataSource: DataSource) {
    this.tokenRepository = dataSource.getRepository(PushToken);
    this.logRepository = dataSource.getRepository(PushNotificationLog);
  }

  /**
   * Register or update a push token
   */
  async registerToken(ctx: ServiceContext, dto: RegisterPushTokenDto): Promise<PushToken> {
    // Check if token already exists for this device/platform
    let token = await this.tokenRepository.findOne({
      where: {
        deviceId: dto.deviceId,
        platform: dto.platform,
        tenantId: ctx.tenantId,
      },
    });

    if (token) {
      // Update existing token
      token.token = dto.token;
      token.userId = dto.userId;
      token.provider = dto.provider || 'firebase';
      token.isActive = true;
      token.isValid = true;
      token.invalidReason = undefined as any;
    } else {
      // Create new token
      token = this.tokenRepository.create({
        userId: dto.userId,
        deviceId: dto.deviceId,
        tenantId: ctx.tenantId,
        token: dto.token,
        platform: dto.platform,
        provider: dto.provider || 'firebase',
        isActive: true,
        isValid: true,
        subscribedTopics: [],
      });
    }

    return this.tokenRepository.save(token);
  }

  /**
   * Find token by ID
   */
  async findTokenById(ctx: ServiceContext, id: string): Promise<PushToken | null> {
    return this.tokenRepository.findOne({
      where: { id, tenantId: ctx.tenantId },
    });
  }

  /**
   * Find tokens by user
   */
  async findTokensByUser(ctx: ServiceContext, userId: string): Promise<PushToken[]> {
    return this.tokenRepository.find({
      where: { userId, tenantId: ctx.tenantId, isActive: true, isValid: true },
    });
  }

  /**
   * Find token by device
   */
  async findTokenByDevice(ctx: ServiceContext, deviceId: string): Promise<PushToken | null> {
    return this.tokenRepository.findOne({
      where: { deviceId, tenantId: ctx.tenantId, isActive: true },
    });
  }

  /**
   * Update token
   */
  async updateToken(ctx: ServiceContext, id: string, dto: UpdatePushTokenDto): Promise<PushToken> {
    const token = await this.findTokenById(ctx, id);
    if (!token) {
      throw new Error('Push token not found');
    }

    Object.assign(token, dto);
    return this.tokenRepository.save(token);
  }

  /**
   * Mark token as invalid
   */
  async markTokenInvalid(ctx: ServiceContext, id: string, reason: string): Promise<PushToken> {
    const token = await this.findTokenById(ctx, id);
    if (!token) {
      throw new Error('Push token not found');
    }

    token.isValid = false;
    token.invalidReason = reason;
    return this.tokenRepository.save(token);
  }

  /**
   * Subscribe to topics
   */
  async subscribeToTopics(ctx: ServiceContext, id: string, dto: SubscribeTopicsDto): Promise<PushToken> {
    const token = await this.findTokenById(ctx, id);
    if (!token) {
      throw new Error('Push token not found');
    }

    const currentTopics = new Set(token.subscribedTopics || []);
    dto.topics.forEach(topic => currentTopics.add(topic));
    token.subscribedTopics = Array.from(currentTopics);

    return this.tokenRepository.save(token);
  }

  /**
   * Unsubscribe from topics
   */
  async unsubscribeFromTopics(ctx: ServiceContext, id: string, topics: string[]): Promise<PushToken> {
    const token = await this.findTokenById(ctx, id);
    if (!token) {
      throw new Error('Push token not found');
    }

    const topicsToRemove = new Set(topics);
    token.subscribedTopics = (token.subscribedTopics || []).filter(t => !topicsToRemove.has(t));

    return this.tokenRepository.save(token);
  }

  /**
   * Deactivate token
   */
  async deactivateToken(ctx: ServiceContext, id: string): Promise<void> {
    await this.tokenRepository.update(
      { id, tenantId: ctx.tenantId },
      { isActive: false }
    );
  }

  /**
   * Deactivate all tokens for device
   */
  async deactivateDeviceTokens(ctx: ServiceContext, deviceId: string): Promise<number> {
    const result = await this.tokenRepository.update(
      { deviceId, tenantId: ctx.tenantId },
      { isActive: false }
    );
    return result.affected || 0;
  }

  /**
   * Send push notification to user
   */
  async sendToUser(ctx: ServiceContext, dto: SendPushNotificationDto): Promise<PushNotificationLogResponseDto[]> {
    if (!dto.userId) {
      throw new Error('User ID is required');
    }

    const tokens = await this.findTokensByUser(ctx, dto.userId);
    if (tokens.length === 0) {
      return [];
    }

    const results: PushNotificationLogResponseDto[] = [];

    for (const token of tokens) {
      const log = await this.sendNotification(ctx, token, dto);
      results.push(this.toLogResponseDto(log));
    }

    return results;
  }

  /**
   * Send push notification to device
   */
  async sendToDevice(ctx: ServiceContext, dto: SendPushNotificationDto): Promise<PushNotificationLogResponseDto | null> {
    if (!dto.deviceId) {
      throw new Error('Device ID is required');
    }

    const token = await this.findTokenByDevice(ctx, dto.deviceId);
    if (!token) {
      return null;
    }

    const log = await this.sendNotification(ctx, token, dto);
    return this.toLogResponseDto(log);
  }

  /**
   * Send bulk notification
   */
  async sendBulk(ctx: ServiceContext, dto: SendBulkNotificationDto): Promise<{ sent: number; failed: number }> {
    let sent = 0;
    let failed = 0;

    let tokens: PushToken[] = [];

    if (dto.userIds && dto.userIds.length > 0) {
      tokens = await this.tokenRepository.find({
        where: {
          userId: In(dto.userIds),
          tenantId: ctx.tenantId,
          isActive: true,
          isValid: true,
        },
      });
    } else if (dto.topic) {
      // Find tokens subscribed to topic
      tokens = await this.tokenRepository
        .createQueryBuilder('token')
        .where('token.tenantId = :tenantId', { tenantId: ctx.tenantId })
        .andWhere('token.isActive = true')
        .andWhere('token.isValid = true')
        .andWhere(':topic = ANY(token.subscribedTopics)', { topic: dto.topic })
        .getMany();
    }

    for (const token of tokens) {
      try {
        await this.sendNotification(ctx, token, {
          title: dto.title,
          body: dto.body,
          data: dto.data,
          category: dto.category,
        });
        sent++;
      } catch {
        failed++;
      }
    }

    return { sent, failed };
  }

  /**
   * Get notification log
   */
  async getNotificationLog(
    ctx: ServiceContext,
    filter: NotificationFilterDto
  ): Promise<{ data: PushNotificationLog[]; total: number }> {
    const query = this.logRepository
      .createQueryBuilder('log')
      .where('log.tenantId = :tenantId', { tenantId: ctx.tenantId });

    if (filter.userId) {
      query.andWhere('log.userId = :userId', { userId: filter.userId });
    }

    if (filter.deviceId) {
      query.andWhere('log.deviceId = :deviceId', { deviceId: filter.deviceId });
    }

    if (filter.category) {
      query.andWhere('log.category = :category', { category: filter.category });
    }

    if (filter.status) {
      query.andWhere('log.status = :status', { status: filter.status });
    }

    if (filter.startDate) {
      query.andWhere('log.createdAt >= :startDate', { startDate: filter.startDate });
    }

    if (filter.endDate) {
      query.andWhere('log.createdAt <= :endDate', { endDate: filter.endDate });
    }

    const total = await query.getCount();

    query.orderBy('log.createdAt', 'DESC');

    if (filter.limit) {
      query.take(filter.limit);
    }

    if (filter.offset) {
      query.skip(filter.offset);
    }

    const data = await query.getMany();

    return { data, total };
  }

  /**
   * Mark notification as delivered
   */
  async markDelivered(ctx: ServiceContext, id: string): Promise<void> {
    await this.logRepository.update(
      { id, tenantId: ctx.tenantId },
      { status: 'delivered', deliveredAt: new Date() }
    );
  }

  /**
   * Mark notification as read
   */
  async markRead(ctx: ServiceContext, id: string): Promise<void> {
    await this.logRepository.update(
      { id, tenantId: ctx.tenantId },
      { status: 'read', readAt: new Date() }
    );
  }

  /**
   * Get notification statistics
   */
  async getStats(ctx: ServiceContext, filter?: NotificationFilterDto): Promise<NotificationStatsDto> {
    const query = this.logRepository
      .createQueryBuilder('log')
      .where('log.tenantId = :tenantId', { tenantId: ctx.tenantId });

    if (filter?.startDate) {
      query.andWhere('log.createdAt >= :startDate', { startDate: filter.startDate });
    }

    if (filter?.endDate) {
      query.andWhere('log.createdAt <= :endDate', { endDate: filter.endDate });
    }

    const logs = await query.getMany();

    const byStatus: Record<PushNotificationStatus, number> = {
      sent: 0,
      delivered: 0,
      failed: 0,
      read: 0,
    };

    const byCategory: Record<string, number> = {};

    let deliveredCount = 0;
    let readCount = 0;

    for (const log of logs) {
      byStatus[log.status]++;

      if (log.category) {
        byCategory[log.category] = (byCategory[log.category] || 0) + 1;
      }

      if (log.status === 'delivered' || log.status === 'read') {
        deliveredCount++;
      }

      if (log.status === 'read') {
        readCount++;
      }
    }

    const total = logs.length;

    return {
      total,
      byStatus,
      byCategory,
      deliveryRate: total > 0 ? (deliveredCount / total) * 100 : 0,
      readRate: total > 0 ? (readCount / total) * 100 : 0,
    };
  }

  /**
   * Send notification (internal)
   */
  private async sendNotification(
    ctx: ServiceContext,
    token: PushToken,
    dto: SendPushNotificationDto
  ): Promise<PushNotificationLog> {
    const log = this.logRepository.create({
      tenantId: ctx.tenantId,
      userId: token.userId,
      deviceId: token.deviceId,
      pushTokenId: token.id,
      title: dto.title,
      body: dto.body,
      data: dto.data || {},
      category: dto.category,
      status: 'sent',
      sentAt: new Date(),
    });

    try {
      // Send via provider (simulated)
      const result = await this.sendViaProvider(token, dto);

      if (result.success) {
        log.providerMessageId = result.messageId || '';
        // Update token last used
        await this.tokenRepository.update(token.id, { lastUsedAt: new Date() });
      } else {
        log.status = 'failed';
        log.errorMessage = result.error || '';

        // Mark token as invalid if provider indicates
        if (result.invalidToken) {
          await this.markTokenInvalid(ctx, token.id, result.error || 'Invalid token');
        }
      }
    } catch (error: any) {
      log.status = 'failed';
      log.errorMessage = error.message;
    }

    return this.logRepository.save(log);
  }

  /**
   * Send via push provider (simulated)
   */
  private async sendViaProvider(
    _token: PushToken,
    _dto: SendPushNotificationDto
  ): Promise<{ success: boolean; messageId?: string; error?: string; invalidToken?: boolean }> {
    // Simulate provider API call
    // In production, this would call Firebase, APNS, etc.

    await new Promise(resolve => setTimeout(resolve, 100));

    // Simulate 98% success rate
    const success = Math.random() > 0.02;

    if (success) {
      return {
        success: true,
        messageId: `msg-${Date.now()}-${Math.random().toString(36).substring(7)}`,
      };
    } else {
      return {
        success: false,
        error: 'Failed to send notification',
        invalidToken: Math.random() > 0.5,
      };
    }
  }

  /**
   * Convert token to response DTO
   */
  toTokenResponseDto(token: PushToken): PushTokenResponseDto {
    return {
      id: token.id,
      userId: token.userId,
      deviceId: token.deviceId,
      platform: token.platform,
      provider: token.provider,
      isActive: token.isActive,
      isValid: token.isValid,
      subscribedTopics: token.subscribedTopics || [],
      lastUsedAt: token.lastUsedAt,
      createdAt: token.createdAt,
    };
  }

  /**
   * Convert log to response DTO
   */
  toLogResponseDto(log: PushNotificationLog): PushNotificationLogResponseDto {
    return {
      id: log.id,
      tenantId: log.tenantId,
      userId: log.userId,
      deviceId: log.deviceId,
      title: log.title,
      body: log.body,
      category: log.category,
      status: log.status,
      sentAt: log.sentAt,
      deliveredAt: log.deliveredAt,
      readAt: log.readAt,
      errorMessage: log.errorMessage,
    };
  }
}
