import admin from 'firebase-admin';
import { loadNotificationConfig } from '../config/notification.config';
import { APNSService } from './apns.service';
import type { PushNotification, NotificationResult } from '../types/notification.types';

const config = loadNotificationConfig();

let fcmApp: admin.app.App | null = null;
let fcmInitPromise: Promise<admin.app.App> | null = null;

function getFCMApp(): admin.app.App {
  if (fcmApp) {
    return fcmApp;
  }

  // If already initializing, throw to prevent duplicate initialization
  // (caller should use getFCMAppAsync for async-safe access)
  if (fcmInitPromise && !fcmApp) {
    throw new Error('Firebase initialization in progress');
  }

  // Check if any Firebase apps exist
  const existingApps = admin.apps;
  if (existingApps && existingApps.length > 0) {
    const existingApp = existingApps.find((app: admin.app.App) => app.name === '[DEFAULT]');
    if (existingApp) {
      fcmApp = existingApp;
      return fcmApp;
    }
  }

  // Initialize new app
  fcmApp = admin.initializeApp({
    credential: admin.credential.cert({
      projectId: config.fcm.projectId,
      clientEmail: config.fcm.clientEmail,
      privateKey: config.fcm.privateKey.replace(/\\n/g, '\n'),
    }),
  });

  return fcmApp;
}

/**
 * Async-safe version that prevents race conditions during initialization.
 * Multiple concurrent calls will share the same initialization promise.
 */
function getFCMAppAsync(): Promise<admin.app.App> {
  if (fcmApp) {
    return Promise.resolve(fcmApp);
  }

  if (fcmInitPromise) {
    return fcmInitPromise;
  }

  fcmInitPromise = (async () => {
    try {
      // Check if any Firebase apps exist
      const existingApps = admin.apps;
      if (existingApps && existingApps.length > 0) {
        const existingApp = existingApps.find((app: admin.app.App) => app.name === '[DEFAULT]');
        if (existingApp) {
          fcmApp = existingApp;
          return fcmApp;
        }
      }

      fcmApp = admin.initializeApp({
        credential: admin.credential.cert({
          projectId: config.fcm.projectId,
          clientEmail: config.fcm.clientEmail,
          privateKey: config.fcm.privateKey.replace(/\\n/g, '\n'),
        }),
      });
      return fcmApp;
    } finally {
      fcmInitPromise = null;
    }
  })();

  return fcmInitPromise;
}

// Retry configuration for transient failures
const RETRY_CONFIG = {
  maxRetries: 3,
  baseDelayMs: 1000,
  maxDelayMs: 5000,
  retryableErrors: [
    'UNAVAILABLE',
    'DEADLINE_EXCEEDED',
    'RESOURCE_EXHAUSTED',
    'INTERNAL',
  ],
};

function isRetryableError(error: unknown): boolean {
  if (error instanceof Error) {
    const message = error.message.toUpperCase();
    return RETRY_CONFIG.retryableErrors.some(re => message.includes(re));
  }
  return false;
}

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number = RETRY_CONFIG.maxRetries
): Promise<T> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));

      if (attempt < maxRetries && isRetryableError(error)) {
        const delay = Math.min(
          RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt),
          RETRY_CONFIG.maxDelayMs
        );
        console.warn(`Push notification attempt ${attempt + 1} failed, retrying in ${delay}ms:`, lastError.message);
        await new Promise(resolve => setTimeout(resolve, delay));
      } else {
        throw lastError;
      }
    }
  }

  throw lastError!;
}

export class PushService {
  private static instance: PushService;
  private sentCount = new Map<string, number>();
  private sentDedup = new Map<string, number>();
  private apnsService: APNSService;
  private cleanupInterval: NodeJS.Timeout;

  private constructor() {
    this.apnsService = APNSService.getInstance();
    this.cleanupInterval = setInterval(() => {
      const now = Date.now();
      for (const [key, timestamp] of this.sentCount.entries()) {
        if (now - timestamp > 60000) {
          this.sentCount.delete(key);
        }
      }
      for (const [key, timestamp] of this.sentDedup.entries()) {
        if (now - timestamp > config.redis.dedupWindowSeconds * 1000) {
          this.sentDedup.delete(key);
        }
      }
    }, 60000);
  }

  static getInstance(): PushService {
    if (!PushService.instance) {
      PushService.instance = new PushService();
    }
    return PushService.instance;
  }

  /**
   * Send push notification using the appropriate service
   * Uses explicit platform from notification data instead of heuristic detection
   */
  async send(notification: PushNotification): Promise<NotificationResult> {
    const rateLimitKey = `push:${notification.userId}`;
    const currentCount = this.sentCount.get(rateLimitKey) || 0;

    if (currentCount >= config.rateLimits.pushPerMinute) {
      throw new Error(`Push rate limit exceeded for user ${notification.userId}`);
    }

    // Deduplication: skip if identical notification was sent recently
    const dedupKey = `${notification.userId}:${notification.title}:${notification.body}`;
    const lastSent = this.sentDedup.get(dedupKey);
    if (lastSent && Date.now() - lastSent < config.redis.dedupWindowSeconds * 1000) {
      return {
        notificationId: `push-dedup-${Date.now()}`,
        channel: 'push',
        status: 'skipped',
        error: 'Duplicate notification within dedup window',
      };
    }

    try {
      // Get the platform explicitly from notification data
      const platform = notification.data?.platform as 'ios' | 'android' | undefined;
      
      if (!platform) {
        return {
          notificationId: `push-${Date.now()}`,
          channel: 'push',
          status: 'failed',
          error: 'Platform not specified in notification data',
        };
      }

      if (platform === 'ios') {
        // Use APNs for iOS
        const apnsToken = notification.data?.apnsToken as string;
        if (!apnsToken) {
          return {
            notificationId: `push-${Date.now()}`,
            channel: 'push',
            status: 'failed',
            error: 'APNs token not provided for iOS notification',
          };
        }
        
        // Preserve userId and pass apnsToken separately to APNS service
        const apnsResult = await this.apnsService.send(notification);
        if (apnsResult.status === 'sent') {
          const apnsDedupKey = `${notification.userId}:${notification.title}:${notification.body}`;
          this.sentDedup.set(apnsDedupKey, Date.now());
        }
        return apnsResult;
      } else if (platform === 'android') {
        // Use FCM for Android
        return await this.sendViaFCM(notification);
      } else {
        return {
          notificationId: `push-${Date.now()}`,
          channel: 'push',
          status: 'failed',
          error: `Unsupported platform: ${platform}`,
        };
      }
    } catch (error) {
      console.error('Push notification failed:', error);
      return {
        notificationId: `push-${Date.now()}`,
        channel: 'push',
        status: 'failed',
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }

  /**
   * Send notification via Firebase Cloud Messaging (Android)
   */
  private async sendViaFCM(notification: PushNotification): Promise<NotificationResult> {
    try {
      const fcmApp = await getFCMAppAsync();
      const messaging = admin.messaging(fcmApp);

      // Validate payload size (FCM limit is 4KB)
      const payloadSize = JSON.stringify(notification).length;
      if (payloadSize > 4096) {
        return {
          notificationId: `fcm-${Date.now()}`,
          channel: 'push',
          status: 'failed',
          error: `Payload size ${payloadSize} exceeds FCM limit of 4KB`,
        };
      }

      const message: admin.messaging.Message = {
        notification: {
          title: notification.title,
          body: notification.body,
        },
        data: notification.data ?
          Object.fromEntries(
            Object.entries(notification.data).map(([k, v]) => [k, String(v)])
          ) : undefined,
        token: notification.data?.fcmToken as string,
      };

      // Use retry logic for transient failures
      const response = await withRetry(() => messaging.send(message));

      this.sentCount.set(`push:${notification.userId}`,
        (this.sentCount.get(`push:${notification.userId}`) || 0) + 1);

      // Track for deduplication
      const dedupKey = `${notification.userId}:${notification.title}:${notification.body}`;
      this.sentDedup.set(dedupKey, Date.now());

      return {
        notificationId: `fcm-${response}`,
        channel: 'push',
        status: 'sent',
        externalId: response,
        deliveredAt: new Date(),
      };
    } catch (error) {
      console.error('FCM send failed:', error);

      // Handle specific FCM errors
      if (error instanceof admin.messaging.MessagingError) {
        const fcmCode = error.code;

        // Non-retryable errors - invalid token, unregistered device
        if (fcmCode === 'messaging/invalid-registration-token' ||
            fcmCode === 'messaging/registration-token-not-registered' ||
            fcmCode === 'messaging/invalid-argument') {
          return {
            notificationId: `fcm-${Date.now()}`,
            channel: 'push',
            status: 'failed',
            error: `Invalid FCM token: ${error.message}`,
          };
        }
      }

      return {
        notificationId: `fcm-${Date.now()}`,
        channel: 'push',
        status: 'failed',
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }

  /**
   * Send notification specifically via FCM (for Android devices)
   */
  async sendViaFCMOnly(notification: PushNotification): Promise<NotificationResult> {
    return this.sendViaFCM(notification);
  }

  /**
   * Send notification specifically via APNs (for iOS devices)
   */
  async sendViaAPNS(notification: PushNotification): Promise<NotificationResult> {
    return this.apnsService.send(notification);
  }

  /**
   * Send batch notifications with retry logic
   */
  async sendBatch(notifications: PushNotification[]): Promise<NotificationResult[]> {
    const results = await Promise.all(
      notifications.map(n => this.send(n))
    );
    return results;
  }

  /**
   * Get rate limit status
   */
  getRateLimitStatus(): { remaining: number; limit: number } {
    return {
      remaining: config.rateLimits.pushPerMinute - this.sentCount.size,
      limit: config.rateLimits.pushPerMinute,
    };
  }

  /**
   * Test connection to FCM by verifying app initialization
   */
  async testFCMConnection(): Promise<{ success: boolean; message: string }> {
    try {
      const fcmApp = await getFCMAppAsync();

      if (!fcmApp) {
        return {
          success: false,
          message: 'Firebase app not initialized',
        };
      }

      return { success: true, message: 'FCM connection successful' };
    } catch (error) {
      console.error('FCM connection test failed:', error);
      return {
        success: false,
        message: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }

  /**
   * Test connection to APNs by making a real connection test
   */
  async testAPNSConnection(): Promise<{ success: boolean; message: string }> {
    try {
      // Use the APNSService to test actual connection
      return await this.apnsService.testConnection();
    } catch (error) {
      console.error('APNs connection test failed:', error);
      return { 
        success: false, 
        message: error instanceof Error ? error.message : 'Unknown error' 
      };
    }
  }
}
