import { Worker, Job, Queue as _Queue } from 'bullmq';
import Redis from 'ioredis';
import NotificationService from './notificationService';
import { NotificationChannelService } from './notificationChannelService';
import { NotificationDeviceProvider } from '#providers/NotificationDeviceProvider';
import { NotificationDeliveryAttemptProvider } from '#providers/NotificationDeliveryAttemptProvider';
import { NotificationMessageProvider } from '#providers/NotificationMessageProvider';
import { NotificationReceiptProvider } from '#providers/NotificationReceiptProvider';
import { LoggingService } from '#services/file-system/logService';
import CircuitBreakerRegistry, { CircuitBreakerOpenError } from './circuitBreakerService';
import NotificationQueueService, {
	QUEUE_NAMES,
	SendNotificationJobData,
	SendBatchNotificationJobData,
	SendBroadcastJobData,
} from './notificationQueueService';
import { NotificationType, NotificationChannel } from './types';
import Config from '#config';

// Redis connection for BullMQ (Redis A) - lazy initialization
let redisConnection: Redis | null = null;

function getRedisConnection(): Redis {
	if (!redisConnection) {
		redisConnection = new Redis({
			host: Config.redisA.host,
			port: Config.redisA.port,
			password: Config.redisA.password,
			maxRetriesPerRequest: null,
		});
	}
	return redisConnection;
}

class NotificationWorkerService {
	private static instance: NotificationWorkerService;
	private worker: Worker | null = null;
	private notificationService: NotificationService;
	private channelService: NotificationChannelService;
	private logger: LoggingService;
	private circuitBreakerRegistry: CircuitBreakerRegistry;
	private isAvailable: boolean = false;

	static getInstance(): NotificationWorkerService {
		NotificationWorkerService.instance ??= new NotificationWorkerService();
		return NotificationWorkerService.instance;
	}

	private constructor() {
		this.notificationService = NotificationService.getInstance();
		// PERF-03: Create no-op SSE service stub instead of null to prevent runtime errors
		// CODE-QUALITY-02: Use proper interface instead of 'as any'
		const noopSseService = {
			sendToUser: () => false,
			addClient: () => {},
			removeClient: () => {},
			broadcast: () => {},
		};
		this.channelService = new NotificationChannelService(
			NotificationDeviceProvider.getInstance(),
			NotificationDeliveryAttemptProvider.getInstance(),
			noopSseService as any,
		);
		this.logger = new LoggingService();
		this.circuitBreakerRegistry = CircuitBreakerRegistry.getInstance();
	}

	async start(): Promise<void> {
		if (this.worker) {
			this.logger.logInfoAsync('Worker', 'Worker already running', null).catch((err) => {
				// SEC-08: Fallback to console if logging fails
				console.error('[Worker] Failed to log info:', err);
				console.warn('[Worker] Worker already running');
			});
			return;
		}

		// Skip worker initialization if Redis A is disabled
		if (!Config.redisA.enabled) {
			this.isAvailable = false;
			console.warn('[Worker] Redis A disabled by configuration, notification worker disabled');
			return;
		}

		try {
			// Test Redis connection before starting worker
			const redis = getRedisConnection();
			await redis.ping();

			this.worker = new Worker(
				QUEUE_NAMES.NOTIFICATIONS,
				async (job: Job) => {
					await this.processJob(job);
				},
				{
					connection: getRedisConnection(),
					concurrency: Config.notifications.queue?.workerConcurrency ?? 20,
				},
			);

			this.worker.on('completed', (job) => {
				this.logger.logInfoAsync('Worker', `Job ${job.id} completed`, null).catch((err) => {
					// SEC-08: Fallback to console if logging fails
					console.error('[Worker] Failed to log info:', err);
					console.log(`[Worker] Job ${job.id} completed`);
				});
			});

			this.worker.on('failed', (job, error) => {
				this.logger.logErrorAsync('Worker', error as Error, null).catch((err) => {
					// SEC-08: Fallback to console if logging fails
					console.error('[Worker] Failed to log error:', err);
					console.error('[Worker] Job failed:', error);
				});

				// Move to DLQ after max retries
				if (job && job.attemptsMade === job.opts.attempts) {
					this.moveToDLQ(job).catch((err) => {
						// SEC-08: Fallback to console if DLQ move fails
						console.error('[Worker] Failed to move job to DLQ:', err);
					});
				}
			});

			this.isAvailable = true;
			this.logger.logInfoAsync('Worker', 'Notification worker started', null).catch((_err) => {
				// SEC-08: Fallback to console if logging fails
				console.log('[Worker] Notification worker started');
			});
		} catch (error) {
			this.isAvailable = false;
			this.logger.logErrorAsync('Worker', error as Error, null).catch((_err) => {
				// SEC-08: Fallback to console if logging fails
				console.error('[Worker] Failed to start worker:', error);
				console.warn('[Worker] Redis unavailable, notification worker disabled');
			});
		}
	}

	async stop(): Promise<void> {
		if (this.worker) {
			await this.worker.close();
			this.worker = null;
			this.logger.logInfoAsync('Worker', 'Notification worker stopped', null).catch((err) => {
				// SEC-08: Fallback to console if logging fails
				console.error('[Worker] Failed to log info:', err);
				console.log('[Worker] Notification worker stopped');
			});
		}
	}

	isWorkerAvailable(): boolean {
		return this.isAvailable;
	}

	private async processJob(job: Job): Promise<void> {
		const { name, data } = job;

		switch (name) {
			case 'send-notification':
				await this.processSendNotification(data as SendNotificationJobData);
				break;
			case 'send-batch':
				await this.processSendBatch(data as SendBatchNotificationJobData);
				break;
			case 'send-broadcast':
				await this.processSendBroadcast(data as SendBroadcastJobData);
				break;
			default:
				throw new Error(`Unknown job name: ${name}`);
		}
	}

	private async processSendNotification(data: SendNotificationJobData): Promise<void> {
		const { messageId, receiptId, userId, channel, traceId: _traceId, tenantId: _tenantId } = data;

		try {
			// Load message
			const messageProvider = NotificationMessageProvider.getInstance();
			const message = await messageProvider.getOne({
				where: { id: messageId },
			});

			if (!message) {
				throw new Error(`Message not found: ${messageId}`);
			}

			// Check user preferences
			const isEnabled = await this.notificationService.checkUserPreferences(
				userId,
				message.type as NotificationType,
				channel as NotificationChannel,
			);

			if (!isEnabled) {
				this.logger.logInfoAsync('Worker', `Notification disabled for user ${userId}`, null).catch((err) => {
					// SEC-08: Fallback to console if logging fails
					console.error('[Worker] Failed to log info:', err);
					console.log(`[Worker] Notification disabled for user ${userId}`);
				});
				return;
			}

			// Get circuit breaker for this channel
			const circuitBreaker = this.circuitBreakerRegistry.getBreaker(channel);

			// Send via circuit breaker
			await circuitBreaker.execute(async () => {
				// Send based on channel
				switch (channel) {
					case 'in_app':
						await this.channelService.sendViaInApp(userId, message, receiptId);
						break;
					case 'push':
						await this.channelService.sendViaPush(userId, message, receiptId);
						break;
					case 'zalo':
						await this.channelService.sendViaZalo(userId, message, receiptId);
						break;
					case 'email':
						await this.channelService.sendViaEmail(userId, message, receiptId);
						break;
					default:
						throw new Error(`Unsupported channel: ${channel}`);
				}
			});
		} catch (error) {
			// Check if circuit breaker is open
			if (error instanceof CircuitBreakerOpenError) {
				this.logger.logErrorAsync('Worker', new Error(`Circuit breaker open for ${channel}`), null).catch((err) => {
					// SEC-08: Fallback to console if logging fails
					console.error('[Worker] Failed to log error:', err);
					console.error(`[Worker] Circuit breaker open for ${channel}`);
				});
				throw error;
			}
			throw error;
		}
	}

	private async processSendBatch(data: SendBatchNotificationJobData): Promise<void> {
		const { messageId, receiptIds, traceId, tenantId } = data;

		// Process in batches to avoid memory issues
		const batchSize = 100;
		for (let i = 0; i < receiptIds.length; i += batchSize) {
			const batch = receiptIds.slice(i, i + batchSize);

			const promises = batch.map(async (receiptId) => {
				const receiptProvider = NotificationReceiptProvider.getInstance();
				const receipt = await receiptProvider.getOne({
					where: { id: receiptId },
				});

				if (!receipt) return;

				const notificationData: any = {
					messageId,
					receiptId,
					userId: receipt.user_id,
					channel: receipt.channel,
					traceId: traceId || crypto.randomUUID(),
				};
				if (tenantId) notificationData.tenantId = tenantId;

				await this.processSendNotification(notificationData);
			});

			await Promise.allSettled(promises);
		}
	}

	private async processSendBroadcast(data: SendBroadcastJobData): Promise<void> {
		const { messageId, segmentId } = data;

		// For broadcast, we don't create receipts immediately
		// Receipts are created on first interaction (lazy creation)
		// This prevents DB explosion for large broadcasts

		// Load message
		const messageProvider = NotificationMessageProvider.getInstance();
		const message = await messageProvider.getOne({
			where: { id: messageId },
		});

		if (!message) {
			throw new Error(`Message not found: ${messageId}`);
		}

		// Get users in segment
		// This would be implemented with segment logic
		// For now, skip as this is Phase 2 feature

		this.logger.logInfoAsync('Worker', `Broadcast ${messageId} to segment ${segmentId}`, null).catch((err) => {
			// SEC-08: Fallback to console if logging fails
			console.error('[Worker] Failed to log info:', err);
			console.log(`[Worker] Broadcast ${messageId} to segment ${segmentId}`);
		});
	}

	private async moveToDLQ(job: Job): Promise<void> {
		const queueService = NotificationQueueService.getInstance();
		const dlq = queueService.getQueue(QUEUE_NAMES.NOTIFICATIONS_DLQ);
		if (!dlq) {
			this.logger.logErrorAsync('Worker', new Error('DLQ not initialized'), null).catch((err) => {
				// SEC-08: Fallback to console if logging fails
				console.error('[Worker] Failed to log error:', err);
				console.error('[Worker] DLQ not initialized');
			});
			return;
		}

		await dlq.add('failed-job', job.data, {
			attempts: job.attemptsMade,
		});
	}
}

export default NotificationWorkerService;
