import { BadRequestError } from "http-errors-enhanced";
import type { NotificationConfig } from "~/schemas/notifications";

type ShoutrrrTargetGetter = (url: URL, scheme: string) => string | null;

const getSearchParam = (url: URL, name: string) => {
	const lowerName = name.toLowerCase();
	for (const [key, value] of url.searchParams.entries()) {
		if (key.toLowerCase() === lowerName) {
			return value;
		}
	}
	return null;
};

const getHttpTarget = (url: URL, useHttp: boolean) => `${useHttp ? "http" : "https"}://${url.host}`;
const getSchemeTarget = (url: URL, scheme: string, defaultHost?: string) => {
	const host = url.host || defaultHost;
	return host ? `${scheme}://${host}` : null;
};

const fixedProviderShoutrrrSchemes = new Set([
	"discord",
	"googlechat",
	"hangouts",
	"ifttt",
	"join",
	"logger",
	"notifiarr",
	"pushover",
	"pushbullet",
	"slack",
	"teams",
	"telegram",
	"twilio",
	"wecom",
]);

const customShoutrrrTargets: Record<string, ShoutrrrTargetGetter> = {
	bark: (url) => getHttpTarget(url, getSearchParam(url, "scheme") === "http"),
	generic: (url) => getHttpTarget(url, getSearchParam(url, "disabletls") === "yes"),
	gotify: (url) => getHttpTarget(url, getSearchParam(url, "DisableTLS") === "true"),
	lark: (url) => getHttpTarget(url, false),
	matrix: (url) => getHttpTarget(url, getSearchParam(url, "disableTLS") === "yes"),
	mattermost: (url) => getHttpTarget(url, getSearchParam(url, "disabletls") === "yes"),
	mqtt: (url, scheme) => getSchemeTarget(url, scheme, "localhost"),
	mqtts: (url, scheme) => getSchemeTarget(url, scheme, "localhost"),
	ntfy: (url) => {
		if (url.hostname === "ntfy.sh") {
			return null;
		}

		return getHttpTarget(url, getSearchParam(url, "scheme") === "http");
	},
	opsgenie: (url) => getHttpTarget(url, false),
	pagerduty: (url) => getHttpTarget(url, false),
	rocketchat: (url) => getHttpTarget(url, false),
	signal: (url) => getHttpTarget(url, getSearchParam(url, "disabletls") === "yes"),
	smtp: (url) => getSchemeTarget(url, "smtp"),
	zulip: (url) => getHttpTarget(url, false),
};

const getComparableTarget = (target: string) => {
	if (!URL.canParse(target)) {
		return null;
	}

	const url = new URL(target);
	if (url.origin !== "null") {
		return url.origin;
	}

	return url.host ? `${url.protocol}//${url.host}` : null;
};

const isAllowedNotificationTarget = (target: string, allowedTargets: readonly string[]) => {
	const comparableTarget = getComparableTarget(target);
	return (
		comparableTarget !== null &&
		allowedTargets.some((allowedTarget) => getComparableTarget(allowedTarget) === comparableTarget)
	);
};

const assertTargetAllowed = (target: string, allowedTargets: readonly string[]) => {
	if (!isAllowedNotificationTarget(target, allowedTargets)) {
		throw new BadRequestError(
			`Notification webhook URL origin is not allowed. Add ${getComparableTarget(target) ?? target} to WEBHOOK_ALLOWED_ORIGINS.`,
		);
	}
};

const getCustomShoutrrrTarget = (shoutrrrUrl: string) => {
	if (!URL.canParse(shoutrrrUrl)) {
		throw new BadRequestError("Invalid custom Shoutrrr URL");
	}

	const parsedUrl = new URL(shoutrrrUrl);
	const scheme = parsedUrl.protocol.slice(0, -1).toLowerCase();

	if (scheme === "generic+http" || scheme === "generic+https") {
		return `${scheme.slice("generic+".length)}://${parsedUrl.host}`;
	}

	if (fixedProviderShoutrrrSchemes.has(scheme)) {
		return null;
	}

	const getTarget = customShoutrrrTargets[scheme];
	if (!getTarget) {
		throw new BadRequestError(`Custom Shoutrrr scheme "${scheme}" is not supported by the SSRF policy.`);
	}

	return getTarget(parsedUrl, scheme);
};

const getNotificationTarget = (notificationConfig: NotificationConfig) => {
	switch (notificationConfig.type) {
		case "email":
		case "slack":
		case "discord":
		case "pushover":
		case "telegram":
			return null;
		case "generic":
			return notificationConfig.url;
		case "gotify":
			return notificationConfig.serverUrl;
		case "ntfy":
			return notificationConfig.serverUrl ?? null;
		case "custom":
			return getCustomShoutrrrTarget(notificationConfig.shoutrrrUrl);
		default: {
			const _exhaustive: never = notificationConfig;
			throw new BadRequestError(
				`Unsupported notification type "${(_exhaustive as NotificationConfig).type}" for the SSRF policy.`,
			);
		}
	}
};

export const assertNotificationTargetAllowed = (
	notificationConfig: NotificationConfig,
	allowedTargets: readonly string[],
) => {
	const target = getNotificationTarget(notificationConfig);
	if (!target) {
		return;
	}

	assertTargetAllowed(target, allowedTargets);
};
