import { Application } from 'express';
import { Resource } from 'express-automatic-routes';
import { Op } from 'sequelize';
import { Req, Res } from '#interfaces/IApi';
import { queryModifier } from '#middlewares/query-modifier';
import verify from '#middlewares/auth';
import { NotificationMessageProvider } from '#providers/NotificationMessageProvider';
import { NotificationReceiptProvider } from '#providers/NotificationReceiptProvider';
import { UserProvider } from '#providers/UserProvider';
import { validateZod, validateQueryZod } from '#middlewares/validators';
import { NotificationMutateBodySchema } from '#contracts/notification/schema';
import { QueryParamsSchema } from '#contracts/shared';
import { sendSuccess } from '#utils/responseUtils';
import NotificationService from '#services/notification/notificationService';
import { Errors } from '#interfaces/error';

export default (_express: Application) => {
	const notificationMessageProvider = NotificationMessageProvider.getInstance();
	const notificationReceiptProvider = NotificationReceiptProvider.getInstance();
	const userProvider = new UserProvider();
	const notificationService = NotificationService.getInstance();

	return <Resource>{
		get: {
			middleware: [verify, validateQueryZod(QueryParamsSchema), queryModifier],
			handler: async (req: Req, res: Res) => {
				try {
					const userId = req.user?.id as string;
					const filter = req.payload?.filters || {};
					const where = {
						[Op.and]: [
							filter,
							{
								user_id: userId,
								deleted_at: null,
							},
						],
					};

					// PERF-01: Use count query instead of getAll to avoid N+1
					const unreadCount = await notificationReceiptProvider.count({
						user_id: userId,
						read_at: null,
						deleted_at: null,
					});

					const data = await notificationReceiptProvider.getAll({
						...(req.payload || {}),
						where,
						include: [
							{
								association: 'message',
							},
						],
					});

					const unreadSummary = { unread_count: unreadCount };

					return sendSuccess(res, { ...data, ...unreadSummary });
				} catch (error) {
					return res.error(error);
				}
			},
		},
		post: {
			middleware: [verify, queryModifier, validateZod(NotificationMutateBodySchema)],
			handler: async (req: Req, res: Res) => {
				try {
					const payloads = Array.isArray(req.body) ? req.body : [req.body];
					const hasBroadcast = payloads.some((item: any) => item.user_id === null);
					const isAdmin = req.user?.roles?.some((r) => ['admin', 'system_admin'].includes(r));

					// Only admins may send broadcast notifications
					if (hasBroadcast) {
						if (!isAdmin) {
							throw Errors.forbidden();
						}
					}

					// S-01: Non-admins may only send notifications to themselves
					if (!isAdmin) {
						const currentUserId = req.user?.id;
						const hasTargetingOther = payloads.some((item: any) => {
							const ids = Array.isArray(item.user_id) ? item.user_id : [item.user_id];
							return ids.some((id: unknown) => id !== null && id !== currentUserId);
						});
						if (hasTargetingOther) {
							throw Errors.forbidden();
						}
					}

					// PERF-02: For broadcast, use cursor-based approach to avoid loading all users into memory
					// Process notifications in batches instead of collecting all IDs first
					if (hasBroadcast) {
						const BATCH_SIZE = 500;
						let page = 1;
						let hasMore = true;
						let totalProcessed = 0;

						// Get the broadcast payload (first one with user_id === null)
						const broadcastPayload = payloads.find((p: any) => p.user_id === null);

						while (hasMore) {
							const batch = await userProvider.getAll({
								attributes: ['id'],
								raw: true,
								pageSize: BATCH_SIZE,
								page,
							});
							const ids = batch.rows
								.map((u: any) => u.id)
								.filter((id: unknown): id is string => typeof id === 'string' && id.trim().length > 0);

							if (ids.length === 0) {
								hasMore = false;
								break;
							}

							// Process this batch immediately instead of collecting all IDs
							const uniqueUserIds: string[] = Array.from(new Set(ids));

							await notificationService.createAndSend({
								userId: uniqueUserIds,
								title: broadcastPayload?.title,
								body: broadcastPayload?.body,
								type: broadcastPayload?.type || 'system_alert',
								channel: broadcastPayload?.channel || 'in_app',
								platform: broadcastPayload?.platform,
								data: broadcastPayload?.data,
								expiredAt: broadcastPayload?.expired_at,
							});

							totalProcessed += uniqueUserIds.length;
							hasMore = ids.length === BATCH_SIZE;
							page++;
						}

						// B-05: Warn if broadcast targets 0 users
						if (totalProcessed === 0) {
							await notificationMessageProvider.logError(
								new Error('Broadcast notification: no users found to send to'),
							);
						}

						// Skip the regular processing since we already handled broadcast
						return sendSuccess(res, { message: `Broadcast sent to ${totalProcessed} users` });
					}

					// P-05: Run independent payloads concurrently (non-broadcast only)
					const allNotifications = await Promise.all(
						payloads.map(async (item: any) => {
							const userIds = Array.isArray(item.user_id) ? item.user_id : [item.user_id];

							const uniqueUserIds: string[] = Array.from(
								new Set(userIds.filter((id: unknown): id is string => typeof id === 'string' && id.trim().length > 0)),
							);

							return notificationService.createAndSend({
								userId: uniqueUserIds,
								title: item.title,
								body: item.body,
								type: item.type || 'system_alert',
								channel: item.channel || 'in_app',
								platform: item.platform,
								data: item.data,
								expiredAt: item.expired_at,
							});
						}),
					);

					const notifications = allNotifications.flat();

					return sendSuccess(
						res,
						notifications.map((n: any) => n.toJSON()),
					);
				} catch (error) {
					return res.error(error);
				}
			},
		},
	};
};
