// =============================================================================
// Schedule Building and Coverage Calculations
// =============================================================================

import type {
	Blister,
	Coverage,
	Intake,
	Medication,
	PackageType,
	ScheduleEvent,
	StockStatus,
	StockThresholds,
} from "../types";
import { getMedDisplayName, getMedTotal, isLiquidContainerPackageType, isTubePackageType } from "../types";

export function parseLocalDateTime(isoString: string): Date {
	const match = isoString.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):?(\d{2})?/);
	if (!match) {
		return new Date(isoString);
	}

	const [, year, month, day, hour, minute, second] = match;
	return new Date(
		parseInt(year, 10),
		parseInt(month, 10) - 1,
		parseInt(day, 10),
		parseInt(hour, 10),
		parseInt(minute, 10),
		parseInt(second ?? "0", 10)
	);
}

function normalizeIntakeUsageForStock(intake: Intake, med: Medication): number {
	const usage = Number(intake.usage);
	if (!Number.isFinite(usage) || usage <= 0) return 0;
	if (isTubePackageType(med.packageType)) return 0;

	const isLiquidStock = isLiquidContainerPackageType(med.packageType) || med.medicationForm === "liquid";
	if (!isLiquidStock) return usage;

	if (intake.intakeUnit === "tsp") return usage * 5;
	if (intake.intakeUnit === "tbsp") return usage * 15;
	return usage;
}

/**
 * Get intakes for a medication, preferring new intakes format over legacy blisters
 */
function getIntakesForMed(med: Medication): Intake[] {
	// Use new intakes array if available and non-empty
	if (med.intakes && med.intakes.length > 0) {
		return med.intakes;
	}
	// Fallback to legacy blisters (convert to Intake format)
	return med.blisters.map((b) => ({
		usage: b.usage,
		every: b.every,
		start: b.start,
		intakeUnit: null,
		takenBy: null, // Legacy format has no per-intake takenBy
		intakeRemindersEnabled: med.intakeRemindersEnabled ?? false,
	}));
}

/**
 * Get blisters for a medication (for backward compatibility with coverage calculations)
 */
function getBlistersForMed(med: Medication): Blister[] {
	if (med.intakes && med.intakes.length > 0) {
		return med.intakes.map((i) => ({ usage: i.usage, every: i.every, start: i.start }));
	}
	return med.blisters;
}

/**
 * Build schedule preview events for medications
 */
export function buildSchedulePreview(
	meds: Medication[],
	locale: string,
	includePast: boolean = false
): { events: ScheduleEvent[]; today: number; nextThree: number; totalBlisters: number } {
	const events: ScheduleEvent[] = [];
	if (!Array.isArray(meds)) return { events, today: 0, nextThree: 0, totalBlisters: 0 };

	const now = new Date();
	const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
	const end = new Date();
	end.setDate(end.getDate() + 180); // 6 months horizon

	meds.forEach((med) => {
		const intakes = getIntakesForMed(med);
		intakes.forEach((intake, idx) => {
			const start = parseLocalDateTime(intake.start);
			if (Number.isNaN(start.getTime())) return;
			for (let d = new Date(start); d <= end; d.setDate(d.getDate() + intake.every)) {
				const isPast = d < todayStart;
				if (isPast && !includePast) continue;
				const whenMs = d.getTime();
				// Use date-only timestamp for stable ID (immune to time changes)
				// This ensures changing intake times doesn't invalidate past dose tracking
				const dateOnlyMs = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
				events.push({
					id: `${med.id}-${idx}-${dateOnlyMs}`,
					medName: getMedDisplayName(med),
					takenBy: intake.takenBy, // Per-intake takenBy (string | null)
					usage: intake.usage,
					intakeUnit: intake.intakeUnit ?? null,
					when: whenMs,
					isPast,
					timeStr: d.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" }),
					dateStr: d.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "short" }),
					intakeRemindersEnabled: intake.intakeRemindersEnabled,
				});
			}
		});
	});

	events.sort((a, b) => a.when - b.when);

	const todayCount = events.filter((e) => {
		const t = new Date(e.when);
		const n = new Date();
		return t.getFullYear() === n.getFullYear() && t.getMonth() === n.getMonth() && t.getDate() === n.getDate();
	}).length;

	return {
		events,
		today: todayCount,
		nextThree: events.length,
		totalBlisters: meds.reduce((acc, m) => acc + getIntakesForMed(m).length, 0),
	};
}

/**
 * Calculate coverage information for medications
 */
export function calculateCoverage(
	meds: Medication[],
	events: Array<{ medName: string; when: number; id: string }>,
	locale: string,
	reminderDaysBefore: number,
	stockCalculationMode: "automatic" | "manual",
	takenDoses: Set<string>,
	takenDoseTimestamps?: Map<string, number>
): { low: Coverage[]; all: Coverage[] } {
	const MS_PER_DAY = 86_400_000;
	const now = Date.now();

	const coverage: Coverage[] = meds.map((m) => {
		const intakes = getIntakesForMed(m);
		const blisters = getBlistersForMed(m);
		// Count unique people from all intakes (for per-intake takenBy)
		const uniquePeople = new Set<string>();
		intakes.forEach((intake) => {
			if (intake.takenBy) uniquePeople.add(intake.takenBy);
		});
		// Also add medication-level takenBy for backward compatibility
		m.takenBy?.forEach((person) => uniquePeople.add(person));
		const personCount = Math.max(1, uniquePeople.size || m.takenBy?.length || 1);

		// Calculate daily consumption rate per intake, accounting for per-intake takenBy.
		// When an intake has a per-intake takenBy (new format), it represents exactly
		// one person's dose — do NOT multiply by personCount again.
		// For legacy intakes (no takenBy), the intake applies to ALL people.
		let dailyRate = 0;
		blisters.forEach((_s, idx) => {
			const intake = intakes[idx];
			if (!intake) return;
			const usageForStock = normalizeIntakeUsageForStock(intake, m);
			const baseRate = intake.every > 0 ? usageForStock / intake.every : 0;
			if (intake?.takenBy) {
				// Per-intake takenBy: this intake is for exactly 1 person
				dailyRate += baseRate;
			} else {
				// Legacy: this intake applies to all people
				dailyRate += baseRate * personCount;
			}
		});

		let consumed = 0;
		const stockCorrectionCutoff = m.lastStockCorrectionAt ? new Date(m.lastStockCorrectionAt).getTime() : 0;

		if (stockCalculationMode === "automatic") {
			// In automatic mode, stock is reduced automatically based on the schedule.
			// Every scheduled dose counts as consumed once its time has passed.
			// Additionally, if a user marks a future dose as taken BEFORE the scheduled
			// time (early intake), that dose is also counted as consumed immediately.
			// This prevents double-counting: once the scheduled time arrives, the dose
			// was already counted via the early-taken path, not again via time.
			blisters.forEach((s, blisterIdx) => {
				const blisterStart = parseLocalDateTime(s.start).getTime();
				const period = Math.max(1, s.every) * MS_PER_DAY;
				const intake = intakes[blisterIdx];
				if (!intake) return;
				const usageForStock = normalizeIntakeUsageForStock(intake, m);

				// After a stock correction, start counting consumption from the NEXT
				// scheduled dose on this blister's grid, because the user's pill count
				// already reflects all consumption up to the correction time.
				// We align to the schedule grid so that e.g. correction at 15:40 with
				// a daily 15:42 dose counts today's 15:42 dose (2 min later), not
				// tomorrow's dose (24h later as the old code did).
				let effectiveStart: number;
				if (stockCorrectionCutoff > 0 && stockCorrectionCutoff >= blisterStart) {
					const elapsedSinceStart = stockCorrectionCutoff - blisterStart;
					const periodsElapsed = Math.floor(elapsedSinceStart / period);
					effectiveStart = blisterStart + (periodsElapsed + 1) * period;
				} else {
					effectiveStart = blisterStart;
				}
				if (Number.isNaN(effectiveStart)) return;

				const intakePerson = intake?.takenBy;

				// For per-intake takenBy, only count for that person
				// For legacy (no takenBy), count for all people in medication takenBy
				const fallbackPeople = m.takenBy?.length > 0 ? m.takenBy : [null];
				const peopleForThisIntake = intakePerson ? [intakePerson] : fallbackPeople;

				// Time-based: count doses where the scheduled time has already passed
				let timeBasedConsumed = 0;
				let lastAutoConsumedDateMs = 0;

				if (effectiveStart <= now) {
					const occurrences = Math.floor((now - effectiveStart) / period) + 1;
					timeBasedConsumed = occurrences * usageForStock * peopleForThisIntake.length;

					// Date-only timestamp of the last auto-consumed dose
					const lastDoseTime = new Date(effectiveStart + (occurrences - 1) * period);
					lastAutoConsumedDateMs = new Date(
						lastDoseTime.getFullYear(),
						lastDoseTime.getMonth(),
						lastDoseTime.getDate()
					).getTime();
				}

				// Early intakes: count future doses already marked as taken.
				// The cutoff is the later of: last auto-consumed date or stock correction date.
				// This prevents double-counting (time-based + early-taken) and respects corrections.
				const stockCorrectionDateOnly =
					stockCorrectionCutoff > 0
						? new Date(
								new Date(stockCorrectionCutoff).getFullYear(),
								new Date(stockCorrectionCutoff).getMonth(),
								new Date(stockCorrectionCutoff).getDate()
							).getTime()
						: 0;
				const earlyCutoff = Math.max(lastAutoConsumedDateMs, stockCorrectionDateOnly);

				let earlyTakenConsumed = 0;
				for (const doseId of takenDoses) {
					const parts = doseId.split("-");
					if (parts.length >= 3) {
						const medId = parseInt(parts[0], 10);
						const bIdx = parseInt(parts[1], 10);
						const timestamp = parseInt(parts[2], 10);
						if (medId === m.id && bIdx === blisterIdx && timestamp > earlyCutoff) {
							earlyTakenConsumed += usageForStock;
						}
					}
				}

				consumed += timeBasedConsumed + earlyTakenConsumed;
			});
		} else {
			// In manual mode, only count doses that are explicitly marked as taken.
			// For stock correction filtering, we use the actual time the dose was marked
			// as taken (takenAt), not the scheduled date. This correctly handles same-day
			// scenarios: if a user corrects stock at 3pm, then takes a dose at 4pm,
			// the dose counts because takenAt (4pm) > correctionTime (3pm).
			takenDoses.forEach((doseId) => {
				const parts = doseId.split("-");
				if (parts.length >= 3) {
					const medId = parseInt(parts[0], 10);
					const blisterIdx = parseInt(parts[1], 10);
					const doseTimestamp = parseInt(parts[2], 10);
					if (medId === m.id && blisters[blisterIdx]) {
						const intake = intakes[blisterIdx];
						if (!intake) return;
						const usageForStock = normalizeIntakeUsageForStock(intake, m);
						// Convert blister start to date-only for comparison (dose timestamps are date-only)
						const blisterStartDate = new Date(blisters[blisterIdx].start);
						const blisterStartDateOnly = new Date(
							blisterStartDate.getFullYear(),
							blisterStartDate.getMonth(),
							blisterStartDate.getDate()
						).getTime();

						// Use actual takenAt timestamp for stock correction comparison.
						// A dose counts only if it was MARKED after the stock correction,
						// regardless of what day it was scheduled for.
						const takenAt = takenDoseTimestamps?.get(doseId) ?? 0;
						const afterCorrectionOrNoCorrectionMs = stockCorrectionCutoff === 0 || takenAt > stockCorrectionCutoff;

						if (
							!Number.isNaN(blisterStartDateOnly) &&
							doseTimestamp >= blisterStartDateOnly &&
							afterCorrectionOrNoCorrectionMs
						) {
							consumed += usageForStock;
						}
					}
				}
			});
		}

		const totalPills = getMedTotal(m);
		const medsLeft = Math.max(0, totalPills - consumed);
		const rawDaysLeft = dailyRate > 0 ? medsLeft / dailyRate : null;
		const daysLeft = rawDaysLeft !== null ? Math.max(0, Math.floor(rawDaysLeft)) : null;
		const depletionMs = daysLeft !== null ? now + daysLeft * MS_PER_DAY : null;
		const depletionDate =
			depletionMs !== null
				? new Date(depletionMs).toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "short" })
				: null;
		const displayName = getMedDisplayName(m);
		const nextEvent = events.find((e) => e.medName === displayName);

		return {
			name: displayName,
			medsLeft: Number(medsLeft.toFixed(1)),
			daysLeft,
			depletionDate,
			depletionTime: depletionMs,
			nextDose: nextEvent
				? new Date(nextEvent.when).toLocaleString(locale, {
						weekday: "short",
						day: "2-digit",
						month: "short",
						hour: "2-digit",
						minute: "2-digit",
					})
				: null,
		};
	});

	const low = coverage.filter((c) => c.medsLeft <= 0 || (c.daysLeft !== null && c.daysLeft <= reminderDaysBefore));
	return { low, all: coverage };
}

function getLiquidDerivedThresholds(baselineDays: number): { lowDays: number; criticalDays: number } {
	const lowDays = Math.max(1, Math.floor(baselineDays));
	const criticalDays = Math.max(1, Math.ceil(lowDays / 2));
	return { lowDays, criticalDays };
}

/**
 * Get stock status based on days left and thresholds
 */
export function getStockStatus(
	daysLeft: number | null,
	medsLeft: number,
	thresholds: StockThresholds,
	packageType?: PackageType
): StockStatus {
	// Out of stock or completely depleted = danger (red)
	if (medsLeft <= 0 || daysLeft === 0) {
		return { level: "out-of-stock", className: "danger", label: "status.outOfStock" };
	}

	// Tube has no stock reminder semantics.
	if (isTubePackageType(packageType)) {
		return { level: "normal", className: "success", label: "status.noSchedule" };
	}

	// No schedule, but has stock = normal
	if (daysLeft === null) {
		return { level: "normal", className: "success", label: "status.noSchedule" };
	}

	if (isLiquidContainerPackageType(packageType)) {
		const liquidThresholds = getLiquidDerivedThresholds(thresholds.criticalStockDays);
		if (daysLeft <= liquidThresholds.criticalDays) {
			return { level: "critical", className: "danger", label: "status.criticalStock" };
		}
		if (daysLeft <= liquidThresholds.lowDays) {
			return { level: "low", className: "warning", label: "status.lowStock" };
		}
		return { level: "normal", className: "success", label: "status.normal" };
	}

	// High stock
	if (daysLeft > thresholds.highStockDays) {
		return { level: "high", className: "high", label: "status.highStock" };
	}

	// Normal stock
	if (daysLeft >= thresholds.lowStockDays) {
		return { level: "normal", className: "success", label: "status.normal" };
	}

	// Critical: at or below critical threshold = danger (red)
	if (daysLeft <= thresholds.criticalStockDays) {
		return { level: "critical", className: "danger", label: "status.criticalStock" };
	}

	// Low stock: below lowStockDays but above critical = warning (yellow)
	return { level: "low", className: "warning", label: "status.lowStock" };
}

/**
 * Get next reminder date for a medication
 */
export function getNextReminderForMed(med: Coverage, reminderDaysBefore: number, locale: string): string {
	if (!med.depletionTime) return "—";

	const reminderTime = med.depletionTime - reminderDaysBefore * 86_400_000;
	const now = Date.now();

	if (reminderTime <= now) {
		return "Due now";
	}

	return new Date(reminderTime).toLocaleDateString(locale, {
		day: "2-digit",
		month: "short",
	});
}

/**
 * Get reminder status text for dashboard display
 */
export function getReminderStatusText(
	reminderDaysBefore: number,
	lowStockDays: number,
	_lowStock: Coverage[],
	allCoverage: Coverage[],
	lastSent: string | null,
	lastType: "stock" | "intake" | null,
	lastChannel: "email" | "push" | "both" | null,
	t: (key: string, options?: Record<string, unknown>) => string,
	locale: string
): { lines: Array<{ text: string; className?: string; strong?: boolean }> } {
	const emptyMeds = allCoverage.filter((c) => c.medsLeft <= 0);
	const medsNeedingReminder = allCoverage
		.filter((c) => c.medsLeft > 0 && c.daysLeft !== null && c.daysLeft <= reminderDaysBefore)
		.sort((a, b) => (a.daysLeft ?? 0) - (b.daysLeft ?? 0));
	const lowStockNotYetCritical = allCoverage.filter(
		(c) => c.medsLeft > 0 && c.daysLeft !== null && c.daysLeft > reminderDaysBefore && c.daysLeft < lowStockDays
	);

	const formatLastSent = (iso: string) => {
		const date = new Date(iso);
		return date.toLocaleDateString(locale, { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
	};

	const getTypeLabel = () =>
		lastType === "intake" ? t("dashboard.reminders.typeIntake") : t("dashboard.reminders.typeStock");
	const getChannelLabel = () => {
		if (lastChannel === "both") return t("dashboard.reminders.channelBoth");
		if (lastChannel === "push") return t("dashboard.reminders.channelPush");
		return t("dashboard.reminders.channelEmail");
	};

	const formatLastInfo = (iso: string) => {
		const dateStr = formatLastSent(iso);
		if (lastType && lastChannel) {
			return `${dateStr} (${getTypeLabel()}, ${getChannelLabel()})`;
		}
		return dateStr;
	};

	const lines: Array<{ text: string; className?: string; strong?: boolean }> = [];

	if (emptyMeds.length > 0) {
		lines.push({
			text: `🚨 ${t("dashboard.reminders.emptyStock", { count: emptyMeds.length })}`,
			className: "danger-text",
			strong: true,
		});
		if (medsNeedingReminder.length > 0) {
			lines.push({
				text: `⚠ ${t("dashboard.reminders.needRefill", { count: medsNeedingReminder.length })}`,
				className: "danger-text",
			});
		}
		if (lowStockNotYetCritical.length > 0) {
			lines.push({
				text: t("dashboard.reminders.lowWarning", { count: lowStockNotYetCritical.length }),
				className: "warning-text",
			});
		}
		if (lastSent) {
			lines.push({ text: `${t("dashboard.reminders.lastReminder")}: ${formatLastInfo(lastSent)}` });
		}
		return { lines };
	}

	if (medsNeedingReminder.length > 0) {
		lines.push({
			text: `⚠ ${t("dashboard.reminders.needRefill", { count: medsNeedingReminder.length })}`,
			className: "danger-text",
			strong: true,
		});
		if (lowStockNotYetCritical.length > 0) {
			lines.push({
				text: t("dashboard.reminders.lowWarning", { count: lowStockNotYetCritical.length }),
				className: "warning-text",
			});
		}
		if (lastSent) {
			lines.push({ text: `${t("dashboard.reminders.lastReminder")}: ${formatLastInfo(lastSent)}` });
		}
		return { lines };
	}

	if (lowStockNotYetCritical.length > 0) {
		const nextMed = lowStockNotYetCritical.sort((a, b) => (a.daysLeft ?? 0) - (b.daysLeft ?? 0))[0];
		const daysUntilReminder = Math.max(0, (nextMed.daysLeft ?? 0) - reminderDaysBefore);
		lines.push({
			text: t("dashboard.reminders.lowWarning", { count: lowStockNotYetCritical.length }),
			className: "warning-text",
		});
		lines.push({
			text: `${t("dashboard.reminders.nextIn")}: ${nextMed.name} ${t("dashboard.reminders.inDays", { days: daysUntilReminder })}`,
		});
		return { lines };
	}

	const allWithDepletion = allCoverage
		.filter((c) => c.depletionTime !== null && c.daysLeft !== null && c.medsLeft > 0)
		.sort((a, b) => (a.daysLeft ?? Infinity) - (b.daysLeft ?? Infinity));

	if (allWithDepletion.length > 0) {
		const nextMed = allWithDepletion[0];
		const daysUntilReminder = (nextMed.daysLeft ?? 0) - reminderDaysBefore;
		if (daysUntilReminder > 0) {
			lines.push({ text: `✓ ${t("dashboard.reminders.allOk")}`, className: "success-text" });
			lines.push({
				text: `${t("dashboard.reminders.nextIn")}: ${nextMed.name} ${t("dashboard.reminders.inDays", { days: daysUntilReminder })}`,
			});
			return { lines };
		}
	}

	lines.push({ text: `✓ ${t("dashboard.reminders.allStockOk")}`, className: "success-text" });
	if (lastSent) {
		lines.push({ text: `${t("dashboard.reminders.lastReminder")}: ${formatLastInfo(lastSent)}` });
	} else {
		lines.push({ text: t("dashboard.reminders.noRemindersNeeded") });
	}
	return { lines };
}

// =============================================================================
// Dose Dismissal & Missed Dose Computation
// =============================================================================

/**
 * Check if a dose is dismissed based on its ID and a dismissedUntil date string.
 * Extracts the date-only timestamp from the dose ID and compares it with the dismissedUntil date.
 *
 * @param doseId - Dose ID in format "medId-intakeIdx-dateOnlyMs" or "medId-intakeIdx-dateOnlyMs-person"
 * @param dismissedUntilDate - YYYY-MM-DD formatted date string, or undefined if not dismissed
 * @returns true if the dose date is on or before the dismissedUntil date
 */
export function isDoseDismissed(doseId: string, dismissedUntilDate: string | undefined): boolean {
	if (!dismissedUntilDate) return false;
	const parts = doseId.split("-");
	if (parts.length < 3) return false;
	const timestamp = parseInt(parts[2], 10);
	if (Number.isNaN(timestamp)) return false;
	const doseDate = new Date(timestamp);
	const doseDateStr = `${doseDate.getFullYear()}-${String(doseDate.getMonth() + 1).padStart(2, "0")}-${String(doseDate.getDate()).padStart(2, "0")}`;
	return doseDateStr <= dismissedUntilDate;
}

/**
 * Compute the list of missed past dose IDs.
 * A dose is "missed" if it is in the past, not taken, not individually dismissed,
 * and not covered by the medication's dismissedUntil date.
 *
 * @param pastDays - Grouped schedule days that are in the past
 * @param medications - Full medication list (used to look up dismissedUntil)
 * @param takenDoses - Set of dose IDs marked as taken
 * @param dismissedDoses - Set of dose IDs individually dismissed
 * @returns Array of dose IDs that are missed
 */
/**
 * Compute the full set of dose IDs for a list of doses, correctly handling
 * per-intake takenBy arrays. Empty arrays produce base IDs (no suffix),
 * non-empty arrays produce one ID per person with a `-person` suffix.
 */
export function expandDoseIds(doses: ReadonlyArray<{ id: string; takenBy: string[] }>): string[] {
	return doses.flatMap((d) => {
		const takenByArray = Array.isArray(d.takenBy) ? d.takenBy : [];
		return takenByArray.length > 0 ? takenByArray.map((p) => `${d.id}-${p}`) : [d.id];
	});
}

export function computeMissedPastDoseIds(
	pastDays: ReadonlyArray<{
		meds: ReadonlyArray<{
			medName: string;
			doses: ReadonlyArray<{ id: string; takenBy: string[] }>;
		}>;
	}>,
	medications: ReadonlyArray<{ name: string; genericName?: string | null; dismissedUntil?: string | null }>,
	takenDoses: Set<string>,
	dismissedDoses: Set<string>
): string[] {
	const totalPastDoses = pastDays.flatMap((d) =>
		d.meds.flatMap((m) => {
			const med = medications.find((medication) => getMedDisplayName(medication as Medication) === m.medName);
			const dismissedUntilDate = med?.dismissedUntil ?? undefined;

			return m.doses.flatMap((dose) => {
				if (isDoseDismissed(dose.id, dismissedUntilDate)) {
					return [];
				}

				const takenByArray = Array.isArray(dose.takenBy) ? dose.takenBy : [];
				return takenByArray.length > 0 ? takenByArray.map((p: string) => `${dose.id}-${p}`) : [dose.id];
			});
		})
	);
	return totalPastDoses.filter((id) => !takenDoses.has(id) && !dismissedDoses.has(id));
}
