import { EmailService } from './email.service';
import { SMSService } from './sms.service';
import { PushService } from './push.service';
import { TemplateService } from './template.service';
import { RedisService } from './redis.service';
import { loadNotificationConfig } from '../config/notification.config';
import type {
  Notification,
  NotificationChannel,
  NotificationResult,
  NotificationPreference,
  DeduplicationKey
} from '../types/notification.types';
import type { TemplateResolutionOptions } from '../types/template.types';

export interface RateLimitResult {
  allowed: boolean;
  currentCount: number;
  limit: number;
  remaining: number;
  resetInSeconds: number;
}

interface DeduplicationEntry {
  externalIds: Set<string>;
  expiresAt: number;
}

export class NotificationService {
  private static instance: NotificationService;
  private emailService: EmailService;
  private smsService: SMSService;
  private pushService: PushService;
  private redisService: RedisService;
  private config: ReturnType<typeof loadNotificationConfig>;
  private pendingDeduplication = new Map<string, DeduplicationEntry>();
  private preferenceCache = new Map<string, NotificationPreference>();

  private constructor() {
    this.emailService = EmailService.getInstance();
    this.smsService = SMSService.getInstance();
    this.pushService = PushService.getInstance();
    this.config = loadNotificationConfig();
    this.redisService = RedisService.getInstance({ url: this.config.redis.url });
  }

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

  async send(notification: Notification): Promise<NotificationResult> {
    switch (notification.channel) {
      case 'email':
        return this.emailService.send(notification);
      case 'sms':
        return this.smsService.send(notification);
      case 'push':
        return this.pushService.send(notification);
      default:
        throw new Error(`Unknown notification channel: ${(notification as any).channel}`);
    }
  }

  async sendWithDeduplication(
    notification: Notification,
    dedupKey: DeduplicationKey
  ): Promise<NotificationResult> {
    const dedupId = `${dedupKey.userId}:${dedupKey.templateId}:${dedupKey.key}`;
    const windowSeconds = dedupKey.windowSeconds || this.config.redis.dedupWindowSeconds;
    const entry = this.pendingDeduplication.get(dedupId);

    if (entry && Date.now() < entry.expiresAt && entry.externalIds.size > 0) {
      return {
        notificationId: `dedup-${Date.now()}`,
        channel: notification.channel,
        status: 'pending',
        error: 'Duplicate notification within deduplication window',
      };
    }

    const result = await this.send(notification);

    if (result.status === 'sent') {
      const now = Date.now();
      this.pendingDeduplication.set(dedupId, {
        externalIds: new Set([result.externalId!]),
        expiresAt: now + windowSeconds * 1000,
      });
    }

    return result;
  }

  async setPreference(
    userId: string,
    channel: NotificationPreference['channel'],
    enabled: boolean,
    categories?: string[]
  ): Promise<NotificationPreference> {
    const preference: NotificationPreference = {
      userId,
      channel,
      enabled,
      categories: categories || [],
      updatedAt: new Date(),
    };

    this.preferenceCache.set(`${userId}:${channel}`, preference);
    return preference;
  }

  async getPreference(
    userId: string,
    channel: NotificationPreference['channel']
  ): Promise<NotificationPreference | null> {
    return this.preferenceCache.get(`${userId}:${channel}`) || null;
  }

  async shouldSend(
    userId: string,
    channel: NotificationPreference['channel'],
    category: string
  ): Promise<boolean> {
    const preference = await this.getPreference(userId, channel);
    
    if (!preference) {
      return true;
    }

    if (!preference.enabled) {
      return false;
    }

    if (preference.categories.length === 0) {
      return true;
    }

    return preference.categories.includes(category);
  }

 async sendWithPreferences(
    notification: Notification,
    category: string
  ): Promise<NotificationResult | null> {
    const userId = notification.channel === 'push'
      ? notification.userId
      : `user-${Date.now()}`;

    const shouldSend = await this.shouldSend(
      userId,
      notification.channel,
      category
    );

    if (!shouldSend) {
      return {
        notificationId: `pref-${Date.now()}`,
        channel: notification.channel,
        status: 'pending',
        error: 'Notification disabled for user preference',
      };
    }

    return this.send(notification);
  }

  async sendWithTemplate(
    recipient: string,
    options: TemplateResolutionOptions & { channel?: NotificationChannel }
  ): Promise<NotificationResult> {
    const channel = options.channel || 'email';
    const templateService = TemplateService.getInstance();

    const resolved = templateService.resolveTemplate({
      templateId: options.templateId,
      locale: options.locale,
      variables: options.variables,
      fallbackLocale: options.fallbackLocale,
    });

    if (!resolved) {
      return {
        notificationId: `${channel}-${Date.now()}`,
        channel,
        status: 'failed',
        error: `Template not found: ${options.templateId}`,
      };
    }

    if (resolved.channel !== channel) {
      return {
        notificationId: `${channel}-${Date.now()}`,
        channel,
        status: 'failed',
        error: `Template ${options.templateId} is for channel '${resolved.channel}', not '${channel}'`,
      };
    }

    switch (channel) {
      case 'email':
        return this.emailService.sendWithTemplate(recipient, options);
      case 'sms':
        return this.smsService.send({
          channel: 'sms',
          to: recipient,
          body: resolved.body,
        });
      case 'push':
        return this.pushService.send({
          channel: 'push',
          userId: recipient,
          title: resolved.subject || '',
          body: resolved.body,
        });
      default:
        return {
          notificationId: `${channel}-${Date.now()}`,
          channel,
          status: 'failed',
          error: `Unknown channel: ${channel}`,
        };
    }
  }

  async checkRateLimit(
    identifier: string,
    channel: NotificationChannel,
    customLimit?: number,
    customWindowSeconds?: number
  ): Promise<RateLimitResult> {
    const limit = customLimit || this.getLimitForChannel(channel);
    const windowSeconds = customWindowSeconds || this.config.rateLimits.windowSeconds;
    const key = `rl:${channel}:${identifier}`;

    const currentCount = await this.redisService.increment(key, windowSeconds);
    const ttl = await this.redisService.getTTL(key);

    return {
      allowed: currentCount <= limit,
      currentCount,
      limit,
      remaining: Math.max(0, limit - currentCount),
      resetInSeconds: Math.max(1, ttl),
    };
  }

  async deduplicateNotification(
    dedupKey: DeduplicationKey,
    customWindowSeconds?: number
  ): Promise<boolean> {
    const dedupId = `dedup:${dedupKey.userId}:${dedupKey.templateId}:${dedupKey.key}`;
    const windowSeconds = customWindowSeconds || dedupKey.windowSeconds || this.config.redis.dedupWindowSeconds;

    const wasSet = await this.redisService.setIfNotExists(dedupId, '1', windowSeconds);
    return wasSet;
  }

  getRateLimitConfig(): {
    emailPerMinute: number;
    smsPerMinute: number;
    pushPerMinute: number;
    windowSeconds: number;
  } {
    return {
      emailPerMinute: this.config.rateLimits.emailPerMinute,
      smsPerMinute: this.config.rateLimits.smsPerMinute,
      pushPerMinute: this.config.rateLimits.pushPerMinute,
      windowSeconds: this.config.rateLimits.windowSeconds,
    };
  }

  getTemplateService(): TemplateService {
    return TemplateService.getInstance();
  }

  private getLimitForChannel(channel: NotificationChannel): number {
    switch (channel) {
      case 'email':
        return this.config.rateLimits.emailPerMinute;
      case 'sms':
        return this.config.rateLimits.smsPerMinute;
      case 'push':
        return this.config.rateLimits.pushPerMinute;
    }
  }
}
