import { NotificationMessage } from '#models/NotificationMessage';
import { NotificationMessageProvider } from '#providers/NotificationMessageProvider';
import { NotificationReceiptProvider } from '#providers/NotificationReceiptProvider';
import { NotificationDeviceProvider } from '#providers/NotificationDeviceProvider';
import { NotificationDeliveryAttemptProvider } from '#providers/NotificationDeliveryAttemptProvider';
import { NotificationPreferenceProvider } from '#providers/NotificationPreferenceProvider';
import NotificationSseService from './notificationSseService';
import { NotificationChannelService } from './notificationChannelService';
import { LoggingService } from '#services/file-system/logService';
import { Op } from 'sequelize';
import { NotificationType, NotificationChannel, Platform, DeliveryStatus, CreateNotificationParams } from './types';
import NotificationQueueService, { PRIORITY } from './notificationQueueService';
import Config from '#config';
import { AuditLogService } from '#services/audit/auditLogService';

export type { NotificationType, NotificationChannel, Platform, DeliveryStatus, CreateNotificationParams };

class NotificationService {
	private static instance: NotificationService;
	private notificationProvider: NotificationMessageProvider;
	private receiptProvider: NotificationReceiptProvider;
	private preferenceProvider: NotificationPreferenceProvider;
	private deliveryLogProvider: NotificationDeliveryAttemptProvider;
	private channelService: NotificationChannelService;
	private logger: LoggingService;
	private queueService: NotificationQueueService;

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

	private constructor() {
		this.notificationProvider = NotificationMessageProvider.getInstance();
		this.receiptProvider = NotificationReceiptProvider.getInstance();
		this.preferenceProvider = NotificationPreferenceProvider.getInstance();
		this.deliveryLogProvider = NotificationDeliveryAttemptProvider.getInstance();
		this.channelService = new NotificationChannelService(
			NotificationDeviceProvider.getInstance(),
			NotificationDeliveryAttemptProvider.getInstance(),
			NotificationSseService.getInstance(),
		);
		this.logger = new LoggingService();
		this.queueService = NotificationQueueService.getInstance();
	}

	/**
	 * Main method - centralized create and send notifications
	 * Phase 1: Use BullMQ for async processing instead of synchronous sends
	 * @param params - Notification creation parameters
	 * @returns Array of created notification messages
	 */
	async createAndSend(params: CreateNotificationParams): Promise<NotificationMessage[]> {
		const userIds = Array.isArray(params.userId) ? params.userId : [params.userId];
		const channel = params.channel as NotificationChannel;
		const requestId = crypto.randomUUID();
		const traceId = crypto.randomUUID();

		// Phase 1: Broadcast not supported - require userId
		if (params.userId === null) {
			throw new Error('Broadcast notifications not supported in Phase 1. Provide userId(s).');
		}

		// Idempotency check using dedupe_key
		const dedupeKey = params.data?.dedupe_key as string | undefined;
		if (dedupeKey) {
			const existingMessage = await this.notificationProvider.getOne({
				where: { dedupe_key: dedupeKey },
			});
			if (existingMessage) {
				this.logger
					.logInfoAsync('Notification', `Duplicate notification skipped (dedupe_key: ${dedupeKey})`, null)
					.catch(() => {});
				return [existingMessage];
			}
		}

		// Create one NotificationMessage (shared content)
		const messageId = crypto.randomUUID();
		const message = await this.notificationProvider.create({
			id: messageId,
			title: params.title,
			body: params.body,
			type: params.type,
			data: params.data ?? null,
			tenant_id: params.tenantId ?? null,
			scheduled_at: null,
			priority: 0,
			dedupe_key: dedupeKey ?? null,
			created_at: new Date(),
			updated_at: new Date(),
		});

		// Create receipts for all users
		const receiptRecords = userIds.map((userId) => ({
			id: crypto.randomUUID(),
			message_id: messageId,
			user_id: userId,
			channel,
			read_at: null,
			deleted_at: null,
			archived_at: null,
			created_at: new Date(),
			updated_at: new Date(),
		}));
		const receipts = await this.receiptProvider.bulkCreate(receiptRecords);

		// Queue jobs instead of synchronous sends
		const priority = this.getPriority(params.type as NotificationType);

		if (receipts.length === 1) {
			// Single user - queue individual job
			const notificationData = {
				messageId,
				receiptId: receipts[0]?.id || '',
				userId: userIds[0] || '',
				channel,
			};
			if (params.tenantId) (notificationData as any).tenantId = params.tenantId;

			await this.queueService.sendNotification(notificationData, { priority });

			// Log notification sent
			await AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: userIds[0] || null,
				actorName: 'System',
				actorEmail: 'system@bekind.vn',
				actorRole: 'SYSTEM',
				action: 'NOTIFICATION_SENT',
				module: 'NOTIFICATION',
				entityId: messageId,
				entityType: 'NotificationMessage',
				description: `Notification sent to user ${userIds[0]} via ${channel}`,
				severity: params.type === 'security_alert' ? 'HIGH' : 'LOW',
				payload: {
					type: params.type,
					channel,
					title: params.title,
					userId: userIds[0],
				},
			});
		} else {
			// Multiple users - queue batch job
			const batchData = {
				messageId,
				receiptIds: receipts.map((r) => r.id),
			};
			if (params.tenantId) (batchData as any).tenantId = params.tenantId;

			await this.queueService.sendBatch(batchData, { priority });

			// Log batch notification sent
			await AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: null,
				actorName: 'System',
				actorEmail: 'system@bekind.vn',
				actorRole: 'SYSTEM',
				action: 'NOTIFICATION_BULK_SENT',
				module: 'NOTIFICATION',
				entityId: messageId,
				entityType: 'NotificationMessage',
				description: `Bulk notification sent to ${userIds.length} users via ${channel}`,
				severity: params.type === 'security_alert' ? 'HIGH' : 'MEDIUM',
				payload: {
					type: params.type,
					channel,
					title: params.title,
					userCount: userIds.length,
					tenantId: params.tenantId,
				},
			});
		}

		return [message];
	}

	/**
	 * Get priority based on notification type
	 * @param type - Notification type
	 * @returns Priority number (1=critical, 2=high, 3=low)
	 */
	private getPriority(type: NotificationType): number {
		const priorityMap: Record<string, number> = {
			security_alert: PRIORITY.P1_CRITICAL,
			payment_failed: PRIORITY.P1_CRITICAL,
			order_created: PRIORITY.P2_HIGH,
			payment_success: PRIORITY.P2_HIGH,
			order_shipped: PRIORITY.P2_HIGH,
			order_delivered: PRIORITY.P2_HIGH,
			chat_message: PRIORITY.P2_HIGH,
			account_update: PRIORITY.P2_HIGH,
			system_alert: PRIORITY.P2_HIGH,
			promotion: PRIORITY.P3_LOW,
		};
		return priorityMap[type] || PRIORITY.P2_HIGH;
	}

	/**
	 * Send notification via appropriate channels.
	 * @param notification - The notification message to send
	 * @param userId - The user ID to send to
	 * @param channel - The channel to send through
	 * @param receiptId - The notification receipt ID
	 * @private
	 */
	private async sendNotification(
		notification: NotificationMessage,
		userId: string,
		channel: NotificationChannel,
		receiptId: string,
	): Promise<void> {
		// Check user preferences (including quiet hours)
		const isEnabled = await this.checkUserPreferences(userId, notification.type as NotificationType, channel);
		if (!isEnabled) {
			this.logger
				.logInfoAsync(
					'Notification',
					`Notification disabled for user ${userId}, type ${notification.type}, channel ${channel}`,
					null,
				)
				.catch(() => {});
			return;
		}

		// Send based on channel
		switch (channel) {
			case 'in_app':
				await this.channelService.sendViaInApp(userId, notification, receiptId);
				break;
			case 'push':
				await this.channelService.sendViaPush(userId, notification, receiptId);
				break;
			case 'zalo':
				await this.channelService.sendViaZalo(userId, notification, receiptId);
				break;
			case 'email':
				await this.channelService.sendViaEmail(userId, notification, receiptId);
				break;
			case 'sms':
				this.logger
					.logInfoAsync('Notification', `SMS channel not yet implemented for notification ${notification.id}`, null)
					.catch(() => {});
				await this.channelService.logDelivery({
					receiptId,
					channel: 'sms',
					status: 'failed',
					errorMessage: 'SMS channel not implemented',
				});
				break;
			default:
				this.logger.logInfoAsync('Notification', `Unsupported channel: ${channel}`, null).catch(() => {});
		}
	}

	/**
	 * Check user preferences including quiet hours.
	 * @param userId - The user ID to check preferences for
	 * @param type - The notification type
	 * @param channel - The notification channel
	 * @returns True if notification should be sent, false otherwise
	 */
	async checkUserPreferences(userId: string, type: NotificationType, channel: NotificationChannel): Promise<boolean> {
		try {
			const preference = await this.preferenceProvider.getOne({
				where: {
					user_id: userId,
					notification_type: type,
					channel,
				},
			});

			// If no preference found, default to enabled
			if (!preference) return true;

			// Check if channel/type is disabled
			if (preference.is_enabled === false) return false;

			// Enforce quiet hours if configured
			if (preference.quiet_hours_start && preference.quiet_hours_end) {
				const now = new Date();
				const startParts = preference.quiet_hours_start.split(':');
				const endParts = preference.quiet_hours_end.split(':');
				const startMinutes = Number(startParts[0]) * 60 + Number(startParts[1]);
				const endMinutes = Number(endParts[0]) * 60 + Number(endParts[1]);
				const currentMinutes = now.getHours() * 60 + now.getMinutes();

				// Handle overnight quiet hours (e.g. 22:00 → 08:00)
				const inQuietHours =
					startMinutes > endMinutes
						? currentMinutes >= startMinutes || currentMinutes < endMinutes
						: currentMinutes >= startMinutes && currentMinutes < endMinutes;

				if (inQuietHours) {
					this.logger
						.logInfoAsync(
							'Notification',
							`Quiet hours active for user ${userId} (${preference.quiet_hours_start}\u2013${preference.quiet_hours_end}), skipping ${channel} notification`,
							null,
						)
						.catch(() => {});
					return false;
				}
			}

			return true;
		} catch (error) {
			this.logger.logErrorAsync('Notification', error as Error, null).catch(() => {});
			return true; // Default to enabled on error
		}
	}

	/**
	 * Retry failed notifications
	 */
	async retryFailedNotifications(): Promise<number> {
		try {
			const requestId = crypto.randomUUID();
			const traceId = crypto.randomUUID();

			// Find failed delivery attempts with retry_count < maxRetries
			const _maxRetries = Config.notifications.maxRetries;
			const failedLogs = await this.deliveryLogProvider.getAll({
				where: {
					status: 'failed',
				},
				pageSize: 100,
				page: 1,
				include: [
					{
						association: 'receipt',
						attributes: ['id', 'user_id', 'channel', 'message_id'],
					},
				],
			});

			if (failedLogs.rows.length === 0) return 0;

			// PERF-01: Fix N+1 query - fetch all related messages in a single query
			// Use message_id from delivery logs' receipt instead of receipt_id
			const messageIds = failedLogs.rows.map((r: any) => r.receipt?.message_id).filter(Boolean);
			const messages = await this.notificationProvider.getAll({
				where: {
					id: { [Op.in]: messageIds as string[] },
				},
			});

			const messageMap = new Map(messages.rows.map((m: any) => [m.id, m]));

			// Prepare eligible logs with their messages
			const eligibleLogs = failedLogs.rows
				.map((log: any) => {
					const message = messageMap.get(log.receipt?.message_id);
					if (!message) return null;
					return {
						log,
						message,
						receipt: { user_id: log.receipt?.user_id, channel: log.receipt?.channel, id: log.receipt?.id },
					};
				})
				.filter(Boolean);

			if (eligibleLogs.length === 0) return 0;

			// Log retry attempt
			await AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: null,
				actorName: 'System',
				actorEmail: 'system@bekind.vn',
				actorRole: 'SYSTEM',
				action: 'NOTIFICATION_RETRY',
				module: 'NOTIFICATION',
				description: `Retrying ${eligibleLogs.length} failed notifications`,
				severity: 'MEDIUM',
				payload: {
					count: eligibleLogs.length,
					messageIds,
				},
			});

			// Fire all retries concurrently
			const results = await Promise.allSettled(
				eligibleLogs.map((item: any) =>
					this.sendNotification(
						item.message,
						item.receipt.user_id,
						item.receipt.channel as NotificationChannel,
						item.receipt.id,
					),
				),
			);

			// Prepare bulk update data
			const updates = results.map((result, idx) => {
				const log = eligibleLogs[idx]!.log;
				const newCount = (log.attempt_no ?? 0) + 1;

				if (result.status === 'fulfilled') {
					return {
						id: log.id,
						attempt_no: newCount,
						status: 'sent',
						sent_at: new Date(),
						updated_at: new Date(),
					};
				}

				const errMsg = result.reason instanceof Error ? result.reason.message : 'Retry failed';
				this.logger
					.logErrorAsync('Notification', result.reason instanceof Error ? result.reason : new Error(errMsg), null)
					.catch(() => {});

				return {
					id: log.id,
					attempt_no: newCount,
					status: 'failed',
					failed_at: new Date(),
					error_message: errMsg,
					updated_at: new Date(),
				};
			});

			// Bulk update logs (more efficient than individual updates)
			await Promise.allSettled(updates.map((update) => this.deliveryLogProvider.put(update.id, update as any)));

			const successCount = results.filter((r) => r.status === 'fulfilled').length;

			// Log retry result
			await AuditLogService.enqueueSystemAudit({
				requestId,
				traceId,
				actorId: null,
				actorName: 'System',
				actorEmail: 'system@bekind.vn',
				actorRole: 'SYSTEM',
				action: 'NOTIFICATION_RETRY_RESULT',
				module: 'NOTIFICATION',
				description: `Notification retry completed: ${successCount}/${eligibleLogs.length} successful`,
				severity: successCount === eligibleLogs.length ? 'LOW' : 'MEDIUM',
				payload: {
					total: eligibleLogs.length,
					successful: successCount,
					failed: eligibleLogs.length - successCount,
				},
			});

			return successCount;
		} catch (error) {
			this.logger
				.logErrorAsync('Notification', error instanceof Error ? error : new Error(String(error)), null)
				.catch(() => {});
			return 0;
		}
	}
}

export default NotificationService;
