import { Types } from 'mongoose';
import QRCodeModel from '../models/QrCode';
import UserModel from '../models/User';
import { NotificationUtils } from '../utils/NotificationUtils';

/**
 * Service for handling QR code expiration for professional subscription users
 * 
 * This service manages:
 * 1. Daily expiration of QR codes at UTC 12:00 AM
 * 2. Pre-expiration notifications at UTC 9:00 PM (3 hours before expiration)
 * 3. Retry logic for failed notifications
 */
export class QRCodeExpirationService {
  private static readonly MAX_RETRY_ATTEMPTS = 3;
  private static readonly RETRY_DELAY_MS = 5000; // 5 seconds

  /**
   * Get all active QR codes for professional subscription users
   * that need to be expired or notified
   */
  private static async getProfessionalQRCodes(
    includeExpired: boolean = false
  ): Promise<Array<{
    _id: Types.ObjectId;
    title: string;
    created_by: Types.ObjectId;
    expired_at: Date | null;
    expiration_notification_sent: boolean;
  }>> {
    try {
      // First, get all professional users
      const professionalUsers = await UserModel.find({
        subscription_tier: 'professional',
        is_delete: false,
        status: 'ACTIVE',
      }).select('_id').lean();

      if (professionalUsers.length === 0) {
        return [];
      }

      const userIds = professionalUsers.map((u) => u._id);

      // Build query for QR codes
      const query: any = {
        created_by: { $in: userIds },
        status: 'ACTIVE',
        isDelete: false,
      };

      // If not including expired, only get codes that aren't expired yet
      if (!includeExpired) {
        query.$or = [
          { expired_at: null },
          { expired_at: { $gt: new Date() } },
        ];
      }

      type QRCodeLean = {
        _id: Types.ObjectId;
        title: string;
        created_by: Types.ObjectId;
        expired_at: Date | null;
        expiration_notification_sent: boolean;
      };

      type ReturnType = {
        _id: Types.ObjectId;
        title: string;
        created_by: Types.ObjectId;
        expired_at: Date | null;
        expiration_notification_sent: boolean;
      };

      const qrCodes = (await QRCodeModel.find(query)
        .select('_id title created_by expired_at expiration_notification_sent')
        .lean() as unknown) as QRCodeLean[];

      return qrCodes.map((code): ReturnType => ({
        _id: code._id as Types.ObjectId,
        title: code.title as string,
        created_by: code.created_by as Types.ObjectId,
        expired_at: code.expired_at ? new Date(code.expired_at) : null,
        expiration_notification_sent: Boolean(code.expiration_notification_sent),
      }));
    } catch (error) {
      console.error('[QRCodeExpirationService] Error fetching professional QR codes:', error);
      throw error;
    }
  }

  /**
   * Send pre-expiration notification to users
   * Called at UTC 9:00 PM daily (3 hours before expiration)
   */
  public static async sendPreExpirationNotifications(): Promise<{
    success: boolean;
    totalCodes: number;
    notificationsSent: number;
    notificationsFailed: number;
    errors: string[];
  }> {
    const startTime = Date.now();
    const errors: string[] = [];
    let notificationsSent = 0;
    let notificationsFailed = 0;

    try {
      console.log(
        `[QRCodeExpirationService] Starting pre-expiration notification job at ${new Date().toISOString()}`
      );

      // Get all active professional QR codes that haven't been notified yet
      const qrCodes = await this.getProfessionalQRCodes(false);

      // Filter to only codes that haven't received notification today
      const now = new Date();
      const todayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));

      const threeHoursFromNow = new Date(now.getTime() + 3 * 60 * 60 * 1000);

      const codesToNotify = qrCodes.filter((code) => {
        // Skip if notification was already sent today
        if (code.expiration_notification_sent) {
          return false;
        }

        // Only notify if it has an expiry date and it's within the next 3 hours
        if (code.expired_at) {
          return code.expired_at <= threeHoursFromNow && code.expired_at > now;
        }

        // Codes with null expired_at are now considered non-expiring by default
        return false;
      });

      console.log(
        `[QRCodeExpirationService] Found ${codesToNotify.length} QR codes to notify (out of ${qrCodes.length} total)`
      );

      if (codesToNotify.length === 0) {
        return {
          success: true,
          totalCodes: 0,
          notificationsSent: 0,
          notificationsFailed: 0,
          errors: [],
        };
      }

      // Group by user to avoid sending multiple notifications to the same user
      const userCodesMap = new Map<string, Array<{ qrCodeId: string; title: string }>>();

      for (const code of codesToNotify) {
        const userId = code.created_by.toString();
        if (!userCodesMap.has(userId)) {
          userCodesMap.set(userId, []);
        }
        userCodesMap.get(userId)!.push({
          qrCodeId: code._id.toString(),
          title: code.title,
        });
      }

      console.log(
        `[QRCodeExpirationService] Sending notifications to ${userCodesMap.size} users`
      );

      // Send notifications to each user
      for (const [userId, codes] of userCodesMap.entries()) {
        try {
          const codeCount = codes.length;
          const message =
            codeCount === 1
              ? `Your QR code "${codes[0].title}" will expire in 3 hours. Generate a new one.`
              : `${codeCount} of your QR codes will expire in 3 hours. Generate new ones.`;

          const notificationResult = await NotificationUtils.createNotification({
            title: 'QR Code Expiration Reminder',
            message,
            type: 'EMERGENCY',
            user_id: userId,
            data: {
              qr_code_count: codeCount.toString(),
              expiration_type: 'daily_professional',
            },
            sendPush: true,
          });

          if (notificationResult.pushSent) {
            notificationsSent++;
            console.log(
              `[QRCodeExpirationService] ✅ Notification sent to user ${userId} for ${codeCount} QR code(s)`
            );
          } else {
            notificationsFailed++;
            const errorMsg = `Failed to send notification to user ${userId}: ${notificationResult.pushError || 'Unknown error'}`;
            errors.push(errorMsg);
            console.error(`[QRCodeExpirationService] ❌ ${errorMsg}`);
          }

          // Mark all QR codes for this user as notification sent
          const qrCodeIds = codes.map((c) => new Types.ObjectId(c.qrCodeId));
          await QRCodeModel.updateMany(
            { _id: { $in: qrCodeIds } },
            {
              $set: {
                expiration_notification_sent: true,
                expiration_notification_sent_at: new Date(),
              },
            }
          );
        } catch (error) {
          notificationsFailed++;
          const errorMsg = `Error processing notification for user ${userId}: ${error instanceof Error ? error.message : 'Unknown error'}`;
          errors.push(errorMsg);
          console.error(`[QRCodeExpirationService] ❌ ${errorMsg}`, error);
        }
      }

      const duration = Date.now() - startTime;
      console.log(
        `[QRCodeExpirationService] Completed pre-expiration notification job in ${duration}ms. ` +
        `Sent: ${notificationsSent}, Failed: ${notificationsFailed}`
      );

      return {
        success: notificationsFailed === 0,
        totalCodes: codesToNotify.length,
        notificationsSent,
        notificationsFailed,
        errors,
      };
    } catch (error) {
      const errorMsg = `Fatal error in sendPreExpirationNotifications: ${error instanceof Error ? error.message : 'Unknown error'}`;
      console.error(`[QRCodeExpirationService] ❌ ${errorMsg}`, error);
      errors.push(errorMsg);
      return {
        success: false,
        totalCodes: 0,
        notificationsSent,
        notificationsFailed,
        errors,
      };
    }
  }

  /**
   * Expire all QR codes for professional subscription users
   * Called at UTC 12:00 AM daily
   */
  public static async expireProfessionalQRCodes(): Promise<{
    success: boolean;
    totalCodes: number;
    expiredCount: number;
    errors: string[];
  }> {
    const startTime = Date.now();
    const errors: string[] = [];

    try {
      console.log(
        `[QRCodeExpirationService] Starting QR code expiration job at ${new Date().toISOString()}`
      );

      // Get all active professional QR codes
      const qrCodes = await this.getProfessionalQRCodes(true);

      console.log(
        `[QRCodeExpirationService] Found ${qrCodes.length} professional QR codes to process`
      );

      if (qrCodes.length === 0) {
        return {
          success: true,
          totalCodes: 0,
          expiredCount: 0,
          errors: [],
        };
      }

      // Set expired_at to current time (UTC midnight) and reset notification flag
      const now = new Date();
      const midnightUTC = new Date(
        Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0)
      );

      const qrCodeIds = qrCodes.map((code) => code._id);

      const updateResult = await QRCodeModel.updateMany(
        {
          _id: { $in: qrCodeIds },
          status: 'ACTIVE',
          isDelete: false,
          expired_at: { $ne: null, $lte: now },
        },
        {
          $set: {
            status: 'INACTIVE',
            expiration_notification_sent: false,
            expiration_notification_sent_at: null,
          },
        }
      );

      const expiredCount = updateResult.modifiedCount;

      const duration = Date.now() - startTime;
      console.log(
        `[QRCodeExpirationService] ✅ Expired ${expiredCount} QR codes in ${duration}ms`
      );

      return {
        success: true,
        totalCodes: qrCodes.length,
        expiredCount,
        errors: [],
      };
    } catch (error) {
      const errorMsg = `Fatal error in expireProfessionalQRCodes: ${error instanceof Error ? error.message : 'Unknown error'}`;
      console.error(`[QRCodeExpirationService] ❌ ${errorMsg}`, error);
      errors.push(errorMsg);
      return {
        success: false,
        totalCodes: 0,
        expiredCount: 0,
        errors,
      };
    }
  }

  /**
   * Retry failed notifications with exponential backoff
   */
  public static async retryFailedNotifications(
    maxRetries: number = this.MAX_RETRY_ATTEMPTS
  ): Promise<{
    success: boolean;
    retried: number;
    succeeded: number;
    failed: number;
    errors: string[];
  }> {
    const errors: string[] = [];
    let succeeded = 0;
    let failed = 0;

    try {
      // Find QR codes that should have been notified but failed
      // This would require tracking failed attempts, which we can add if needed
      // For now, this is a placeholder for future retry logic

      return {
        success: true,
        retried: 0,
        succeeded,
        failed,
        errors,
      };
    } catch (error) {
      const errorMsg = `Error in retryFailedNotifications: ${error instanceof Error ? error.message : 'Unknown error'}`;
      console.error(`[QRCodeExpirationService] ❌ ${errorMsg}`, error);
      errors.push(errorMsg);
      return {
        success: false,
        retried: 0,
        succeeded,
        failed,
        errors,
      };
    }
  }

  /**
   * Reset expiration for a QR code (when user regenerates it)
   * This should be called when a professional user regenerates their QR code
   */
  public static async resetQRCodeExpiration(qrCodeId: string): Promise<{
    success: boolean;
    error?: string;
  }> {
    try {
      type QRCodeWithUserLean = {
        _id: Types.ObjectId;
        created_by: {
          _id: Types.ObjectId;
          subscription_tier?: string;
        } | Types.ObjectId;
      } | null;

      const qrCode = (await QRCodeModel.findById(qrCodeId)
        .populate('created_by', 'subscription_tier')
        .lean() as unknown) as QRCodeWithUserLean;

      if (!qrCode) {
        return { success: false, error: 'QR code not found' };
      }

      // Only reset for professional users
      // Check if created_by is populated (object with properties) vs just ObjectId
      const isPopulated = typeof qrCode.created_by === 'object' &&
        qrCode.created_by !== null &&
        '_id' in qrCode.created_by;

      if (!isPopulated) {
        return { success: true }; // Not populated, can't check tier
      }

      const user = qrCode.created_by as { _id: Types.ObjectId; subscription_tier?: string };
      if (user.subscription_tier !== 'professional') {
        return { success: true }; // Not an error, just not applicable
      }

      // Reset expiration and notification flags
      await QRCodeModel.findByIdAndUpdate(qrCodeId, {
        $set: {
          expired_at: null,
          expiration_notification_sent: false,
          expiration_notification_sent_at: null,
        },
      });

      return { success: true };
    } catch (error) {
      const errorMsg = `Error resetting QR code expiration: ${error instanceof Error ? error.message : 'Unknown error'}`;
      console.error(`[QRCodeExpirationService] ❌ ${errorMsg}`, error);
      return { success: false, error: errorMsg };
    }
  }

  /**
   * Get expiration statistics for monitoring
   */
  public static async getExpirationStats(): Promise<{
    totalProfessionalCodes: number;
    activeCodes: number;
    expiredCodes: number;
    codesWithNotifications: number;
    codesWithoutNotifications: number;
  }> {
    try {
      const qrCodes = await this.getProfessionalQRCodes(true);

      const now = new Date();
      const activeCodes = qrCodes.filter(
        (code) => !code.expired_at || new Date(code.expired_at) > now
      ).length;
      const expiredCodes = qrCodes.length - activeCodes;
      const codesWithNotifications = qrCodes.filter(
        (code) => code.expiration_notification_sent
      ).length;
      const codesWithoutNotifications = qrCodes.length - codesWithNotifications;

      return {
        totalProfessionalCodes: qrCodes.length,
        activeCodes,
        expiredCodes,
        codesWithNotifications,
        codesWithoutNotifications,
      };
    } catch (error) {
      console.error('[QRCodeExpirationService] Error getting expiration stats:', error);
      throw error;
    }
  }
}

