import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { and, eq, gte, lte } from "drizzle-orm";
import { db } from "../db/client.js";
import { getDataDir } from "../db/path-utils.js";
import { doseTracking, medications, users } from "../db/schema.js";
import {
	getDateLocale,
	getFooterHtml,
	getFooterPlain,
	getTranslations,
	type Language,
	t,
} from "../i18n/translations.js";

import { env } from "../plugins/env.js";
import { getAllUserSettings, type UserSettings } from "../routes/settings.js";
import type { ServiceLogger } from "../utils/logger.js";
// Import shared utilities
import {
	cleanOldIntakeReminders,
	createDefaultIntakeReminderState,
	getEffectiveTimezone,
	getTodaysIntakes,
	getUpcomingIntakes,
	type IntakeReminderState,
	normalizeIntakeUsageForStock,
	parseIntakeReminderState,
	parseIntakesJson,
	parseTakenByJson,
	type UpcomingIntake,
} from "../utils/scheduler-utils.js";
import { computeMedicationCurrentStock } from "./current-stock.js";
import {
	createNotificationActionContext,
	storeNotificationActionGroupNtfyMessageId,
} from "./notification-actions-service.js";
import { getSmtpConfig, sendEmailNotification, sendPushNotification } from "./notifications/delivery.js";
import { updateReminderSentTime, updateUserReminderSentTime } from "./notifications/state.js";

const REMINDER_MINUTES_BEFORE = parseInt(process.env.REMINDER_MINUTES_BEFORE ?? "15", 10);
const CHECK_INTERVAL_MS = 60 * 1000; // Check every 1 minute

const intakeReminderStateFile = resolve(getDataDir(), "intake-reminder-state.json");

function loadIntakeReminderState(logger: ServiceLogger): IntakeReminderState {
	try {
		if (existsSync(intakeReminderStateFile)) {
			return parseIntakeReminderState(readFileSync(intakeReminderStateFile, "utf-8"));
		}
	} catch (error: unknown) {
		const errorMessage = error instanceof Error ? error.message : String(error);
		logger.error(`[IntakeReminder] Failed to load reminder state file=${intakeReminderStateFile}: ${errorMessage}`);
	}
	return createDefaultIntakeReminderState();
}

function saveIntakeReminderState(state: IntakeReminderState): void {
	writeFileSync(intakeReminderStateFile, JSON.stringify(state, null, 2));
}

function buildDoseIdForIntake(intake: UpcomingIntake & { medicationId: number; blisterIndex: number }): string {
	const intakeDate = intake.intakeTime;
	const dateOnlyMs = new Date(intakeDate.getFullYear(), intakeDate.getMonth(), intakeDate.getDate()).getTime();
	if (intake.takenBy) {
		return `${intake.medicationId}-${intake.blisterIndex}-${dateOnlyMs}-${intake.takenBy}`;
	}
	return `${intake.medicationId}-${intake.blisterIndex}-${dateOnlyMs}`;
}

async function getUsernameForLog(userId: number): Promise<string> {
	const user = await db.select({ username: users.username }).from(users).where(eq(users.id, userId));
	const username = user[0]?.username?.trim();
	return username && username.length > 0 ? username : `unknown-user-${userId}`;
}

function formatIntakeLog(intake: {
	medName: string;
	medicationId: number;
	blisterIndex: number;
	intakeTime: Date;
	intakeTimeStr: string;
	usage: number;
	doseUnit?: string;
	takenBy?: string | null;
}): string {
	const takenBy = intake.takenBy ? intake.takenBy : "none";
	const doseUnit = intake.doseUnit ?? "mg";
	return `${intake.medName} (medId=${intake.medicationId}, intakeIndex=${intake.blisterIndex}, time=${intake.intakeTime.toISOString()}, localTime=${intake.intakeTimeStr}, usage=${intake.usage} ${doseUnit}, takenBy=${takenBy})`;
}

function getMedicationDisplayName(med: { id: number; name: string | null; genericName: string | null }): string {
	const commercialName = med.name?.trim() ?? "";
	if (commercialName) return commercialName;

	const genericName = med.genericName?.trim() ?? "";
	if (genericName) return genericName;

	return `Medication #${med.id}`;
}

function getPushProviderLabel(url: string): string {
	const normalizedUrl = url.trim().toLowerCase();
	if (normalizedUrl.startsWith("ntfy://")) return "ntfy";
	if (normalizedUrl.startsWith("discord://")) return "discord";
	if (normalizedUrl.startsWith("pushover://")) return "pushover";
	if (normalizedUrl.startsWith("gotify://")) return "gotify";
	if (normalizedUrl.startsWith("telegram://")) return "telegram";

	try {
		const parsedUrl = new URL(url);
		return parsedUrl.hostname || parsedUrl.protocol.replace(":", "") || "unknown";
	} catch {
		return "unknown";
	}
}

function formatActionContextLog(options: {
	actionMode: "full" | "view-only";
	doseCount: number;
	actionContext: Awaited<ReturnType<typeof createNotificationActionContext>> | null;
}): string {
	const { actionMode, doseCount, actionContext } = options;
	return `actionMode=${actionMode}, doses=${doseCount}, actions=${actionContext?.actions.length ?? 0}, hasRespondUrl=${actionContext?.respondUrl ? "yes" : "no"}, hasViewUrl=${actionContext?.viewUrl ? "yes" : "no"}, sequenceId=${actionContext?.sequenceId ?? "none"}, groupId=${actionContext?.groupId ?? "n/a"}`;
}

async function autoMarkDueIntakesAsTaken(
	settings: UserSettings & { userId: number },
	rows: (typeof medications.$inferSelect)[],
	locale: string,
	tz: string,
	logger: ServiceLogger
): Promise<number> {
	if (settings.stockCalculationMode !== "automatic") {
		logger.debug(
			`[IntakeReminder] Auto-mark disabled for userId=${settings.userId} because stockCalculationMode=${settings.stockCalculationMode}`
		);
		return 0;
	}

	const now = new Date();
	const nowInTimezone = new Date(now.toLocaleString("en-US", { timeZone: tz }));
	const todayStart = new Date(now.toLocaleString("en-US", { timeZone: tz }));
	todayStart.setHours(0, 0, 0, 0);
	const todayEnd = new Date(now.toLocaleString("en-US", { timeZone: tz }));
	todayEnd.setHours(23, 59, 59, 999);

	const existingToday = await db
		.select({ doseId: doseTracking.doseId })
		.from(doseTracking)
		.where(
			and(
				eq(doseTracking.userId, settings.userId),
				gte(doseTracking.takenAt, todayStart),
				lte(doseTracking.takenAt, todayEnd)
			)
		);
	const existingDoseIds = new Set(existingToday.map((d) => d.doseId));
	const trackedDoses = await db
		.select()
		.from(doseTracking)
		.where(and(eq(doseTracking.userId, settings.userId), eq(doseTracking.dismissed, false)));

	let inserted = 0;

	for (const med of rows) {
		if (med.isObsolete) {
			continue;
		}

		const intakes = parseIntakesJson(
			med.intakesJson,
			{ usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson },
			med.intakeRemindersEnabled ?? false
		);
		if (intakes.length === 0) {
			continue;
		}

		const medicationTakenBy = parseTakenByJson(med.takenByJson);
		const medDisplayName = getMedicationDisplayName({ id: med.id, name: med.name, genericName: med.genericName });
		let remainingStock = computeMedicationCurrentStock({
			medication: med,
			doses: trackedDoses,
			stockCalculationMode: settings.stockCalculationMode,
			nowMs: now.getTime(),
		});
		if (remainingStock <= 0) {
			continue;
		}
		const todaysIntakes = getTodaysIntakes(
			medDisplayName,
			intakes,
			medicationTakenBy,
			med.pillWeightMg,
			locale,
			tz,
			med.id,
			med.doseUnit ?? "mg"
		);

		for (const intake of todaysIntakes) {
			const intakeTimeInTimezone = new Date(intake.intakeTime.toLocaleString("en-US", { timeZone: tz }));
			if (intakeTimeInTimezone.getTime() > nowInTimezone.getTime()) {
				continue;
			}
			if (intake.medicationId === undefined || intake.blisterIndex === undefined) {
				continue;
			}

			const doseId = buildDoseIdForIntake({
				...intake,
				medicationId: intake.medicationId,
				blisterIndex: intake.blisterIndex,
			});

			if (existingDoseIds.has(doseId)) {
				continue;
			}

			const intakeDefinition = intakes[intake.blisterIndex];
			const usage = intakeDefinition
				? normalizeIntakeUsageForStock(intakeDefinition, med.medicationForm, med.packageType)
				: 0;
			if (remainingStock <= 0) {
				break;
			}

			await db.insert(doseTracking).values({
				userId: settings.userId,
				doseId,
				takenAt: intake.intakeTime,
				markedBy: null,
				takenSource: "automatic",
				dismissed: false,
			});

			logger.info(
				`[IntakeReminder] Auto-marked intake for userId=${settings.userId}: ${formatIntakeLog({
					medName: intake.medName,
					medicationId: intake.medicationId,
					blisterIndex: intake.blisterIndex,
					intakeTime: intake.intakeTime,
					intakeTimeStr: intake.intakeTimeStr,
					usage: intake.usage,
					doseUnit: intake.doseUnit,
					takenBy: intake.takenBy,
				})}`
			);

			existingDoseIds.add(doseId);
			trackedDoses.push({
				id: 0,
				userId: settings.userId,
				doseId,
				takenAt: intake.intakeTime,
				markedBy: null,
				takenSource: "automatic",
				dismissed: false,
			});
			remainingStock = Math.max(0, remainingStock - usage);
			inserted++;
		}
	}

	if (inserted === 0) {
		logger.debug(`[IntakeReminder] Auto-mark completed for userId=${settings.userId}: no due intakes`);
	} else {
		logger.info(`[IntakeReminder] Auto-mark completed for userId=${settings.userId}: inserted=${inserted}`);
	}

	return inserted;
}

async function sendIntakeReminderEmail(
	email: string,
	intakes: UpcomingIntake[],
	language: Language,
	isRepeat: boolean = false,
	repeatIntervalMinutes?: number,
	currentCount?: number,
	maxCount?: number
): Promise<{ success: boolean; error?: string; messageId?: string; smtpResponse?: string }> {
	const smtp = getSmtpConfig();

	if (!smtp.host || !smtp.user) {
		return { success: false, error: "SMTP not configured" };
	}

	const tr = getTranslations(language);

	// Helper to format dosage with weight
	const formatDosage = (intake: UpcomingIntake): string => {
		const pillText = `<strong>${intake.usage}</strong> ${intake.usage === 1 ? tr.common.pill : tr.intakeReminder.pills}`;
		if (intake.pillWeightMg) {
			const totalMg = intake.usage * intake.pillWeightMg;
			const weightStr = totalMg >= 1000 ? `${(totalMg / 1000).toFixed(1)} g` : `${totalMg} mg`;
			return `${pillText} (${weightStr})`;
		}
		return pillText;
	};

	// Helper to format medication name with takenBy (single person or null)
	const formatMedName = (intake: UpcomingIntake): string => {
		if (intake.takenBy) {
			return `${intake.medName} <span style="color: #6b7280; font-size: 12px;">${t(tr.intakeReminder.takenBy, { name: intake.takenBy })}</span>`;
		}
		return intake.medName;
	};

	const tableRows = intakes
		.map(
			(intake) => `
      <tr>
        <td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb;">${formatMedName(intake)}</td>
        <td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; text-align: center;">${formatDosage(intake)}</td>
        <td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; text-align: center;">${intake.intakeTimeStr}</td>
      </tr>
    `
		)
		.join("");

	const alertText =
		intakes.length === 1
			? tr.intakeReminder.alertSingle
			: t(tr.intakeReminder.alertMultiple, { count: intakes.length });

	// Different description for repeat reminders
	let description: string;
	if (isRepeat && repeatIntervalMinutes && currentCount !== undefined && maxCount !== undefined) {
		const remainingReminders = maxCount - currentCount;
		if (remainingReminders <= 0) {
			description = language === "de" ? "⚠️ Dies ist die letzte Erinnerung." : "⚠️ This is the last reminder.";
		} else if (remainingReminders === 1) {
			description =
				language === "de"
					? `ℹ️ Eine weitere Erinnerung wird in ${repeatIntervalMinutes} Minuten gesendet.`
					: `ℹ️ One more reminder will be sent in ${repeatIntervalMinutes} minutes.`;
		} else {
			description =
				language === "de"
					? `ℹ️ ${remainingReminders} weitere Erinnerungen werden alle ${repeatIntervalMinutes} Minuten gesendet.`
					: `ℹ️ ${remainingReminders} more reminders will be sent every ${repeatIntervalMinutes} minutes.`;
		}
	} else {
		description = t(tr.intakeReminder.description, { minutes: REMINDER_MINUTES_BEFORE });
	}

	const html = `
    <div style="font-family: system-ui, -apple-system, sans-serif; max-width: 100%; margin: 0 auto; padding: 12px; background: #f9fafb;">
      <div style="background: white; border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
        <h2 style="color: #1f2937; margin: 0 0 8px; font-size: 18px;">${tr.intakeReminder.title}</h2>
        <p style="color: #6b7280; margin: 0 0 16px; font-size: 13px;">${description}</p>
        
        <div style="padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; background: #eff6ff; border: 1px solid #bfdbfe;">
          <p style="margin: 0; color: #1e40af; font-weight: 500; font-size: 13px;">
            ${alertText}
          </p>
        </div>

        <table style="width: 100%; border-collapse: collapse; background: white;">
          <thead>
            <tr style="background: #f3f4f6;">
              <th style="padding: 10px 12px; text-align: left; font-size: 11px; text-transform: uppercase; color: #6b7280;">${tr.intakeReminder.tableHeaders.medication}</th>
              <th style="padding: 10px 12px; text-align: center; font-size: 11px; text-transform: uppercase; color: #6b7280;">${tr.intakeReminder.tableHeaders.dosage}</th>
              <th style="padding: 10px 12px; text-align: center; font-size: 11px; text-transform: uppercase; color: #6b7280;">${tr.intakeReminder.tableHeaders.time}</th>
            </tr>
          </thead>
          <tbody>
            ${tableRows}
          </tbody>
        </table>

        <hr style="border: none; border-top: 1px solid #e5e7eb; margin: 16px 0;" />
        <p style="color: #9ca3af; font-size: 11px; margin: 0;">
          ${getFooterHtml(language)}
        </p>
      </div>
    </div>
  `;

	// Helper for plain text dosage
	const formatDosagePlain = (intake: UpcomingIntake): string => {
		const pillText = `${intake.usage} ${intake.usage === 1 ? tr.common.pill : tr.intakeReminder.pills}`;
		if (intake.pillWeightMg) {
			const totalMg = intake.usage * intake.pillWeightMg;
			const weightStr = totalMg >= 1000 ? `${(totalMg / 1000).toFixed(1)} g` : `${totalMg} mg`;
			return `${pillText} (${weightStr})`;
		}
		return pillText;
	};

	const plainText = `${tr.intakeReminder.title}

${description}

${intakes
	.map((i) => {
		const takenByStr = i.takenBy ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy })}` : "";
		return `${i.medName}${takenByStr}: ${formatDosagePlain(i)} - ${i.intakeTimeStr}`;
	})
	.join("\n")}

---
${getFooterPlain(language)}`;

	const subject = isRepeat
		? `[Reminder] ${t(tr.intakeReminder.subject, { medications: intakes.map((i) => i.medName).join(", ") })}`
		: t(tr.intakeReminder.subject, { medications: intakes.map((i) => i.medName).join(", ") });

	const mailResult = await sendEmailNotification({
		to: email,
		subject: `💊 ${subject}`,
		text: plainText,
		html,
		from: smtp.from,
	});

	if (!mailResult.success) {
		return { success: false, error: mailResult.error ?? "Unknown error" };
	}

	return {
		success: true,
		messageId: mailResult.messageId,
		smtpResponse: mailResult.smtpResponse,
	};
}

async function checkAndSendIntakeReminders(logger: ServiceLogger): Promise<void> {
	logger.debug(`[IntakeReminder] Checking for intake reminders...`);

	// Get all user settings to iterate over each user
	const allUserSettings = await getAllUserSettings();
	logger.debug(`[IntakeReminder] Scheduler cycle loaded user settings count=${allUserSettings.length}`);

	if (allUserSettings.length === 0) {
		logger.debug(`[IntakeReminder] No users with settings found`);
		return; // No users with settings
	}

	for (const userSettings of allUserSettings) {
		await checkAndSendIntakeRemindersForUser(userSettings, logger);
	}

	logger.debug(`[IntakeReminder] Scheduler cycle finished`);
}

export async function checkAndSendIntakeRemindersForUser(
	settings: UserSettings & { userId: number },
	logger: ServiceLogger
): Promise<void> {
	const username = await getUsernameForLog(settings.userId);
	logger.info(
		`[IntakeReminder] Evaluating intake reminders for user=${username} (userId=${settings.userId}, emailEnabled=${settings.emailEnabled}, pushEnabled=${settings.shoutrrrEnabled}, skipTaken=${settings.skipRemindersForTakenDoses}, repeat=${settings.repeatRemindersEnabled}, mode=${settings.stockCalculationMode})`
	);

	const language = settings.language;
	const tr = getTranslations(language);

	const rows = await db
		.select()
		.from(medications)
		.where(and(eq(medications.userId, settings.userId), eq(medications.isObsolete, false)));

	const activeRows = rows.filter((med) => med.isObsolete !== true).sort((left, right) => left.id - right.id);

	const locale = getDateLocale(language);
	const tz = getEffectiveTimezone(settings.timezone ?? null);

	const autoMarkedCount = await autoMarkDueIntakesAsTaken(settings, activeRows, locale, tz, logger);
	if (autoMarkedCount > 0) {
		logger.info(
			`[IntakeReminder] Auto-mark summary for user=${username} (userId=${settings.userId}): autoMarkedCount=${autoMarkedCount}`
		);
	}

	if (settings.stockCalculationMode === "automatic" && settings.skipRemindersForTakenDoses) {
		logger.info(
			`[IntakeReminder] Reminder sending skipped for user=${username} (userId=${settings.userId}) because stockCalculationMode=automatic and skipRemindersForTakenDoses=true`
		);
		return;
	}

	// Check if any intake reminder notifications are enabled (granular check)
	const emailEnabled = settings.emailEnabled && settings.notificationEmail && settings.emailIntakeReminders;
	const shoutrrrEnabled = settings.shoutrrrEnabled && settings.shoutrrrUrl && settings.shoutrrrIntakeReminders;

	if (!emailEnabled && !shoutrrrEnabled) {
		logger.debug(
			`[IntakeReminder] Notification sending disabled for user=${username} (userId=${settings.userId}): both email and push intake reminders are off`
		);
		return; // No intake reminder notifications enabled for this user
	}

	// Build medication entries that have at least one reminder-enabled intake.
	// Intake-level reminders are the single source of truth.
	const reminderEntries = activeRows
		.map((med) => {
			const intakes = parseIntakesJson(
				med.intakesJson,
				{ usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson },
				false
			);
			const intakesWithReminders = intakes.filter((intake) => intake.intakeRemindersEnabled === true);
			return { med, intakes, intakesWithReminders };
		})
		.filter((entry) => entry.intakesWithReminders.length > 0);

	if (reminderEntries.length === 0) {
		logger.debug(
			`[IntakeReminder] No reminder-enabled intake definitions for user=${username} (userId=${settings.userId})`
		);
		return; // No medications have reminders enabled for this user
	}

	const now = new Date();
	const state = loadIntakeReminderState(logger);
	const trackedDoses = await db
		.select()
		.from(doseTracking)
		.where(and(eq(doseTracking.userId, settings.userId), eq(doseTracking.dismissed, false)));

	const reminderEntriesWithStock = reminderEntries.map((entry) => ({
		...entry,
		currentStock: computeMedicationCurrentStock({
			medication: entry.med,
			doses: trackedDoses,
			stockCalculationMode: settings.stockCalculationMode,
			nowMs: now.getTime(),
		}),
	}));
	const suppressedEmptyStockEntries = reminderEntriesWithStock.filter((entry) => entry.currentStock <= 0);
	if (suppressedEmptyStockEntries.length > 0) {
		logger.info(
			`[IntakeReminder] Skipping reminder-enabled medications with empty stock for user=${username} (userId=${settings.userId}): count=${suppressedEmptyStockEntries.length}, meds=${suppressedEmptyStockEntries
				.map((entry) =>
					getMedicationDisplayName({ id: entry.med.id, name: entry.med.name, genericName: entry.med.genericName })
				)
				.join(", ")}`
		);
	}
	const reminderEntriesEligible = reminderEntriesWithStock.filter((entry) => entry.currentStock > 0);
	if (reminderEntriesEligible.length === 0) {
		logger.info(
			`[IntakeReminder] No reminder-eligible medications with stock remaining for user=${username} (userId=${settings.userId})`
		);
		return;
	}
	const allUpcoming: (UpcomingIntake & { medicationId: number; blisterIndex: number })[] = [];
	let scheduledIntakesTodayCount = 0;
	// Get start and end of today in user's timezone (for filtering today's doses only)
	const todayStart = new Date(now.toLocaleString("en-US", { timeZone: tz }));
	todayStart.setHours(0, 0, 0, 0);

	const todayEnd = new Date(now.toLocaleString("en-US", { timeZone: tz }));
	todayEnd.setHours(23, 59, 59, 999);

	// Find intakes: upcoming ones in reminder window + past ones for repeat reminders
	for (const { med, intakes, intakesWithReminders } of reminderEntriesEligible) {
		// Medication-level takenBy (for fallback/display purposes)
		const medicationTakenBy = parseTakenByJson(med.takenByJson);
		const medDisplayName = getMedicationDisplayName({ id: med.id, name: med.name, genericName: med.genericName });

		// Process each intake separately to track blisterIndex
		intakesWithReminders.forEach((intake, _blisterIndex) => {
			const actualIndex = intakes.indexOf(intake); // Get the actual index in original array

			const todaysIntakesForThisDefinition = getTodaysIntakes(
				medDisplayName,
				[intake],
				medicationTakenBy,
				med.pillWeightMg,
				locale,
				tz,
				med.id,
				med.doseUnit ?? "mg"
			);
			scheduledIntakesTodayCount += todaysIntakesForThisDefinition.length;

			// Always get upcoming intakes (15 min before) for first reminders
			const upcomingIntakes = getUpcomingIntakes(
				medDisplayName,
				[intake],
				REMINDER_MINUTES_BEFORE,
				medicationTakenBy,
				med.pillWeightMg,
				locale,
				tz,
				undefined, // nowOverride
				med.id,
				med.doseUnit ?? "mg"
			);

			// Add upcoming intakes for first reminders
			allUpcoming.push(
				...upcomingIntakes.map((upcomingIntake) => ({
					...upcomingIntake,
					medicationId: med.id,
					blisterIndex: actualIndex,
				}))
			);

			// If repeat reminders enabled, also check for missed intakes (past the intake time)
			if (settings.repeatRemindersEnabled) {
				const missedIntakes = todaysIntakesForThisDefinition.filter(
					(todayIntake) => todayIntake.intakeTime.getTime() < now.getTime()
				);

				// Add missed intakes for repeat reminders (only if not already in upcoming list)
				const upcomingTimes = new Set(upcomingIntakes.map((i) => i.intakeTime.getTime()));
				allUpcoming.push(
					...missedIntakes
						.filter((missed) => !upcomingTimes.has(missed.intakeTime.getTime()))
						.map((missed) => ({
							...missed,
							medicationId: med.id,
							blisterIndex: actualIndex,
						}))
				);
			}
		});
	}

	if (allUpcoming.length === 0) {
		logger.debug(
			`[IntakeReminder] No upcoming intakes in reminder window for user=${username} (userId=${settings.userId}, scheduledToday=${scheduledIntakesTodayCount})`
		);
		return; // No upcoming intakes for today
	}

	logger.info(
		`[IntakeReminder] Candidate intakes for user=${username} (userId=${settings.userId}): scheduledToday=${scheduledIntakesTodayCount}, candidates=${allUpcoming.length}`
	);

	// Determine which doses need reminders (new or repeated)
	const nowMs = Date.now();
	const maxReminders = settings.maxNaggingReminders ?? 5;
	type ReminderWithCount = (typeof allUpcoming)[number] & {
		currentSendCount: number; // 0 = advance reminder (no counter), 1+ = nagging count
		maxReminders: number;
		isAdvanceReminder: boolean; // true if this is the 15-min-before reminder
	};
	let remindersToSend: ReminderWithCount[] = [];

	for (const intake of allUpcoming) {
		const key = `user_${settings.userId}:${intake.medName}:${intake.intakeTime.getTime()}`;
		const existingEntry = state.reminders[key];
		const intakeTimeMs = intake.intakeTime.getTime();
		const isIntakePast = intakeTimeMs < nowMs;

		if (!existingEntry) {
			// New dose - send first reminder
			if (isIntakePast) {
				// Intake time already passed and we have no state entry. Check how recently it was missed.
				const minutesSinceIntake = (nowMs - intakeTimeMs) / 60000;
				const gracePeriodMinutes = (settings.reminderRepeatIntervalMinutes ?? 30) + REMINDER_MINUTES_BEFORE;

				if (minutesSinceIntake <= gracePeriodMinutes) {
					// Recently missed — scheduler likely recovered from sleep/restart.
					// Send a catch-up reminder (counts as first nagging reminder).
					remindersToSend.push({ ...intake, currentSendCount: 1, maxReminders, isAdvanceReminder: false });
				} else {
					// Long ago — seed state without notification (user likely already noticed)
					state.reminders[key] = {
						firstSentAt: nowMs,
						lastSentAt: nowMs,
						sendCount: 0,
						advanceSent: false,
					};
				}
			} else {
				// Upcoming - this is advance reminder (no counter)
				remindersToSend.push({ ...intake, currentSendCount: 0, maxReminders, isAdvanceReminder: true });
			}
		} else if (settings.repeatRemindersEnabled && isIntakePast) {
			// Intake time passed - check if we need to send nagging reminder
			const intervalMs = settings.reminderRepeatIntervalMinutes * 60 * 1000;
			const timeSinceLastReminder = nowMs - existingEntry.lastSentAt;

			// If only advance reminder was sent (sendCount=0), first nagging has count=1
			// Otherwise increment from current sendCount
			const currentNaggingCount = existingEntry.sendCount;

			if (currentNaggingCount >= maxReminders) {
			} else if (timeSinceLastReminder >= intervalMs) {
				const nextSendCount = currentNaggingCount + 1;
				remindersToSend.push({ ...intake, currentSendCount: nextSendCount, maxReminders, isAdvanceReminder: false });
			}
		}
		// Else: Already sent and either repeats disabled or intake not yet past - skip
	}

	if (remindersToSend.length === 0) {
		logger.debug(
			`[IntakeReminder] No reminders to send for user=${username} (userId=${settings.userId}) after state/repeat evaluation`
		);
		return; // All reminders already sent and no repeats needed
	}

	logger.info(
		`[IntakeReminder] Reminders selected for user=${username} (userId=${settings.userId}): count=${remindersToSend.length} :: ${remindersToSend
			.map((intake) =>
				formatIntakeLog({
					medName: intake.medName,
					medicationId: intake.medicationId,
					blisterIndex: intake.blisterIndex,
					intakeTime: intake.intakeTime,
					intakeTimeStr: intake.intakeTimeStr,
					usage: intake.usage,
					doseUnit: intake.doseUnit,
					takenBy: intake.takenBy,
				})
			)
			.join(" | ")}`
	);

	// If skipRemindersForTakenDoses is enabled, filter out doses that were already taken today
	if (settings.skipRemindersForTakenDoses) {
		const beforeFilterCount = remindersToSend.length;
		// Query doses marked as taken today (takenAt is timestamp, stored as seconds since epoch)
		const takenToday = await db
			.select()
			.from(doseTracking)
			.where(
				and(
					eq(doseTracking.userId, settings.userId),
					gte(doseTracking.takenAt, todayStart),
					lte(doseTracking.takenAt, todayEnd)
				)
			);

		const takenDoseIds = new Set(takenToday.map((d) => d.doseId));

		// Filter out reminders for doses that were already taken
		remindersToSend = remindersToSend.filter((intake) => {
			// Convert to date-only timestamp (midnight) to match frontend dose ID format
			const intakeDate = intake.intakeTime;
			const dateOnlyMs = new Date(intakeDate.getFullYear(), intakeDate.getMonth(), intakeDate.getDate()).getTime();

			// Check both with and without person suffix
			if (intake.takenBy) {
				// For person-specific intake, check if that person has taken it
				const doseId = `${intake.medicationId}-${intake.blisterIndex}-${dateOnlyMs}-${intake.takenBy}`;
				const isTaken = takenDoseIds.has(doseId);
				return !isTaken;
			} else {
				// For non-person-specific intakes
				const doseId = `${intake.medicationId}-${intake.blisterIndex}-${dateOnlyMs}`;
				const isTaken = takenDoseIds.has(doseId);
				return !isTaken;
			}
		});

		const filteredOutCount = beforeFilterCount - remindersToSend.length;
		if (filteredOutCount > 0) {
			logger.info(
				`[IntakeReminder] Removed reminders for already taken doses for user=${username} (userId=${settings.userId}): removed=${filteredOutCount}, remaining=${remindersToSend.length}`
			);
		}

		if (remindersToSend.length === 0) {
			logger.info(
				`[IntakeReminder] All candidate reminders already taken for user=${username} (userId=${settings.userId}); nothing to send`
			);
			return;
		}
	}

	// Determine if this is a repeat reminder:
	// - Any intake already has a state entry AND is past (repeat after first reminder)
	// - OR intake is past even without state entry (missed the 15-min window)
	const isRepeatReminder = remindersToSend.some((intake) => {
		const intakeTimeMs = intake.intakeTime.getTime();
		const isIntakePast = intakeTimeMs < nowMs;
		return isIntakePast; // Use repeat message for ANY missed intake
	});

	let emailSuccess = false;
	let shoutrrrSuccess = false;

	// Send email if enabled for intake reminders
	if (emailEnabled) {
		// Calculate counts for repeat reminder text
		const hasNaggingReminder = remindersToSend.some((r) => !r.isAdvanceReminder);
		const highestSendCount = Math.max(...remindersToSend.map((r) => r.currentSendCount));
		const maxReminderCount = remindersToSend[0]?.maxReminders ?? 5;

		const result = await sendIntakeReminderEmail(
			settings.notificationEmail!,
			remindersToSend,
			language,
			isRepeatReminder,
			settings.reminderRepeatIntervalMinutes,
			hasNaggingReminder ? highestSendCount : undefined,
			hasNaggingReminder ? maxReminderCount : undefined
		);
		emailSuccess = result.success;
		if (!result.success) {
			logger.error(
				`[IntakeReminder] Email delivery failed for user=${username} (userId=${settings.userId}): ${result.error}`
			);
		} else {
			logger.info(
				`[IntakeReminder] Email delivered for user=${username} (userId=${settings.userId}, recipient=${settings.notificationEmail}, reminders=${remindersToSend.length}, messageId=${result.messageId ?? "n/a"})`
			);
		}
	}

	// Send Shoutrrr notification if enabled for intake reminders
	if (shoutrrrEnabled) {
		// Check if any reminder is a nagging reminder (not advance)
		const hasNaggingReminder = remindersToSend.some((r) => !r.isAdvanceReminder);
		const highestSendCount = Math.max(...remindersToSend.map((r) => r.currentSendCount));
		const maxReminderCount = remindersToSend[0]?.maxReminders ?? 5;

		let title: string;
		if (hasNaggingReminder && highestSendCount > 0) {
			// Nagging reminder - show counter
			const counterStr = `(${highestSendCount}/${maxReminderCount})`;
			title =
				language === "de"
					? `⚠️ Erinnerung: Medikamenteneinnahme ${counterStr}`
					: `⚠️ Reminder: Medication intake ${counterStr}`;
		} else {
			// Advance reminder - no counter
			title = t(tr.push.intakeTitle, { minutes: REMINDER_MINUTES_BEFORE });
		}

		// Only show repeat note for nagging reminders, not for advance reminders
		let repeatNote = "";
		if (hasNaggingReminder && settings.reminderRepeatIntervalMinutes) {
			const remainingReminders = maxReminderCount - highestSendCount;
			if (remainingReminders <= 0) {
				// Last reminder
				repeatNote = language === "de" ? "\n\n⚠️ Dies ist die letzte Erinnerung." : "\n\n⚠️ This is the last reminder.";
			} else if (remainingReminders === 1) {
				// One more reminder
				repeatNote =
					language === "de"
						? `\n\nℹ️ Eine weitere Erinnerung wird in ${settings.reminderRepeatIntervalMinutes} Minuten gesendet.`
						: `\n\nℹ️ One more reminder will be sent in ${settings.reminderRepeatIntervalMinutes} minutes.`;
			} else {
				// Multiple reminders remaining
				repeatNote =
					language === "de"
						? `\n\nℹ️ ${remainingReminders} weitere Erinnerungen werden alle ${settings.reminderRepeatIntervalMinutes} Minuten gesendet.`
						: `\n\nℹ️ ${remainingReminders} more reminders will be sent every ${settings.reminderRepeatIntervalMinutes} minutes.`;
			}
		}

		const message =
			remindersToSend
				.map((i) => {
					const takenByStr = i.takenBy ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy })}` : "";
					let dosage = `${i.usage} ${i.usage === 1 ? tr.common.pill : tr.common.pills}`;
					if (i.pillWeightMg) {
						const totalMg = i.usage * i.pillWeightMg;
						dosage += totalMg >= 1000 ? ` (${(totalMg / 1000).toFixed(1)} g)` : ` (${totalMg} mg)`;
					}
					return `• ${i.medName}${takenByStr}: ${dosage} @ ${i.intakeTimeStr}`;
				})
				.join("\n") +
			repeatNote +
			`\n\n---\n${getFooterPlain(language)}`;
		const actionMode = remindersToSend.length === 1 ? "full" : "view-only";
		const actionDoseIds = remindersToSend.map((intake) =>
			buildDoseIdForIntake({
				...intake,
				medicationId: intake.medicationId,
				blisterIndex: intake.blisterIndex,
			})
		);
		let actionContext: Awaited<ReturnType<typeof createNotificationActionContext>> | null = null;
		let actionContextFailed = false;
		try {
			actionContext = await createNotificationActionContext({
				userId: settings.userId,
				title,
				message,
				doseIds: actionDoseIds,
				scheduledFor: remindersToSend[0]?.intakeTime ?? new Date(),
				publicAppUrl: env.PUBLIC_APP_URL,
				language,
				actionMode,
			});
		} catch (error) {
			actionContextFailed = true;
			const errorMessage = error instanceof Error ? error.message : String(error);
			logger.error(
				`[IntakeReminder] Notification action context failed for user=${username} (userId=${settings.userId}, provider=${getPushProviderLabel(
					settings.shoutrrrUrl!
				)}, ${formatActionContextLog({ actionMode, doseCount: actionDoseIds.length, actionContext: null })}): ${errorMessage}`
			);
		}
		if (!actionContext) {
			if (actionContextFailed) {
				logger.warn(
					`[IntakeReminder] Sending intake reminders without actions after action context failure for user=${username} (userId=${settings.userId}, provider=${getPushProviderLabel(
						settings.shoutrrrUrl!
					)})`
				);
			} else {
				logger.warn(
					`[IntakeReminder] No reachable public app URL configured; sending intake reminders without actions for user=${username} (userId=${settings.userId})`
				);
			}
		} else {
			logger.info(
				`[IntakeReminder] Notification action context ready for user=${username} (userId=${settings.userId}, provider=${getPushProviderLabel(
					settings.shoutrrrUrl!
				)}, ${formatActionContextLog({ actionMode, doseCount: actionDoseIds.length, actionContext })})`
			);
		}

		const pushProvider = getPushProviderLabel(settings.shoutrrrUrl!);
		logger.info(
			`[IntakeReminder] Sending push reminder for user=${username} (userId=${settings.userId}, provider=${pushProvider}, reminders=${remindersToSend.length}, priority=${hasNaggingReminder ? 4 : 3}, ${formatActionContextLog({ actionMode, doseCount: actionDoseIds.length, actionContext })})`
		);

		const result = await sendPushNotification(settings.shoutrrrUrl!, title, message, {
			actions: actionContext?.actions,
			respondUrl: actionContext?.respondUrl,
			viewUrl: actionContext?.viewUrl,
			clickUrl: actionContext?.respondUrl ?? actionContext?.viewUrl,
			sequenceId: actionContext?.sequenceId,
			tags: ["pill"],
			priority: hasNaggingReminder ? 4 : 3,
		});
		shoutrrrSuccess = result.success;
		if (!result.success) {
			logger.error(
				`[IntakeReminder] Push delivery failed for user=${username} (userId=${settings.userId}, provider=${pushProvider}, reminders=${remindersToSend.length}, ${formatActionContextLog({ actionMode, doseCount: actionDoseIds.length, actionContext })}): ${result.error}`
			);
		} else {
			if (actionContext?.groupId && result.providerMessageId) {
				try {
					await storeNotificationActionGroupNtfyMessageId(actionContext.groupId, result.providerMessageId);
					logger.info(
						`[IntakeReminder] Stored ntfy message id for action group for user=${username} (userId=${settings.userId}, groupId=${actionContext.groupId}, providerMessageId=${result.providerMessageId})`
					);
				} catch (error) {
					const errorMessage = error instanceof Error ? error.message : String(error);
					logger.warn(
						`[IntakeReminder] Failed to store ntfy message id for action group for user=${username} (userId=${settings.userId}, groupId=${actionContext.groupId}, providerMessageId=${result.providerMessageId}): ${errorMessage}`
					);
				}
			} else if (actionContext?.groupId && pushProvider === "ntfy") {
				logger.warn(
					`[IntakeReminder] Push delivered without ntfy message id for action group for user=${username} (userId=${settings.userId}, groupId=${actionContext.groupId})`
				);
			}

			logger.info(
				`[IntakeReminder] Push delivered for user=${username} (userId=${settings.userId}, provider=${pushProvider}, reminders=${remindersToSend.length}, providerMessageId=${result.providerMessageId ?? "n/a"}, ${formatActionContextLog({ actionMode, doseCount: actionDoseIds.length, actionContext })})`
			);
		}
	}

	// Update state if any notification was sent successfully
	if (emailSuccess || shoutrrrSuccess) {
		// Update or create entries for sent reminders
		for (const intake of remindersToSend) {
			const key = `user_${settings.userId}:${intake.medName}:${intake.intakeTime.getTime()}`;
			const existing = state.reminders[key];

			if (existing) {
				// Update existing entry
				if (intake.isAdvanceReminder) {
					// Advance reminder - don't increment nagging count
					state.reminders[key] = {
						...existing,
						lastSentAt: nowMs,
						advanceSent: true,
					};
				} else {
					// Nagging reminder - increment count
					state.reminders[key] = {
						firstSentAt: existing.firstSentAt,
						lastSentAt: nowMs,
						sendCount: existing.sendCount + 1,
						advanceSent: existing.advanceSent,
					};
				}
			} else {
				// Create new entry
				if (intake.isAdvanceReminder) {
					// Advance reminder - sendCount stays 0
					state.reminders[key] = {
						firstSentAt: nowMs,
						lastSentAt: nowMs,
						sendCount: 0,
						advanceSent: true,
					};
				} else {
					// First nagging reminder - sendCount starts at 1
					state.reminders[key] = {
						firstSentAt: nowMs,
						lastSentAt: nowMs,
						sendCount: 1,
						advanceSent: false,
					};
				}
			}
		}

		// Clean up old entries (remove doses from past days)
		state.reminders = cleanOldIntakeReminders(state.reminders, tz);

		saveIntakeReminderState(state);

		// Update global reminder state for UI display
		const singleChannel = emailSuccess ? "email" : "push";
		const channel = emailSuccess && shoutrrrSuccess ? "both" : singleChannel;
		updateReminderSentTime("intake", channel);

		// Also update user settings in database so frontend can display the info
		// Get the first reminder's medication name and taken by for display
		const firstReminder = remindersToSend[0];
		const medName = firstReminder?.medName;
		const takenBy = firstReminder?.takenBy || undefined;
		await updateUserReminderSentTime(settings.userId, "intake", channel, medName, takenBy);
		logger.info(
			`[IntakeReminder] Reminder state persisted for user=${username} (userId=${settings.userId}, channel=${channel}, reminders=${remindersToSend.length}, firstMed=${medName ?? "n/a"}, firstTakenBy=${takenBy ?? "none"})`
		);
	} else {
		logger.info(
			`[IntakeReminder] No reminder channel succeeded for user=${username} (userId=${settings.userId}, remindersAttempted=${remindersToSend.length})`
		);
	}
}

let intakeCheckInterval: NodeJS.Timeout | null = null;

export function startIntakeReminderScheduler(logger: ServiceLogger): void {
	logger.info(`[IntakeReminder] Starting intake reminder scheduler (checks every minute)...`);

	// Run immediately on start
	checkAndSendIntakeReminders(logger).catch((err) => logger.error(`[IntakeReminder] Error: ${err}`));

	// Then run every minute
	intakeCheckInterval = setInterval(() => {
		checkAndSendIntakeReminders(logger).catch((err) => logger.error(`[IntakeReminder] Error: ${err}`));
	}, CHECK_INTERVAL_MS);

	logger.info(`[IntakeReminder] Scheduler started - checking every minute for upcoming intakes`);
}

export function stopIntakeReminderScheduler(): void {
	if (intakeCheckInterval) {
		clearInterval(intakeCheckInterval);
		intakeCheckInterval = null;
	}
}
