import { NotificationMessage } from '#models/NotificationMessage';
import { User } from '#models/User';
import { NotificationDevice } from '#models/NotificationDevice';
import { NotificationDeviceProvider } from '#providers/NotificationDeviceProvider';
import { NotificationDeliveryAttemptProvider } from '#providers/NotificationDeliveryAttemptProvider';
import NotificationSseService from './notificationSseService';
import OneSignalService from './notificationPushService';
import MailService from './notificationEmailService';
import { LoggingService } from '#services/file-system/logService';
import { createSimpleRateLimit } from '#middlewares/rate-limiter';
import Config from '#config';
import {
	NotificationChannel,
	Platform,
	DeliveryResult,
	DeliveryStatus as _DeliveryStatus,
	ZaloOaApiResponse,
	ZaloZnsApiResponse as _ZaloZnsApiResponse,
} from './types';

const logger = new LoggingService();

// Rate limiting configuration (CODE-QUALITY-03: moved to environment variables with defaults)
const RATE_LIMIT_WINDOW_MS = Config.notifications.rateLimiting.rateLimitWindowMs;
const RATE_LIMIT_MAX_REQUESTS = {
	onesignal: Config.notifications.rateLimiting.rateLimitOnesignalMax,
	zalo: Config.notifications.rateLimiting.rateLimitZaloMax,
	email: Config.notifications.rateLimiting.rateLimitEmailMax,
};

// Distributed rate limiters using Redis (PERF-02: replaced in-memory with distributed)
const onesignalRateLimit = createSimpleRateLimit({
	windowMs: RATE_LIMIT_WINDOW_MS,
	max: RATE_LIMIT_MAX_REQUESTS.onesignal,
});
const zaloRateLimit = createSimpleRateLimit({ windowMs: RATE_LIMIT_WINDOW_MS, max: RATE_LIMIT_MAX_REQUESTS.zalo });
const emailRateLimit = createSimpleRateLimit({ windowMs: RATE_LIMIT_WINDOW_MS, max: RATE_LIMIT_MAX_REQUESTS.email });

// CODE-QUALITY-02: Define proper interface for SSE service stub instead of using 'as any'
interface _NoOpSseService {
	sendToUser: (userId: string, event: string, data: unknown) => boolean;
	addClient: (userId: string, res: any) => void;
	removeClient: (userId: string) => void;
	broadcast: (event: string, data: unknown) => void;
}

/**
 * Handles sending notifications via each delivery channel (SSE, push, email, Zalo).
 * Also owns device-loading and delivery-log helpers used by the channel methods.
 */
export class NotificationChannelService {
	constructor(
		private deviceProvider: NotificationDeviceProvider,
		private deliveryLogProvider: NotificationDeliveryAttemptProvider,
		private sseService: NotificationSseService,
	) {}

	// ─── Email ──────────────────────────────────────────────────────────────────

	/**
	 * Send notification via email channel
	 * @param userId - The user ID to send to
	 * @param notification - The notification message to send
	 * @param receiptId - The notification receipt ID
	 * @returns True if sent successfully, false otherwise
	 */
	async sendViaEmail(userId: string, notification: NotificationMessage, receiptId: string): Promise<boolean> {
		try {
			// Rate limiting check (PERF-02: using distributed rate limiter)
			if (!(await emailRateLimit(userId))) {
				await this.logDelivery({
					receiptId,
					channel: 'email',
					status: 'failed',
					errorMessage: 'Rate limit exceeded for email',
				});
				return false;
			}

			// Q-07: Use MailService singleton to reuse SMTP transporter
			const mailService = MailService.getInstance();
			const fromAddress = Config.email.user;
			if (!fromAddress) {
				await this.logDelivery({
					receiptId,
					channel: 'email',
					status: 'failed',
					errorMessage: 'EMAIL_USER not configured',
				});
				return false;
			}

			const user = await User.findByPk(userId, { attributes: ['email'] });
			if (!user?.email) {
				await this.logDelivery({
					receiptId,
					channel: 'email',
					status: 'failed',
					errorMessage: 'User email not found',
				});
				return false;
			}

			// Validate email format
			const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
			if (!emailRegex.test(user.email)) {
				await this.logDelivery({
					receiptId,
					channel: 'email',
					status: 'failed',
					errorMessage: 'Invalid email format',
				});
				return false;
			}

			// Additional email validation
			const email = user.email.trim().toLowerCase();
			if (email.length > 254) {
				await this.logDelivery({
					receiptId,
					channel: 'email',
					status: 'failed',
					errorMessage: 'Email address too long',
				});
				return false;
			}

			await mailService.sendmail({
				from: fromAddress,
				to: email,
				subject: notification.title ?? 'Thông báo',
				text: notification.body ?? '',
			});

			await this.logDelivery({ receiptId, channel: 'email', status: 'sent' });
			return true;
		} catch (error) {
			await this.logDelivery({
				receiptId,
				channel: 'email',
				status: 'failed',
				errorMessage: error instanceof Error ? error.message : 'Unknown error',
			});
			return false;
		}
	}

	// ─── SSE ────────────────────────────────────────────────────────────────────

	/**
	 * Send notification via in-app channel (SSE)
	 * @param userId - The user ID to send to
	 * @param notification - The notification message to send
	 * @param receiptId - The notification receipt ID
	 * @returns True if sent successfully, false otherwise
	 */
	async sendViaInApp(userId: string, notification: NotificationMessage, receiptId: string): Promise<boolean> {
		try {
			const wasSent = this.sseService.sendToUser(userId, 'notification', {
				id: notification.id,
				title: notification.title,
				body: notification.body,
				type: notification.type,
				data: notification.data,
				is_read: null,
				expired_at: null,
				created_at: notification.created_at,
			});

			if (wasSent) {
				await this.logDelivery({ receiptId, channel: 'in_app', status: 'delivered' });
			}

			return wasSent;
		} catch (error) {
			await this.logDelivery({
				receiptId,
				channel: 'in_app',
				status: 'failed',
				errorMessage: error instanceof Error ? error.message : 'Unknown error',
			});
			return false;
		}
	}

	// ─── Push (OneSignal) ────────────────────────────────────────────────────────

	/**
	 * Send notification via push channel (OneSignal)
	 * @param userId - The user ID to send to
	 * @param notification - The notification message to send
	 * @param receiptId - The notification receipt ID
	 * @returns Array of delivery results for each device
	 */
	async sendViaPush(userId: string, notification: NotificationMessage, receiptId: string): Promise<DeliveryResult[]> {
		const results: DeliveryResult[] = [];

		try {
			// Rate limiting check (PERF-02: using distributed rate limiter)
			if (!(await onesignalRateLimit(userId))) {
				results.push({
					channel: 'push',
					status: 'failed',
					errorMessage: 'Rate limit exceeded for OneSignal',
				});
				return results;
			}

			// Load all mobile devices (iOS and Android)
			const devices = await this.loadUserDevices(userId);

			for (const device of devices) {
				if (!device.one_signal_player_id) {
					results.push({
						deviceId: device.id,
						channel: 'push',
						status: 'failed',
						errorMessage: 'No OneSignal player ID',
					});
					continue;
				}

				try {
					const response = await OneSignalService.sendNotification({
						player_ids: [device.one_signal_player_id].filter((id): id is string => Boolean(id)),
						headings: { en: notification.title || '' },
						contents: { en: notification.body || '' },
						data: { notificationId: notification.id, type: notification.type, ...notification.data },
					});

					if (typeof response === 'object' && response && response.id) {
						results.push({ deviceId: device.id, channel: 'push', status: 'sent' });
						await this.logDelivery({
							receiptId,
							deviceId: device.id,
							channel: 'push',
							status: 'sent',
						});
					} else {
						results.push({ deviceId: device.id, channel: 'push', status: 'failed', errorMessage: 'OneSignal failed' });
					}
				} catch (error) {
					results.push({
						deviceId: device.id,
						channel: 'push',
						status: 'failed',
						errorMessage: error instanceof Error ? error.message : 'Unknown error',
					});
				}
			}
		} catch (error) {
			logger.logErrorAsync('Notification', error as Error, null).catch(() => {});
		}

		return results;
	}

	// ─── Zalo ────────────────────────────────────────────────────────────────────

	/**
	 * Send notification via Zalo mini app channel
	 * @param userId - The user ID to send to
	 * @param notification - The notification message to send
	 * @param receiptId - The notification receipt ID
	 * @returns True if sent successfully, false otherwise
	 */
	async sendViaZalo(userId: string, notification: NotificationMessage, receiptId: string): Promise<boolean> {
		try {
			// Rate limiting check (PERF-02: using distributed rate limiter)
			if (!(await zaloRateLimit(userId))) {
				await this.logDelivery({
					receiptId,
					channel: 'zalo',
					status: 'failed',
					errorMessage: 'Rate limit exceeded for Zalo',
				});
				return false;
			}

			const zaloDevices = await this.loadUserDevices(userId, 'zalo_miniapp' as Platform);

			if (zaloDevices.length === 0) {
				await this.logDelivery({
					receiptId,
					channel: 'zalo',
					status: 'failed',
					errorMessage: 'No Zalo devices found',
				});
				return false;
			}

			let successCount = 0;

			for (const device of zaloDevices) {
				if (!device.device_id) continue;

				try {
					const oaResult = await this.sendViaZaloOA(device.device_id, notification);
					if (oaResult) {
						successCount++;
						await this.logDelivery({
							receiptId,
							deviceId: device.id,
							channel: 'zalo',
							status: 'sent',
						});
					} else {
						// Phase 2: Implement ZNS fallback with phone number
						// Currently removed because device.device_id is Zalo user ID, not phone
						await this.logDelivery({
							receiptId,
							deviceId: device.id,
							channel: 'zalo',
							status: 'failed',
							errorMessage: 'Zalo OA failed',
						});
					}
				} catch (error) {
					await this.logDelivery({
						receiptId,
						deviceId: device.id,
						channel: 'zalo',
						status: 'failed',
						errorMessage: error instanceof Error ? error.message : 'Unknown error',
					});
				}
			}

			return successCount > 0;
		} catch (error) {
			await this.logDelivery({
				receiptId,
				channel: 'zalo',
				status: 'failed',
				errorMessage: error instanceof Error ? error.message : 'Unknown error',
			});
			return false;
		}
	}

	private async sendViaZaloOA(zaloUserId: string, notification: NotificationMessage): Promise<boolean> {
		try {
			const accessToken = Config.zalo.oaAccessToken;
			if (!accessToken || accessToken.trim() === '') {
				logger
					.logErrorAsync('Notification', new Error('ZALO_OA_ACCESS_TOKEN not configured or empty'), null)
					.catch(() => {});
				return false;
			}

			// Validate token format - Zalo access tokens are typically alphanumeric with specific patterns
			// Basic validation: should be alphanumeric, reasonable length, no whitespace
			if (accessToken.length < 10 || accessToken.length > 200) {
				logger
					.logErrorAsync('Notification', new Error('ZALO_OA_ACCESS_TOKEN appears to be invalid (invalid length)'), null)
					.catch(() => {});
				return false;
			}

			if (!/^[a-zA-Z0-9_-]+$/.test(accessToken)) {
				logger
					.logErrorAsync(
						'Notification',
						new Error('ZALO_OA_ACCESS_TOKEN appears to be invalid (contains invalid characters)'),
						null,
					)
					.catch(() => {});
				return false;
			}

			// Validate Zalo user ID
			if (!zaloUserId || typeof zaloUserId !== 'string' || zaloUserId.trim() === '') {
				logger.logErrorAsync('Notification', new Error('Invalid Zalo user ID'), null).catch(() => {});
				return false;
			}

			const response = await fetch('https://openapi.zalo.me/v2.0/oa/message', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
					Authorization: `Bearer ${accessToken}`,
				},
				body: JSON.stringify({
					recipient: {
						user_id: zaloUserId,
					},
					message: {
						text: notification.body,
					},
				}),
			});

			const data = (await response.json()) as ZaloOaApiResponse;
			return data.error === 0;
		} catch (error) {
			logger.logErrorAsync('Notification', error as Error, null).catch(() => {});
			return false;
		}
	}

	// ─── Shared helpers ──────────────────────────────────────────────────────────

	/**
	 * Load user devices for notification delivery
	 * @param userId - The user ID to load devices for
	 * @param platform - Optional platform filter
	 * @param limit - Maximum number of devices to return (default: 100)
	 * @returns Array of active user devices
	 */
	async loadUserDevices(userId: string, platform?: Platform, limit: number = 100): Promise<NotificationDevice[]> {
		try {
			const where: Record<string, unknown> = { user_id: userId, is_active: true };
			if (platform) where.platform = platform;

			const result = await this.deviceProvider.getAll({
				where,
				sortField: 'last_used_at',
				sortOrder: 'DESC',
				pageSize: limit,
				page: 1,
			});
			return result.rows;
		} catch (error) {
			logger.logErrorAsync('Notification', error as Error, null).catch(() => {});
			return [];
		}
	}

	/**
	 * Log delivery attempt to database
	 * @param params - Delivery attempt parameters
	 * @private
	 */
	async logDelivery(params: {
		receiptId: string;
		channel: NotificationChannel;
		status: string;
		errorMessage?: string;
		deviceId?: string;
		attemptNo?: number;
	}): Promise<void> {
		try {
			await this.deliveryLogProvider.create({
				id: crypto.randomUUID(),
				receipt_id: params.receiptId,
				device_id: params.deviceId ?? null,
				channel: params.channel,
				status: params.status as 'queued' | 'processing' | 'sent' | 'delivered' | 'failed' | 'cancelled' | 'expired',
				error_message: params.errorMessage ?? null,
				attempt_no: params.attemptNo ?? 1,
				sent_at: params.status === 'sent' ? new Date() : null,
				delivered_at: params.status === 'delivered' ? new Date() : null,
				failed_at: params.status === 'failed' ? new Date() : null,
				created_at: new Date(),
			});
		} catch (error) {
			logger.logErrorAsync('Notification', error as Error, null).catch(() => {});
		}
	}
}
