/**
 * Theme C (P1-4) — maintenance-window evaluator.
 *
 * Pure decision function. Given:
 *   - the current time
 *   - the monitorId being checked
 *   - the full set of maintenance windows (typically pre-loaded for
 *     a per-tick window: only rows where deletedAt IS NULL and
 *     endsAt >= now OR recurrence IS NOT NULL)
 *
 * Returns true if the check falls inside an active window. The worker
 * uses this to:
 *   1. Mark the resulting Heartbeat with status='maintenance' instead
 *      of 'down', so SLA math can exclude it.
 *   2. Suppress alert dispatch for the affected monitor (the rules
 *      engine receives no event during a window).
 *
 * Window semantics:
 *   - monitorId=null  -> applies to all monitors (operator-wide)
 *   - one-shot        -> matches when startsAt <= now < endsAt
 *   - weekly recur    -> matches when:
 *       (a) today's day-of-week token is in `recurrence`
 *       (b) UTC clock time falls between startsAt and endsAt of the
 *           original window declaration (start/end interpreted as
 *           same-day clock window in UTC)
 *
 * Recurrence format is a comma-separated subset of:
 *   SUN, MON, TUE, WED, THU, FRI, SAT
 */
import type { MaintenanceWindow } from '@prisma/client';
import { prisma } from '@/lib/prisma';

const DAY_TOKENS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'] as const;

function clockMinutesUtc(d: Date): number {
    return d.getUTCHours() * 60 + d.getUTCMinutes();
}

function dayOfWeekUtc(d: Date): typeof DAY_TOKENS[number] {
    return DAY_TOKENS[d.getUTCDay()];
}

/**
 * One-shot match: now ∈ [startsAt, endsAt).
 */
function matchesOneShot(now: Date, win: MaintenanceWindow): boolean {
    const t = now.getTime();
    return t >= win.startsAt.getTime() && t < win.endsAt.getTime();
}

/**
 * Weekly-recurrence match: same UTC clock window, on a recurrence day.
 */
function matchesRecurrence(now: Date, win: MaintenanceWindow): boolean {
    if (!win.recurrence) return false;
    const days = win.recurrence
        .split(',')
        .map((s) => s.trim().toUpperCase());
    if (!days.includes(dayOfWeekUtc(now))) return false;

    const nowMin = clockMinutesUtc(now);
    const startMin = clockMinutesUtc(win.startsAt);
    const endMin = clockMinutesUtc(win.endsAt);

    // Handle windows that cross midnight (e.g., 23:00 → 02:00) by allowing
    // wrap-around.
    if (startMin <= endMin) {
        return nowMin >= startMin && nowMin < endMin;
    }
    return nowMin >= startMin || nowMin < endMin;
}

export function isWithinMaintenanceWindow(
    now: Date,
    monitorId: number,
    windows: MaintenanceWindow[]
): boolean {
    for (const win of windows) {
        if (win.deletedAt) continue;
        if (win.monitorId !== null && win.monitorId !== monitorId) continue;
        if (matchesOneShot(now, win)) return true;
        if (matchesRecurrence(now, win)) return true;
    }
    return false;
}

/**
 * AUDIT-2 #16.13 (2026-05-23): in-process cache for the worker's
 * per-monitor-check maintenance-window lookup.
 *
 * Background: monitor-engine.ts:65 ran prisma.maintenanceWindow.findMany
 * on every single check. For 500 monitors at 60 s interval, that's
 * 500 queries/min for what's usually an empty table. With WAN
 * latency, each one costs ~150 ms — minutes of wasted DB time per
 * minute of monitoring.
 *
 * Cache strategy:
 *   - 10 s TTL (short enough that newly-added windows take effect
 *     before any operator alarm fires; long enough to eliminate
 *     the per-check round-trip)
 *   - Explicit invalidation via invalidateMaintenanceWindowsCache()
 *     called by the routes that mutate maintenance windows (so the
 *     operator never sees stale data after creating/editing a window)
 *   - Caches the FULL set of "active or recurring, undeleted"
 *     windows — the per-monitor filter happens in
 *     isWithinMaintenanceWindow which is already cheap.
 */
let cachedWindows: MaintenanceWindow[] | null = null;
let cacheExpiresAt = 0;
const CACHE_TTL_MS = 10_000;

export async function getActiveMaintenanceWindowsCached(now: Date): Promise<MaintenanceWindow[]> {
    if (cachedWindows !== null && Date.now() < cacheExpiresAt) {
        return cachedWindows;
    }
    cachedWindows = await prisma.maintenanceWindow.findMany({
        where: {
            deletedAt: null,
            OR: [
                { endsAt: { gte: now } },         // active or future one-shot
                { recurrence: { not: null } },    // recurring (don't filter on endsAt)
            ],
        },
    });
    cacheExpiresAt = Date.now() + CACHE_TTL_MS;
    return cachedWindows;
}

/**
 * Force a refresh on the next call. Used by the maintenance-windows
 * API routes after a mutation. Cheap (just clears the timestamp).
 */
export function invalidateMaintenanceWindowsCache(): void {
    cachedWindows = null;
    cacheExpiresAt = 0;
}

/** Test-only: reset cache between tests. */
export function _resetMaintenanceCacheForTests(): void {
    invalidateMaintenanceWindowsCache();
}
