import { eq } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { db } from "../db/client.js";
import { userSettings } from "../db/schema.js";
import { getDateLocale, getFooterPlain, getTranslations, type Language, t } from "../i18n/translations.js";
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
import { env } from "../plugins/env.js";
import {
	createTestNotificationActionContext,
	storeNotificationActionGroupNtfyMessageId,
} from "../services/notification-actions-service.js";
import {
	type PushNotificationOptions,
	renderNotificationActionPayload,
} from "../services/notifications/action-renderer.js";
import { getSmtpConfig, sendEmailNotification } from "../services/notifications/delivery.js";
import {
	classifyTestEmailFailure,
	getAllUserSettingsFromDb,
	getAvailableTimezones,
	getDefaultSettings,
	getNotificationProvider,
	loadUserSettingsFromDb,
	normalizeSettingsTimezone,
	sanitizeNotificationUrl,
	type UserSettings,
	validateNotificationHostname,
} from "../services/settings-service.js";
import type { AuthUser } from "../types/fastify.js";

export type { UserSettings } from "../services/settings-service.js";

type SettingsBody = {
	timezone: string;
	emailEnabled: boolean;
	notificationEmail: string;
	reminderDaysBefore: number;
	repeatDailyReminders: boolean;
	lowStockDays: number;
	normalStockDays: number;
	highStockDays: number;
	shoutrrrEnabled: boolean;
	shoutrrrUrl: string;
	emailStockReminders: boolean;
	emailIntakeReminders: boolean;
	emailPrescriptionReminders: boolean;
	shoutrrrStockReminders: boolean;
	shoutrrrIntakeReminders: boolean;
	shoutrrrPrescriptionReminders: boolean;
	skipRemindersForTakenDoses: boolean;
	repeatRemindersEnabled: boolean;
	reminderRepeatIntervalMinutes: number;
	maxNaggingReminders: number;
	language: string;
	stockCalculationMode: "automatic" | "manual";
	shareMedicationOverview: boolean;
	upcomingTodayOnly: boolean;
	shareScheduleTodayOnly: boolean;
	swapDashboardMainSections: boolean;
};

type TestEmailBody = {
	email: string;
};

type TestShoutrrrBody = {
	url: string;
};

const settingsEndpointSecurity: ReadonlyArray<Record<string, readonly string[]>> = [
	{ bearerAuth: [] },
	{ cookieAuth: [] },
];
const settingsErrorSchema = {
	type: "object",
	properties: {
		error: { type: "string" },
		code: { type: "string" },
	},
};

function envInt(key: string, defaultVal: number): number {
	const val = process.env[key];
	if (val === undefined) return defaultVal;
	const parsed = parseInt(val, 10);
	return Number.isNaN(parsed) ? defaultVal : parsed;
}

function getLanguage(language: string | null | undefined): Language {
	return language === "de" ? "de" : "en";
}

function buildInteractiveTestPushNotification(language: Language): { title: string; message: string } {
	const tr = getTranslations(language);
	const reminderAt = new Date(Date.now() + 60 * 1000);
	const reminderTime = new Intl.DateTimeFormat(getDateLocale(language), {
		hour: "2-digit",
		minute: "2-digit",
	}).format(reminderAt);

	return {
		title: t(tr.push.intakeTitle, { minutes: 1 }),
		message: `• MedAssist-ng Test: 1 ${tr.common.pill} (100 mg) @ ${reminderTime}\n\n---\n${getFooterPlain(language)}`,
	};
}

async function getOrCreateUserSettings(userId: number) {
	let [settings] = await db.select().from(userSettings).where(eq(userSettings.userId, userId));

	if (!settings) {
		[settings] = await db
			.insert(userSettings)
			.values({
				userId,
				...getDefaultSettings(),
			})
			.returning();
	}

	return settings;
}

// Export for use in reminder scheduler
export async function loadUserSettings(userId: number): Promise<UserSettings> {
	return loadUserSettingsFromDb(userId);
}

// Get all users with settings for scheduler
export async function getAllUserSettings(): Promise<UserSettings[]> {
	return getAllUserSettingsFromDb();
}

export async function settingsRoutes(app: FastifyInstance) {
	// All settings routes require auth
	app.addHook("preHandler", requireAuth);

	// Helper to get user ID from request
	// Returns anonymous user ID when auth is disabled
	async function getUserId(request: FastifyRequest, reply: FastifyReply): Promise<number> {
		// If auth is disabled, use the anonymous user
		if (!env.AUTH_ENABLED) {
			return getAnonymousUserId();
		}

		const authUser = request.user as unknown as AuthUser | null;
		if (!authUser) {
			reply.status(401).send({ error: "Not authenticated" });
			throw new Error("AUTH_REQUIRED");
		}
		return authUser.id;
	}

	// Get settings for current user
	// Suppress request logs — polled every 30s for reminder status refresh
	app.get(
		"/settings",
		{
			logLevel: "warn",
			schema: {
				tags: ["settings"],
				summary: "Get current user settings",
				security: settingsEndpointSecurity,
				response: {
					200: { type: "object", additionalProperties: true },
					401: settingsErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);

			const settings = await getOrCreateUserSettings(userId);
			const reminderHour = envInt("REMINDER_HOUR", 6);
			const reminderMinutesBefore = envInt("REMINDER_MINUTES_BEFORE", 15);

			return reply.send({
				timezone: settings.timezone ?? "",
				availableTimezones: getAvailableTimezones(),
				serverTimezone: process.env.TZ || "UTC",
				// User notification settings (from DB)
				emailEnabled: settings.emailEnabled,
				notificationEmail: settings.notificationEmail ?? "",
				reminderDaysBefore: settings.reminderDaysBefore,
				repeatDailyReminders: settings.repeatDailyReminders,
				lowStockDays: settings.lowStockDays,
				normalStockDays: settings.normalStockDays,
				highStockDays: settings.highStockDays,
				shoutrrrEnabled: settings.shoutrrrEnabled,
				shoutrrrUrl: settings.shoutrrrUrl ?? "",
				emailStockReminders: settings.emailStockReminders,
				emailIntakeReminders: settings.emailIntakeReminders,
				emailPrescriptionReminders: settings.emailPrescriptionReminders ?? true,
				shoutrrrStockReminders: settings.shoutrrrStockReminders,
				shoutrrrIntakeReminders: settings.shoutrrrIntakeReminders,
				shoutrrrPrescriptionReminders: settings.shoutrrrPrescriptionReminders ?? true,
				skipRemindersForTakenDoses: settings.skipRemindersForTakenDoses,
				repeatRemindersEnabled: settings.repeatRemindersEnabled ?? false,
				reminderRepeatIntervalMinutes: settings.reminderRepeatIntervalMinutes ?? 30,
				maxNaggingReminders: settings.maxNaggingReminders ?? 5,
				language: settings.language,
				stockCalculationMode: settings.stockCalculationMode ?? "automatic",
				shareMedicationOverview: settings.shareMedicationOverview ?? false,
				upcomingTodayOnly: settings.upcomingTodayOnly ?? false,
				shareScheduleTodayOnly: settings.shareScheduleTodayOnly ?? false,
				swapDashboardMainSections: settings.swapDashboardMainSections ?? false,
				// SMTP settings (from .env - shared/server-configured)
				smtpHost: process.env.SMTP_HOST ?? "",
				smtpPort: parseInt(process.env.SMTP_PORT ?? "587", 10),
				smtpUser: process.env.SMTP_USER ?? "",
				smtpFrom: process.env.SMTP_FROM ?? "",
				smtpSecure: process.env.SMTP_SECURE === "true",
				hasSmtpPassword: !!(process.env.SMTP_TOKEN || process.env.SMTP_PASS),
				// Reminder state for this user
				lastAutoEmailSent: settings.lastAutoEmailSent,
				lastNotificationType: settings.lastNotificationType,
				lastNotificationChannel: settings.lastNotificationChannel,
				lastReminderMedName: settings.lastReminderMedName ?? null,
				lastReminderTakenBy: settings.lastReminderTakenBy ?? null,
				// Stock reminder tracking (separate from intake)
				lastStockReminderSent: settings.lastStockReminderSent ?? null,
				lastStockReminderChannel: settings.lastStockReminderChannel ?? null,
				lastStockReminderMedNames: settings.lastStockReminderMedNames ?? null,
				// Prescription reminder tracking (separate from stock/intake)
				lastPrescriptionReminderSent: settings.lastPrescriptionReminderSent ?? null,
				lastPrescriptionReminderChannel: settings.lastPrescriptionReminderChannel ?? null,
				lastPrescriptionReminderMedNames: settings.lastPrescriptionReminderMedNames ?? null,
				// Server settings (from .env, read-only)
				reminderHour,
				reminderMinutesBefore,
				expiryWarningDays: parseInt(process.env.EXPIRY_WARNING_DAYS ?? "30", 10),
			});
		}
	);

	// Update settings for current user
	app.put<{ Body: SettingsBody }>(
		"/settings",
		{
			schema: {
				tags: ["settings"],
				summary: "Update current user settings",
				security: settingsEndpointSecurity,
				body: {
					type: "object",
					required: ["emailEnabled", "notificationEmail", "reminderDaysBefore", "language"],
					properties: {
						timezone: { type: "string" },
						emailEnabled: { type: "boolean" },
						notificationEmail: { type: "string" },
						reminderDaysBefore: { type: "number" },
						repeatDailyReminders: { type: "boolean" },
						lowStockDays: { type: "number" },
						normalStockDays: { type: "number" },
						highStockDays: { type: "number" },
						shoutrrrEnabled: { type: "boolean" },
						shoutrrrUrl: { type: "string" },
						emailStockReminders: { type: "boolean" },
						emailIntakeReminders: { type: "boolean" },
						emailPrescriptionReminders: { type: "boolean" },
						shoutrrrStockReminders: { type: "boolean" },
						shoutrrrIntakeReminders: { type: "boolean" },
						shoutrrrPrescriptionReminders: { type: "boolean" },
						skipRemindersForTakenDoses: { type: "boolean" },
						repeatRemindersEnabled: { type: "boolean" },
						reminderRepeatIntervalMinutes: { type: "number" },
						maxNaggingReminders: { type: "number" },
						language: { type: "string", enum: ["en", "de"] },
						stockCalculationMode: { type: "string", enum: ["automatic", "manual"] },
						shareMedicationOverview: { type: "boolean" },
						upcomingTodayOnly: { type: "boolean" },
						shareScheduleTodayOnly: { type: "boolean" },
						swapDashboardMainSections: { type: "boolean" },
					},
					example: {
						emailEnabled: true,
						notificationEmail: "daniel@example.com",
						reminderDaysBefore: 7,
						repeatDailyReminders: true,
						lowStockDays: 14,
						normalStockDays: 30,
						highStockDays: 90,
						shoutrrrEnabled: false,
						shoutrrrUrl: "",
						emailStockReminders: true,
						emailIntakeReminders: true,
						emailPrescriptionReminders: true,
						shoutrrrStockReminders: false,
						shoutrrrIntakeReminders: false,
						shoutrrrPrescriptionReminders: false,
						skipRemindersForTakenDoses: true,
						repeatRemindersEnabled: true,
						reminderRepeatIntervalMinutes: 30,
						maxNaggingReminders: 5,
						language: "en",
						stockCalculationMode: "automatic",
						shareMedicationOverview: false,
						upcomingTodayOnly: false,
						shareScheduleTodayOnly: false,
						swapDashboardMainSections: false,
						timezone: "",
					},
				},
				response: {
					200: { type: "object", properties: { success: { type: "boolean" } } },
					401: settingsErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);

			const body = request.body;

			// Check if any stock reminders are configured
			const hasEmailStock = body.emailEnabled && body.emailStockReminders && body.notificationEmail;
			const hasShoutrrrStock = body.shoutrrrEnabled && body.shoutrrrStockReminders && body.shoutrrrUrl;
			const hasAnyStockReminder = hasEmailStock || hasShoutrrrStock;

			// Disable repeatDailyReminders if no stock reminders are configured
			const repeatDailyReminders = hasAnyStockReminder ? (body.repeatDailyReminders ?? false) : false;

			// Update or insert user settings
			const existingSettings = await db.select().from(userSettings).where(eq(userSettings.userId, userId));

			const settingsData = {
				timezone: normalizeSettingsTimezone(body.timezone),
				emailEnabled: body.emailEnabled,
				notificationEmail: body.notificationEmail || null,
				emailStockReminders: body.emailStockReminders ?? true,
				emailIntakeReminders: body.emailIntakeReminders ?? true,
				emailPrescriptionReminders: body.emailPrescriptionReminders ?? true,
				shoutrrrEnabled: body.shoutrrrEnabled ?? false,
				shoutrrrUrl: body.shoutrrrUrl || null,
				shoutrrrStockReminders: body.shoutrrrStockReminders ?? true,
				shoutrrrIntakeReminders: body.shoutrrrIntakeReminders ?? true,
				shoutrrrPrescriptionReminders: body.shoutrrrPrescriptionReminders ?? true,
				reminderDaysBefore: body.reminderDaysBefore,
				repeatDailyReminders,
				skipRemindersForTakenDoses: body.skipRemindersForTakenDoses ?? false,
				repeatRemindersEnabled: body.repeatRemindersEnabled ?? false,
				reminderRepeatIntervalMinutes: body.reminderRepeatIntervalMinutes ?? 30,
				maxNaggingReminders: body.maxNaggingReminders ?? 5,
				lowStockDays: body.lowStockDays ?? 30,
				normalStockDays: body.normalStockDays ?? 90,
				highStockDays: body.highStockDays ?? 180,
				language: body.language ?? "en",
				stockCalculationMode: body.stockCalculationMode ?? "automatic",
				shareMedicationOverview: body.shareMedicationOverview ?? false,
				upcomingTodayOnly: body.upcomingTodayOnly ?? false,
				shareScheduleTodayOnly: body.shareScheduleTodayOnly ?? false,
				swapDashboardMainSections: body.swapDashboardMainSections ?? false,
				updatedAt: new Date(),
			};

			if (existingSettings.length > 0) {
				await db.update(userSettings).set(settingsData).where(eq(userSettings.userId, userId));
			} else {
				await db.insert(userSettings).values({
					userId: userId,
					...settingsData,
				});
			}

			return reply.send({ success: true });
		}
	);

	// Update only the language setting (lightweight, called on dropdown change)
	app.put<{ Body: { language: string } }>(
		"/settings/language",
		{
			schema: {
				tags: ["settings"],
				summary: "Update UI language",
				security: settingsEndpointSecurity,
				body: {
					type: "object",
					required: ["language"],
					properties: {
						language: { type: "string", enum: ["en", "de"] },
					},
					example: {
						language: "de",
					},
				},
				response: {
					200: { type: "object", properties: { success: { type: "boolean" } } },
					400: settingsErrorSchema,
					401: settingsErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);
			const { language } = request.body;

			if (!language || !["en", "de"].includes(language)) {
				return reply.status(400).send({ error: "Invalid language" });
			}

			const existingSettings = await db.select().from(userSettings).where(eq(userSettings.userId, userId));

			if (existingSettings.length > 0) {
				await db.update(userSettings).set({ language, updatedAt: new Date() }).where(eq(userSettings.userId, userId));
			} else {
				await db.insert(userSettings).values({
					userId,
					...getDefaultSettings(),
					language,
				});
			}

			return reply.send({ success: true });
		}
	);

	// Test email - use SMTP settings from process.env
	app.post<{ Body: TestEmailBody }>(
		"/settings/test-email",
		{
			schema: {
				tags: ["settings"],
				summary: "Send test email",
				description: "Sends a test message using configured SMTP settings.",
				security: settingsEndpointSecurity,
				body: {
					type: "object",
					required: ["email"],
					properties: {
						email: { type: "string", format: "email" },
					},
					example: {
						email: "daniel@example.com",
					},
				},
				response: {
					200: {
						type: "object",
						properties: {
							success: { type: "boolean" },
							message: { type: "string" },
						},
					},
					400: settingsErrorSchema,
					401: settingsErrorSchema,
					500: settingsErrorSchema,
					502: settingsErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const { email } = request.body;

			const smtp = getSmtpConfig();

			request.log.info(
				{
					to: email,
					hasSmtpHost: Boolean(smtp.host),
					hasSmtpUser: Boolean(smtp.user),
					hasSmtpPass: Boolean(smtp.pass),
					hasSmtpFrom: Boolean(smtp.from),
					smtpPort: smtp.port,
					smtpSecure: smtp.secure,
				},
				"[Settings] Test email request received"
			);

			if (!smtp.host || !smtp.user) {
				request.log.warn(
					{ to: email, hasSmtpHost: Boolean(smtp.host), hasSmtpUser: Boolean(smtp.user) },
					"[Settings] Test email skipped: SMTP not configured"
				);
				return reply.status(400).send({ error: "SMTP not configured" });
			}

			try {
				request.log.info({ to: email }, "[Settings] Sending test email");

				const mailResult = await sendEmailNotification({
					from: smtp.from,
					to: email,
					subject: "MedAssist-ng - Test Email",
					text: "This is a test email from MedAssist-ng. If you received this, your email configuration is working correctly!",
					html: `
          <div style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
            <h2 style="color: #2563eb;">MedAssist-ng - Test Email</h2>
            <p>This is a test email from MedAssist-ng.</p>
            <p style="color: #10b981; font-weight: 600;">✓ If you received this, your email configuration is working correctly!</p>
            <hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
            <p style="color: #6b7280; font-size: 14px;">Sent from MedAssist-ng Medication Planner</p>
          </div>
        `,
				});

				if (!mailResult.success) {
					throw new Error(mailResult.error ?? "Failed to send test email");
				}

				request.log.info({ to: email, messageId: mailResult.messageId }, "[Settings] Test email sent");

				return reply.send({ success: true, message: "Test email sent successfully" });
			} catch (error) {
				request.log.error({ to: email, error }, "[Settings] Test email failed");
				const failure = classifyTestEmailFailure(error);
				return reply.status(failure.status).send({ error: failure.message, code: failure.code });
			}
		}
	);

	// Test Shoutrrr/ntfy notification
	app.post<{ Body: TestShoutrrrBody }>(
		"/settings/test-shoutrrr",
		{
			schema: {
				tags: ["settings"],
				summary: "Send test push notification",
				description: "Sends a test notification via a Shoutrrr-compatible URL.",
				security: settingsEndpointSecurity,
				body: {
					type: "object",
					required: ["url"],
					properties: {
						url: { type: "string" },
					},
					example: {
						url: "ntfy://user:token@push.example.com/medassist",
					},
				},
				response: {
					200: {
						type: "object",
						properties: {
							success: { type: "boolean" },
							message: { type: "string" },
						},
					},
					400: settingsErrorSchema,
					401: settingsErrorSchema,
					500: settingsErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const { url } = request.body;

			if (!url) {
				return reply.status(400).send({ error: "Notification URL is required" });
			}

			try {
				const userId = await getUserId(request, reply);
				const settings = await getOrCreateUserSettings(userId);
				const language = getLanguage(settings.language);
				const { title, message } = buildInteractiveTestPushNotification(language);
				const actionContext = await createTestNotificationActionContext({
					userId,
					title,
					message,
					publicAppUrl: env.PUBLIC_APP_URL,
					language,
				});
				const provider = getNotificationProvider(url);
				const result = await sendShoutrrrNotification(url, title, message, {
					actions: actionContext?.actions,
					respondUrl: actionContext?.respondUrl,
					viewUrl: actionContext?.viewUrl,
					clickUrl: actionContext?.respondUrl ?? actionContext?.viewUrl,
					sequenceId: actionContext?.sequenceId,
					tags: ["pill"],
					priority: 3,
				});

				if (result.success) {
					if (actionContext?.groupId && result.providerMessageId) {
						await storeNotificationActionGroupNtfyMessageId(actionContext.groupId, result.providerMessageId);
					}

					request.log.info({ provider }, "[Settings] Test push notification sent");
					return reply.send({ success: true, message: "Test notification sent successfully" });
				} else {
					request.log.warn({ provider, error: result.error ?? "unknown" }, "[Settings] Test push notification failed");
					return reply.status(500).send({ error: result.error });
				}
			} catch (error) {
				request.log.error(
					{ provider: getNotificationProvider(url), error },
					"[Settings] Unexpected error while sending test push notification"
				);
				const errorMessage = error instanceof Error ? error.message : "Unknown error";
				return reply.status(500).send({ error: `Failed to send notification: ${errorMessage}` });
			}
		}
	);
}

// Send notification via Shoutrrr-compatible URL (supports ntfy, Discord, Telegram, etc.)
export async function sendShoutrrrNotification(
	urlStr: string,
	title: string,
	message: string,
	options: PushNotificationOptions = {}
): Promise<{ success: boolean; error?: string; providerMessageId?: string }> {
	try {
		if (urlStr.startsWith("pushover://")) {
			const pushoverAuthority = urlStr.slice("pushover://".length).split("/")[0] ?? "";
			const atIndex = pushoverAuthority.lastIndexOf("@");
			const credentialPart = atIndex >= 0 ? pushoverAuthority.slice(0, atIndex) : "";
			const userKey = atIndex >= 0 ? pushoverAuthority.slice(atIndex + 1) : "";

			const tokenSeparatorIndex = credentialPart.indexOf(":");
			const apiToken = tokenSeparatorIndex >= 0 ? credentialPart.slice(tokenSeparatorIndex + 1) : "";

			const parsedPushover = new URL(urlStr);

			if (!apiToken || !userKey) {
				return { success: false, error: "Invalid Pushover URL format" };
			}

			const pushoverBody = new URLSearchParams({
				token: apiToken,
				user: userKey,
				title,
				message,
			});

			const devices = parsedPushover.searchParams.get("devices");
			if (devices) {
				pushoverBody.set("device", devices);
			}

			const priority = parsedPushover.searchParams.get("priority");
			if (priority && /^-?\d+$/.test(priority)) {
				pushoverBody.set("priority", priority);
			}

			const response = await fetch("https://api.pushover.net/1/messages.json", {
				method: "POST",
				headers: { "Content-Type": "application/x-www-form-urlencoded" },
				body: pushoverBody.toString(),
				redirect: "error",
			});

			if (response.ok) return { success: true };
			const errorText = await response.text();
			return { success: false, error: `HTTP ${response.status}: ${errorText}` };
		}

		if (urlStr.startsWith("telegram://")) {
			const parsedTelegram = new URL(urlStr);
			const token = parsedTelegram.username;
			if (!token || parsedTelegram.hostname !== "telegram") {
				return { success: false, error: "Invalid Telegram URL format" };
			}

			const chatsRaw = parsedTelegram.searchParams.get("chats") ?? parsedTelegram.searchParams.get("channels") ?? "";
			const chats = chatsRaw
				.split(",")
				.map((chat) => chat.trim())
				.filter(Boolean);

			if (chats.length === 0) {
				return { success: false, error: "Telegram URL requires chats parameter" };
			}

			const parseModeRaw = parsedTelegram.searchParams.get("parseMode")?.toLowerCase();
			let parseMode: "HTML" | "Markdown" | "MarkdownV2" | undefined;
			if (parseModeRaw === "html") {
				parseMode = "HTML";
			} else if (parseModeRaw === "markdown") {
				parseMode = "Markdown";
			} else if (parseModeRaw === "markdownv2") {
				parseMode = "MarkdownV2";
			}

			const notificationRaw = parsedTelegram.searchParams.get("notification")?.toLowerCase();
			const disableNotification = notificationRaw === "no" || notificationRaw === "false";

			const previewRaw = parsedTelegram.searchParams.get("preview")?.toLowerCase();
			const disablePreview = previewRaw === "no" || previewRaw === "false";

			if (!/^\d+:[A-Za-z0-9_-]+$/.test(token)) {
				return { success: false, error: "Invalid Telegram token format" };
			}

			const telegramSendMessageUrl = new URL("/bot/sendMessage", "https://api.telegram.org");
			telegramSendMessageUrl.pathname = `/bot${token}/sendMessage`;

			for (const chatId of chats) {
				const payload: Record<string, string | boolean> = {
					chat_id: chatId,
					text: `${title}\n\n${message}`,
					disable_notification: disableNotification,
					disable_web_page_preview: disablePreview,
				};
				if (parseMode) {
					payload.parse_mode = parseMode;
				}

				// codeql[js/request-forgery]: host is fixed to api.telegram.org and token is pattern-validated.
				const response = await fetch(telegramSendMessageUrl.toString(), {
					method: "POST",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify(payload),
					redirect: "error",
				});

				if (!response.ok) {
					const errorText = await response.text();
					return { success: false, error: `HTTP ${response.status}: ${errorText}` };
				}
			}

			return { success: true };
		}

		if (urlStr.startsWith("gotify://")) {
			const parsedGotify = new URL(urlStr);
			const hostValidationError = validateNotificationHostname(parsedGotify.hostname);
			if (hostValidationError) {
				return { success: false, error: hostValidationError };
			}

			const pathParts = parsedGotify.pathname
				.split("/")
				.map((part) => part.trim())
				.filter(Boolean);

			if (pathParts.length === 0) {
				return { success: false, error: "Invalid Gotify URL format" };
			}

			const token = pathParts[pathParts.length - 1];
			const basePath = pathParts.slice(0, -1).join("/");

			const disableTlsRaw = parsedGotify.searchParams.get("disabletls")?.toLowerCase();
			const protocol = disableTlsRaw === "yes" || disableTlsRaw === "true" || disableTlsRaw === "1" ? "http" : "https";

			const gotifyWebhookUrl = `${protocol}://${parsedGotify.host}${basePath ? `/${basePath}` : ""}/message?token=${encodeURIComponent(token)}`;

			const gotifyPriority = parsedGotify.searchParams.get("priority");
			const gotifyMessage = gotifyPriority ? `${message}\n\n(priority=${gotifyPriority})` : message;

			// Reuse validated https webhook path to keep a single outbound request sink.
			return sendShoutrrrNotification(gotifyWebhookUrl, title, gotifyMessage);
		}

		// Validate and sanitize URL to prevent SSRF - this reconstructs the URL
		// from validated components, breaking taint tracking
		const validation = sanitizeNotificationUrl(urlStr);
		if ("error" in validation) {
			return { success: false, error: validation.error };
		}

		// Use ONLY the reconstructed URL from validation - never the original urlStr
		const { url: sanitizedUrl, isNtfy, auth } = validation;

		let targetUrl: string;
		const method = "POST";
		let headers: Record<string, string> = {};
		let body: string | undefined;
		const renderedPayload = renderNotificationActionPayload(urlStr, message, options);

		// Remove emojis from title for header compatibility
		const cleanTitle = title
			.replace(
				/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE00}-\u{FE0F}]|[\u{2000}-\u{206F}]|⚠|️/gu,
				""
			)
			.trim();

		// Determine notification type based on URL hostname
		// Use JSON format only for known webhook services that require it
		// Use proper URL parsing to prevent bypass attacks (e.g., evil.com?hooks.slack.com)
		let isJsonWebhook = false;
		let isDiscordWebhook = false;
		try {
			const parsedUrl = new URL(sanitizedUrl);
			const hostname = parsedUrl.hostname.toLowerCase();
			const pathname = parsedUrl.pathname.toLowerCase();
			isDiscordWebhook =
				(hostname === "discord.com" || hostname === "discordapp.com") && pathname.startsWith("/api/webhooks");

			isJsonWebhook =
				// Discord webhooks
				isDiscordWebhook ||
				// Slack webhooks
				hostname === "hooks.slack.com" ||
				hostname.endsWith(".hooks.slack.com") ||
				// Telegram API
				hostname === "api.telegram.org" ||
				// Gotify (can be self-hosted, so check if "gotify" is in hostname)
				hostname.includes("gotify");
		} catch {
			// If URL parsing fails, default to ntfy-style
			isJsonWebhook = false;
		}

		// Default to ntfy-style (plain text with Title header) for all other HTTP URLs
		// This works for ntfy, Apprise, and most simple push services
		if (!isJsonWebhook) {
			targetUrl = sanitizedUrl;
			// Use RFC 2047 Base64 encoding for Title header to safely pass non-ASCII
			// characters (umlauts, accents, etc.) through HTTP headers
			const encodedTitle = `=?UTF-8?B?${Buffer.from(cleanTitle, "utf-8").toString("base64")}?=`;
			headers = { Title: encodedTitle, Tags: "pill" };
			body = renderedPayload.message;

			// Add auth if present (extracted during sanitization)
			if (auth) {
				headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`;
			}

			if (isNtfy) {
				headers = { ...headers, ...renderedPayload.headers };
			}
		} else if (sanitizedUrl.startsWith("http://") || sanitizedUrl.startsWith("https://")) {
			targetUrl = sanitizedUrl;
			headers = { "Content-Type": "application/json" };
			if (isDiscordWebhook) {
				body = JSON.stringify({ content: `${title}\n\n${renderedPayload.message}` });
			} else {
				body = JSON.stringify({
					title,
					message: renderedPayload.message,
					text: `${title}\n\n${renderedPayload.message}`,
				});
			}
		} else {
			return {
				success: false,
				error: "Unsupported URL format. Use ntfy://, discord://, pushover://, gotify://, telegram://, or https:// URL",
			};
		}

		// SSRF protection: targetUrl is reconstructed from sanitizeNotificationUrl() which validates:
		// - Only http/https protocols allowed
		// - Blocks localhost (localhost, 127.0.0.1, ::1)
		// - Blocks private IPs (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 169.254.x.x)
		// - Blocks internal hostnames (.local, .internal, .lan, metadata.google.internal)
		// - redirect: "error" prevents redirect-based bypass attacks
		// This is an intentional feature: users configure their own external notification services
		// lgtm [js/request-forgery]
		const response = await fetch(targetUrl, {
			method,
			headers,
			body,
			redirect: "error", // Don't follow redirects that could bypass validation
		});

		if (response.ok) {
			let providerMessageId: string | undefined;
			if (isNtfy) {
				try {
					const payload = (await response.json()) as { id?: unknown };
					providerMessageId = typeof payload.id === "string" && payload.id.length > 0 ? payload.id : undefined;
				} catch {
					providerMessageId = undefined;
				}
			}

			return { success: true, providerMessageId };
		} else {
			const errorText = await response.text();
			return { success: false, error: `HTTP ${response.status}: ${errorText}` };
		}
	} catch (error) {
		const errorMessage = error instanceof Error ? error.message : "Unknown error";
		return { success: false, error: errorMessage };
	}
}
