import type { TFunction } from "i18next";
import pLimit from "p-limit";
import { type Urgency, WebPushError } from "web-push";
import { IS_E2E_TEST_RUN } from "~/utils/e2e";
import { APP_ICON_URL } from "~/utils/urls";
import type { NotificationSubscription } from "../../../db/tables";
import { getFixedTForLanguage } from "../../../modules/i18n/i18next.server";
import { logger } from "../../../utils/logger";
import * as NotificationRepository from "../NotificationRepository.server";
import type { Notification } from "../notifications-types";
import { notificationLink } from "../notifications-utils";
import webPush, { webPushEnabled } from "./webPush.server";

const NOTIFICATION_URGENCY: Record<Notification["type"], Urgency> = {
	SQ_ADDED_TO_GROUP: "high",
	SQ_NEW_MATCH: "high",
	TO_ADDED_TO_TEAM: "normal",
	TO_BRACKET_STARTED: "high",
	TO_CHECK_IN_OPENED: "high",
	TO_TEST_CREATED: "normal",
	TO_LIKE_RECEIVED: "high",
	TO_LIKE_ACCEPTED: "high",
	BADGE_ADDED: "normal",
	BADGE_MANAGER_ADDED: "normal",
	PLUS_VOTING_STARTED: "normal",
	PLUS_SUGGESTION_ADDED: "normal",
	TAGGED_TO_ART: "normal",
	SEASON_STARTED: "normal",
	SCRIM_NEW_REQUEST: "high",
	SCRIM_SCHEDULED: "high",
	SCRIM_CANCELED: "high",
	SCRIM_STARTING_SOON: "high",
	SCRIM_AUTO_DELETED: "normal",
	COMMISSIONS_CLOSED: "normal",
	FRIEND_REQUEST_RECEIVED: "normal",
};

/**
 * Create notifications both in the database and send push notifications to users (if enabled).
 */
export async function notify({
	userIds,
	notification,
	defaultSeenUserIds,
}: {
	/** Array of user ids to notify */
	userIds: Array<number>;
	/** Array of user ids that should have the notification marked as seen by default */
	defaultSeenUserIds?: Array<number>;
	/** Notification to send (same for all users) */
	notification: Notification;
}) {
	if (userIds.length === 0) {
		return;
	}

	const dededuplicatedUserIds = Array.from(new Set(userIds));

	if (isNotificationAlreadySent(notification, dededuplicatedUserIds)) {
		return;
	}

	try {
		await NotificationRepository.insert(
			notification,
			dededuplicatedUserIds.map((userId) => ({
				userId,
				seen: defaultSeenUserIds?.includes(userId) ? 1 : 0,
			})),
		);
	} catch (e) {
		logger.error("Failed to notify users", e);
	}

	const subscriptions = await NotificationRepository.subscriptionsByUserIds(
		dededuplicatedUserIds,
	);
	if (subscriptions.length > 0) {
		const t = await getFixedTForLanguage("en-US", ["common"]);

		const limit = pLimit(50);

		await Promise.all(
			subscriptions.map(({ id, subscription }) =>
				limit(() =>
					sendPushNotification({
						subscription,
						subscriptionId: id,
						notification,
						t,
					}),
				),
			),
		);
	}
}

const sentNotifications = new Set<string>();

export function clearSentNotificationsForTesting() {
	sentNotifications.clear();
}

// deduplicates notifications as a failsafe & anti-abuse mechanism
function isNotificationAlreadySent(
	notification: Notification,
	userIds: Array<number>,
) {
	// e2e tests should not be affected by this
	if (IS_E2E_TEST_RUN) {
		return false;
	}

	// bulk notifications are typically not something you can repeat
	if (userIds.length > 10) {
		return false;
	}

	const sortedUserIds = [...userIds].sort((a, b) => a - b).join(",");
	const key = `${notification.type}-${JSON.stringify(notification.meta)}-${sortedUserIds}`;
	if (sentNotifications.has(key)) {
		return true;
	}
	sentNotifications.add(key);

	if (sentNotifications.size > 10_000) {
		sentNotifications.clear();
	}

	return false;
}

async function sendPushNotification({
	subscription,
	subscriptionId,
	notification,
	t,
}: {
	subscription: NotificationSubscription;
	subscriptionId: number;
	notification: Notification;
	t: TFunction<["common"], undefined>;
}) {
	if (!webPushEnabled) return;

	try {
		await webPush.sendNotification(
			subscription,
			JSON.stringify(pushNotificationOptions(notification, t)),
			{ urgency: NOTIFICATION_URGENCY[notification.type] },
		);
	} catch (err) {
		if (!(err instanceof WebPushError)) {
			logger.error("Failed to send push notification (unknown error)", err);
			// if we get "Not Found" or "Gone" we should delete the subscription as it is expired or no longer valid
		} else if (err.statusCode === 404 || err.statusCode === 410) {
			await NotificationRepository.deleteSubscriptionById(subscriptionId);
		} else {
			logger.error("Failed to send push notification", err);
		}
	}
}

function pushNotificationOptions(
	notification: Notification,
	t: TFunction<["common"], undefined>,
): Parameters<ServiceWorkerRegistration["showNotification"]>[1] & {
	title: string;
} {
	return {
		title: t(`common:notifications.title.${notification.type}`),
		body: t(
			`common:notifications.text.${notification.type}`,
			notification.meta,
		),
		icon: notification.pictureUrl ?? APP_ICON_URL,
		data: { url: notificationLink(notification) },
	};
}
