/**
 * Notification Sender Worker
 *
 * Background worker that polls for pending notifications and sends them
 * via Telegram with exponential backoff retry logic.
 */

import { prisma } from '../shared/database/prisma.client';
import { logger } from '../shared/utils/logger';
import { telegramClient } from '../modules/notification/telegram/telegram.client';
import { NotificationStatus, NotificationType } from '@prisma/client';

export interface NotificationJobData {
  notificationId: string;
  type: NotificationType;
  priority: number;
  metadata?: Record<string, any>;
}

export interface NotificationJobResult {
  success: boolean;
  notificationId: string;
  messageId?: number;
  error?: string;
  attempts: number;
  processedAt: Date;
}

export interface NotificationQueueStats {
  pending: number;
  sent: number;
  failed: number;
  total: number;
  isRunning: boolean;
  lastPollAt: Date | null;
}

export interface NotificationSenderConfig {
  pollIntervalMs: number;
  batchSize: number;
  maxRetries: number;
  baseRetryDelayMs: number;
  maxRetryDelayMs: number;
}

const DEFAULT_CONFIG: NotificationSenderConfig = {
  pollIntervalMs: parseInt(process.env.NOTIFICATION_POLL_INTERVAL || '5000', 10),
  batchSize: parseInt(process.env.NOTIFICATION_BATCH_SIZE || '10', 10),
  maxRetries: parseInt(process.env.NOTIFICATION_MAX_RETRIES || '3', 10),
  baseRetryDelayMs: parseInt(process.env.NOTIFICATION_BASE_RETRY_DELAY || '1000', 10),
  maxRetryDelayMs: parseInt(process.env.NOTIFICATION_MAX_RETRY_DELAY || '30000', 10),
};

class NotificationSenderWorker {
  private static instance: NotificationSenderWorker;
  private config: NotificationSenderConfig;
  private isRunning: boolean = false;
  private isPaused: boolean = false;
  private pollInterval: NodeJS.Timeout | null = null;
  private lastPollAt: Date | null = null;

  private constructor(config: Partial<NotificationSenderConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  public static getInstance(config?: Partial<NotificationSenderConfig>): NotificationSenderWorker {
    if (!NotificationSenderWorker.instance) {
      NotificationSenderWorker.instance = new NotificationSenderWorker(config);
    }
    return NotificationSenderWorker.instance;
  }

  public start(): void {
    if (this.isRunning) {
      logger.warn('Notification sender worker is already running');
      return;
    }

    this.isRunning = true;
    logger.info({ config: this.config }, 'Starting notification sender worker');

    this.poll();
    this.pollInterval = setInterval(() => {
      this.poll();
    }, this.config.pollIntervalMs);
  }

  public stop(): void {
    if (!this.isRunning) {
      return;
    }

    this.isRunning = false;

    if (this.pollInterval) {
      clearInterval(this.pollInterval);
      this.pollInterval = null;
    }

    logger.info('Notification sender worker stopped');
  }

  public async poll(): Promise<void> {
    if (!this.isRunning || this.isPaused) {
      return;
    }

    this.lastPollAt = new Date();

    try {
      const pendingNotifications = await this.fetchPendingNotifications();

      if (pendingNotifications.length === 0) {
        logger.debug('No pending notifications to process');
        return;
      }

      logger.info({ count: pendingNotifications.length }, 'Processing pending notifications');

      for (const notification of pendingNotifications) {
        await this.processNotification(notification.id);
      }
    } catch (error) {
      logger.error({ error }, 'Error polling for pending notifications');
    }
  }

  public pause(): void {
    if (!this.isRunning) {
      return;
    }
    this.isPaused = true;
    logger.info('Notification worker paused');
  }

  public resume(): void {
    if (!this.isRunning || !this.isPaused) {
      return;
    }
    this.isPaused = false;
    logger.info('Notification worker resumed');
  }

  public async clean(daysOld: number = 30): Promise<number> {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - daysOld);

    const result = await prisma.notification.deleteMany({
      where: {
        status: NotificationStatus.SENT,
        sentAt: { lt: cutoffDate },
      },
    });

    logger.info({ deletedCount: result.count, daysOld }, 'Cleaned old notifications');
    return result.count;
  }

  public async obliterate(): Promise<void> {
    this.stop();
    await prisma.notification.deleteMany({
      where: {
        status: NotificationStatus.SENT,
      },
    });
    logger.warn('Notification queue obliterated - all sent notifications deleted');
  }

  private async fetchPendingNotifications() {
    return await prisma.notification.findMany({
      where: {
        status: NotificationStatus.PENDING,
      },
      orderBy: [
        { priority: 'desc' },
        { createdAt: 'asc' },
      ],
      take: this.config.batchSize,
    });
  }

  private async processNotification(notificationId: string): Promise<void> {
    try {
      const notification = await prisma.notification.findUnique({
        where: { id: notificationId },
      });

      if (!notification) {
        logger.warn({ notificationId }, 'Notification not found');
        return;
      }

      if (notification.status !== NotificationStatus.PENDING) {
        logger.debug({ notificationId, status: notification.status }, 'Notification not pending');
        return;
      }

      await this.incrementAttempt(notificationId);

      const result = await this.sendWithRetry(notification);

      if (result.success) {
        await this.markAsSent(notificationId, result.messageId);
        logger.info({
          notificationId,
          messageId: result.messageId,
          attempts: notification.attempts + 1,
        }, 'Notification sent successfully');
      } else {
        await this.markAsFailed(notificationId, result.error);
        logger.error({
          notificationId,
          error: result.error,
          attempts: notification.attempts + 1,
        }, 'Notification failed after all retries');
      }
    } catch (error) {
      logger.error({ error, notificationId }, 'Error processing notification');
      await this.markAsFailed(notificationId, error instanceof Error ? error.message : 'Unknown error');
    }
  }

  private async sendWithRetry(
    notification: {
      id: string;
      message: string;
      attempts: number;
    }
  ): Promise<{ success: boolean; messageId?: number; error?: string }> {
    let lastError: string | undefined;

    for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
      try {
        const result = await telegramClient.sendToAdmin(notification.message);

        if (result.success) {
          const response: { success: true; messageId?: number } = { success: true };
          if (result.messageId) {
            response.messageId = result.messageId;
          }
          return response;
        }

        lastError = result.error;

        if (attempt < this.config.maxRetries) {
          const delay = this.calculateBackoff(attempt);
          logger.info({
            notificationId: notification.id,
            attempt: attempt + 1,
            maxRetries: this.config.maxRetries,
            delay,
            error: lastError,
          }, 'Retrying notification send');
          await this.sleep(delay);
        }
      } catch (error) {
        lastError = error instanceof Error ? error.message : 'Unknown error';

        if (attempt < this.config.maxRetries) {
          const delay = this.calculateBackoff(attempt);
          logger.info({
            notificationId: notification.id,
            attempt: attempt + 1,
            maxRetries: this.config.maxRetries,
            delay,
            error: lastError,
          }, 'Retrying notification send after error');
          await this.sleep(delay);
        }
      }
    }

    const response: { success: false; error?: string } = { success: false };
    if (lastError) {
      response.error = lastError;
    }
    return response;
  }

  private calculateBackoff(attempt: number): number {
    const delay = this.config.baseRetryDelayMs * Math.pow(2, attempt);
    return Math.min(delay, this.config.maxRetryDelayMs);
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  private async incrementAttempt(notificationId: string): Promise<void> {
    await prisma.notification.update({
      where: { id: notificationId },
      data: {
        attempts: { increment: 1 },
        lastAttemptAt: new Date(),
      },
    });
  }

  private async markAsSent(notificationId: string, messageId?: number): Promise<void> {
    const data: { status: NotificationStatus; sentAt: Date; errorMessage: null; messageId?: number } = {
      status: NotificationStatus.SENT,
      sentAt: new Date(),
      errorMessage: null,
    };
    if (messageId) {
      data.messageId = messageId;
    }
    await prisma.notification.update({
      where: { id: notificationId },
      data,
    });
  }

  private async markAsFailed(notificationId: string, errorMessage?: string): Promise<void> {
    const shouldRetry = await this.shouldRetry(notificationId);

    await prisma.notification.update({
      where: { id: notificationId },
      data: {
        status: shouldRetry ? NotificationStatus.PENDING : NotificationStatus.FAILED,
        errorMessage: errorMessage || null,
      },
    });
  }

  private async shouldRetry(notificationId: string): Promise<boolean> {
    const notification = await prisma.notification.findUnique({
      where: { id: notificationId },
      select: { attempts: true },
    });

    if (!notification) {
      return false;
    }

    return notification.attempts < this.config.maxRetries;
  }

  public async queueNotification(notificationId: string): Promise<void> {
    logger.debug({ notificationId }, 'Notification queued for sending');
  }

  public async retryFailedNotifications(limit: number = 10): Promise<number> {
    try {
      const failedNotifications = await prisma.notification.findMany({
        where: {
          status: NotificationStatus.FAILED,
          attempts: { lt: this.config.maxRetries },
        },
        orderBy: [
          { priority: 'desc' },
          { createdAt: 'asc' },
        ],
        take: limit,
      });

      for (const notification of failedNotifications) {
        await prisma.notification.update({
          where: { id: notification.id },
          data: { status: NotificationStatus.PENDING },
        });
      }

      logger.info({ count: failedNotifications.length }, 'Queued failed notifications for retry');
      return failedNotifications.length;
    } catch (error) {
      logger.error({ error }, 'Error retrying failed notifications');
      return 0;
    }
  }

  public async getStatistics(): Promise<{
    pending: number;
    sent: number;
    failed: number;
    total: number;
  }> {
    const [pending, sent, failed, total] = await Promise.all([
      prisma.notification.count({ where: { status: NotificationStatus.PENDING } }),
      prisma.notification.count({ where: { status: NotificationStatus.SENT } }),
      prisma.notification.count({ where: { status: NotificationStatus.FAILED } }),
      prisma.notification.count(),
    ]);

    return { pending, sent, failed, total };
  }

  public isActive(): boolean {
    return this.isRunning;
  }

  public async getQueueStats(): Promise<NotificationQueueStats> {
    const stats = await this.getStatistics();
    return {
      ...stats,
      isRunning: this.isRunning && !this.isPaused,
      lastPollAt: this.lastPollAt,
    };
  }

  public async getWorkerHealth(): Promise<{
    healthy: boolean;
    isRunning: boolean;
    isPaused: boolean;
    uptime: number;
  }> {
    return {
      healthy: this.isRunning,
      isRunning: this.isRunning,
      isPaused: this.isPaused,
      uptime: this.isRunning ? Date.now() - (this.lastPollAt?.getTime() || Date.now()) : 0,
    };
  }
}

const workerInstance = NotificationSenderWorker.getInstance();

export function startNotificationWorker(): void {
  workerInstance.start();
}

export function stopNotificationWorker(): void {
  workerInstance.stop();
}

export function getNotificationWorker(): NotificationSenderWorker {
  return workerInstance;
}

export async function queueNotification(notificationId: string): Promise<void> {
  return await workerInstance.queueNotification(notificationId);
}

export async function retryFailedNotifications(limit?: number): Promise<number> {
  return await workerInstance.retryFailedNotifications(limit);
}

export async function getWorkerStatistics(): Promise<{
  pending: number;
  sent: number;
  failed: number;
  total: number;
}> {
  return await workerInstance.getStatistics();
}

export function getNotificationQueue(): NotificationSenderWorker {
  return workerInstance;
}

export async function getQueueStats(): Promise<NotificationQueueStats> {
  return await workerInstance.getQueueStats();
}

export function pauseQueue(): void {
  workerInstance.pause();
}

export function resumeQueue(): void {
  workerInstance.resume();
}

export async function cleanQueue(daysOld?: number): Promise<number> {
  return await workerInstance.clean(daysOld);
}

export async function obliterateQueue(): Promise<void> {
  return await workerInstance.obliterate();
}

export async function getWorkerHealth(): Promise<{
  healthy: boolean;
  isRunning: boolean;
  isPaused: boolean;
  uptime: number;
}> {
  return await workerInstance.getWorkerHealth();
}

export { NotificationSenderWorker };
export default workerInstance;
