/**
 * Alarm subsystem — Thunderbird/Outlook-style reminder popups.
 *
 * Design decisions (2026-04-24):
 *   - One shared subsystem for calendar events + tasks (+ future mail reminders).
 *   - Popup shows item title, scheduled time, Snooze (5 / 15 / 30 min / 1 hr /
 *     custom), Dismiss. Snooze delays the alarm by the chosen interval; Dismiss
 *     suppresses it permanently.
 *   - Dismissed / snoozed state lives in localStorage (per-device). Google
 *     Calendar's own reminders aren't mutated by this — mailx's popup is a
 *     local convenience, not a reminder-authority replacement.
 *   - Default lead time: 10 min before calendar event start; at task due time
 *     for tasks. Per-event overrides from Google Calendar's `reminders.overrides`
 *     are a follow-up (we don't currently fetch that field).
 *
 * Check cadence: every 30 s while the tab/window is focused. No check when
 * hidden — alarm won't fire until user returns, which matches Thunderbird's
 * behavior (focus-gated). OS-level notifications are a separate follow-up.
 */
import { getCalendarEvents, getTasks, showReminderPopup, deleteCalendarEvent, logClientEvent, getReminderSound } from "../lib/api-client.js";
/** Alarm diagnostics. A plain console.log from the WebView never reaches
 *  the daemon log file, so snooze/dismiss/fire decisions were invisible
 *  after the fact — "is snooze working?" couldn't be answered from the
 *  log. alog() mirrors the line to the daemon log via logClientEvent. */
function alog(tag, data) {
    console.log(`[alarm] ${tag}`, data ?? "");
    try {
        logClientEvent(`[alarm] ${tag}`, data);
    }
    catch { /* never break the alarm path */ }
}
const DISMISSED_KEY = "mailx-alarm-dismissed"; // { uuid: true }
const SNOOZED_KEY = "mailx-alarm-snoozed"; // { uuid: epoch-ms-end }
const LOOKBACK_MS = 60 * 60 * 1000; // fire past-due alarms from the last 60 min
const LOOKAHEAD_MS = 2 * 60 * 60 * 1000; // poll window: upcoming 2 hr
const POLL_INTERVAL_MS = 30_000;
// ── Reminder sound ──────────────────────────────────────────────────────────
// Cached resolution of the `reminders.sound` config: {mute} → silent,
// {url} → custom file blob, {} → built-in chime. Resolved once (changing the
// setting needs a restart, like other config). Fetch failures fall to the chime.
let _soundCache = null;
let _lastSoundAt = 0;
async function playReminderSound() {
    const now = Date.now();
    if (now - _lastSoundAt < 2000)
        return; // throttle a burst into one alert
    _lastSoundAt = now;
    try {
        if (_soundCache === null) {
            const r = await getReminderSound().catch(() => ({}));
            if (r?.mute)
                _soundCache = { mute: true };
            else if (r?.dataBase64) {
                const bin = atob(r.dataBase64);
                const bytes = new Uint8Array(bin.length);
                for (let i = 0; i < bin.length; i++)
                    bytes[i] = bin.charCodeAt(i);
                _soundCache = { url: URL.createObjectURL(new Blob([bytes], { type: r.mime || "audio/mpeg" })) };
            }
            else
                _soundCache = {};
        }
        if (_soundCache.mute)
            return;
        if (_soundCache.url) {
            const a = new Audio(_soundCache.url);
            // A custom file that won't decode/play → fall back to the chime.
            a.play().catch(() => playChime());
            return;
        }
        playChime();
    }
    catch {
        try {
            playChime();
        }
        catch { /* no audio available */ }
    }
}
/** Built-in default: a soft rising two-note chime via the Web Audio API — no
 *  asset to load, so it can never fail to fetch. */
function playChime() {
    const Ctx = window.AudioContext || window.webkitAudioContext;
    if (!Ctx)
        return;
    const ctx = new Ctx();
    const t0 = ctx.currentTime;
    const tone = (freq, start, dur) => {
        const o = ctx.createOscillator();
        const g = ctx.createGain();
        o.type = "sine";
        o.frequency.value = freq;
        g.gain.setValueAtTime(0.0001, t0 + start);
        g.gain.linearRampToValueAtTime(0.22, t0 + start + 0.02);
        g.gain.exponentialRampToValueAtTime(0.0001, t0 + start + dur);
        o.connect(g);
        g.connect(ctx.destination);
        o.start(t0 + start);
        o.stop(t0 + start + dur + 0.05);
    };
    tone(880, 0, 0.35); // A5
    tone(1174.66, 0.16, 0.42); // D6
    setTimeout(() => { try {
        ctx.close();
    }
    catch { /* */ } }, 1300);
}
function loadDismissed() {
    try {
        return JSON.parse(localStorage.getItem(DISMISSED_KEY) || "{}");
    }
    catch {
        return {};
    }
}
function saveDismissed(map) {
    try {
        localStorage.setItem(DISMISSED_KEY, JSON.stringify(map));
    }
    catch { /* private mode */ }
}
function loadSnoozed() {
    try {
        return JSON.parse(localStorage.getItem(SNOOZED_KEY) || "{}");
    }
    catch {
        return {};
    }
}
function saveSnoozed(map) {
    try {
        localStorage.setItem(SNOOZED_KEY, JSON.stringify(map));
    }
    catch { /* */ }
}
/** Prune expired entries so the maps don't grow forever. */
function pruneState(now) {
    const snoozed = loadSnoozed();
    let changed = false;
    for (const [uuid, until] of Object.entries(snoozed)) {
        // Snoozed entries older than 7 days have been fired already (or the
        // event is long past); drop them.
        if (until < now - 7 * 86400_000) {
            delete snoozed[uuid];
            changed = true;
        }
    }
    if (changed)
        saveSnoozed(snoozed);
    // Dismissed is sparse — don't bother pruning aggressively.
}
/** Per-occurrence key for dismissed/snoozed/fired tracking. Recurring
 *  events all share `ev.uuid`, so dismissing one occurrence used to
 *  dismiss every future one. Including startMs makes today's 9am
 *  meeting distinct from yesterday's. Tasks also use this — duplicate
 *  taskId+dueMs is fine since completed tasks drop out of getTasks. */
function occKey(uuid, ms) {
    return `${uuid}:${ms}`;
}
async function collectDueAlarms(now) {
    const dismissed = loadDismissed();
    const snoozed = loadSnoozed();
    const items = [];
    try {
        const events = await getCalendarEvents(now - LOOKBACK_MS, now + LOOKAHEAD_MS);
        for (const ev of events) {
            // Prefer providerId (Google Calendar event id) as the dedup
            // root. It's stable across local resyncs — if calendar sync
            // purges + reinserts a row, mailx-uuid changes but providerId
            // doesn't. Using uuid here was the cause of "popup keeps
            // reappearing even when I don't react" (Bob 2026-05-14): a
            // resync mid-popup-display assigned a new uuid → openPopups /
            // firedThisSession / dismissed all keyed on the old one →
            // every poll spawned a NEW popup for what was logically the
            // same event-occurrence. uuid is the fallback for local-only
            // rows that never got a providerId.
            const stableId = ev.providerId || ev.uuid;
            if (!stableId)
                continue;
            const startMs = ev.startMs || 0;
            if (!startMs)
                continue;
            // Per-event popup reminder offsets. Bob 2026-05-13: "alarm
            // should popup for events that have reminders set" — i.e.
            // ONLY fire when the event (or its calendar default) actually
            // configured a popup reminder. The upstream filter in
            // google-sync.ts keeps `method === "popup"` only (Google also
            // supports email/sms; those aren't ours to fire). An empty
            // reminderMinutes here means "no popup reminders" — skip the
            // event entirely instead of fabricating a 10-min default that
            // the user never asked for.
            if (!Array.isArray(ev.reminderMinutes) || ev.reminderMinutes.length === 0)
                continue;
            const offsets = ev.reminderMinutes.map((m) => m * 60_000);
            // Per event-occurrence, emit at most ONE alarm — the one whose
            // alarm time is the latest among those still eligible (i.e.,
            // closest to "now" but already in the past). If Bob set
            // reminders at -60/-30/-15/-5/0 min and mailx wakes from
            // hidden-tab / reboot with several inside the 60-min lookback,
            // we'd otherwise stack one popup per offset. Earlier offsets
            // get auto-dismissed so they can't re-fire later as stragglers.
            const occBaseKey = occKey(stableId, startMs);
            let pick = null;
            const eligibleOffsets = [];
            for (const offsetMs of offsets) {
                const key = `${occBaseKey}@${offsetMs}`;
                if (dismissed[key])
                    continue;
                const alarm = startMs - offsetMs;
                const effective = snoozed[key] || alarm;
                if (effective <= now && effective > now - LOOKBACK_MS) {
                    eligibleOffsets.push(offsetMs);
                    if (!pick || effective > pick.effective) {
                        pick = { offsetMs, effective, key };
                    }
                }
            }
            if (pick) {
                // Diagnostic for "dismissed reminder reappears after restart"
                // (Bob 2026-05-16): log the exact fire key so a re-fire can
                // be compared against the `[alarm] dismiss saved` line from
                // before the restart — a mismatch pinpoints a key shift
                // (stableId / startMs instability across resync).
                alog("fire decision", { key: pick.key, title: ev.title || "", startMs, effective: pick.effective });
                items.push({
                    uuid: pick.key, kind: "calendar",
                    title: ev.title || "(no title)",
                    alarmMs: pick.effective, whenMs: startMs,
                    htmlLink: ev.htmlLink,
                });
                // Suppress the earlier offsets so they don't fire as
                // stragglers next poll. Persist immediately so a refresh
                // doesn't lose the suppression.
                let touched = false;
                for (const offsetMs of eligibleOffsets) {
                    if (offsetMs === pick.offsetMs)
                        continue;
                    const k = `${occBaseKey}@${offsetMs}`;
                    if (!dismissed[k]) {
                        dismissed[k] = true;
                        touched = true;
                    }
                }
                if (touched)
                    saveDismissed(dismissed);
            }
        }
    }
    catch { /* sidebar will surface API errors; alarm path stays quiet */ }
    try {
        const tasks = await getTasks(false);
        for (const t of tasks) {
            if (!t.uuid || !t.dueMs)
                continue;
            const key = occKey(t.uuid, t.dueMs);
            if (dismissed[key])
                continue;
            const effective = snoozed[key] || t.dueMs;
            if (effective <= now && effective > now - LOOKBACK_MS) {
                items.push({
                    uuid: key, kind: "task",
                    title: t.title || "(no title)",
                    alarmMs: effective, whenMs: t.dueMs,
                });
            }
        }
    }
    catch { /* */ }
    return items;
}
function formatWhen(ms) {
    if (!ms)
        return "";
    const d = new Date(ms);
    return d.toLocaleString(undefined, { weekday: "short", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
}
let firedThisSession = new Set();
/** uuids whose popup is currently visible on screen — set when the
 *  popup opens, cleared when the user resolves it (button or close).
 *  pollAlarms skips uuids in here so a tick that lands while a popup is
 *  still up doesn't open a second window for the same event. Distinct
 *  from `firedThisSession` (which is "shown at any point this session"
 *  and gets cleared by snooze) — `openPopups` is "right now on screen". */
let openPopups = new Set();
function escapeHtml(s) {
    return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[c]));
}
/** Render the popup using an inline DOM overlay — used when no msger
 *  host is available (browser mode, Android shells without
 *  showReminderPopup wired). Returns the same shape as the msger path
 *  so the caller can switch on `r.button` uniformly. */
function showInWebViewPopup(opts) {
    return new Promise(resolve => {
        const overlay = document.createElement("div");
        overlay.className = "alarm-overlay";
        const panel = document.createElement("div");
        panel.className = "alarm-panel";
        const buttonsHtml = opts.buttons.map(b => `<button type="button" class="alarm-btn${b === "Open" || b === "Dismiss" ? " alarm-btn-primary" : ""}" data-button="${escapeHtml(b)}">${escapeHtml(b)}</button>`).join("");
        panel.innerHTML = `
            <div class="alarm-head">
                <span class="alarm-icon">⏰</span>
                <span class="alarm-title">${escapeHtml(opts.title)}</span>
                <button type="button" class="alarm-close" data-button="" aria-label="Close">&times;</button>
            </div>
            <div class="alarm-body">${opts.html}</div>
            <div class="alarm-foot">${buttonsHtml}</div>
        `;
        overlay.appendChild(panel);
        document.body.appendChild(overlay);
        const msgapi = window.msgapi;
        try {
            msgapi?.setAlwaysOnTop?.(true);
        }
        catch { /* */ }
        const cleanup = (button) => {
            overlay.remove();
            try {
                msgapi?.setAlwaysOnTop?.(false);
            }
            catch { /* */ }
            document.removeEventListener("keydown", onKey, true);
            resolve({ button });
        };
        const onKey = (e) => {
            if (e.key === "Escape") {
                e.stopPropagation();
                cleanup("");
            }
        };
        document.addEventListener("keydown", onKey, true);
        panel.querySelectorAll("[data-button]").forEach(btn => {
            btn.addEventListener("click", () => cleanup(btn.dataset.button || ""));
        });
    });
}
async function firePopupForItem(item) {
    // Already-showing guard. The popup chain is fire-and-forget so
    // without an explicit registry there's nothing stopping a second
    // window for the same uuid (e.g., a 2-min Open snooze expires while
    // the user is still looking at the first popup). `openPopups` tracks
    // "currently on screen" — pollAlarms also checks it.
    if (openPopups.has(item.uuid))
        return;
    openPopups.add(item.uuid);
    // Buttons map to msger's `buttons` array for the keyboard-default
    // detection in template mode; in rawHtml we render our own controls,
    // but the array still flows back as the default-button fallback if
    // the user closes the window without picking. Order = priority.
    const buttons = ["snooze", "Dismiss"];
    if (item.htmlLink)
        buttons.push("Open");
    if (item.kind === "calendar")
        buttons.push("Delete");
    // rawHtml mode. msger's design intent is that we deliver any HTML
    // form and the parent process receives whatever `window.ipc.postMessage`
    // serializes — `MessageBoxResult` includes a `form: serde_json::Value`
    // field for exactly this. We mirror Thunderbird's snooze popup: a
    // row of preset chips PLUS a custom "N units" form. Snooze submits
    // `{button:"snooze", form:{minutes:N}}` so the daemon can handle any
    // duration; Dismiss/Open/Delete still discriminate by button label.
    const icon = item.kind === "calendar" ? "📅" : "☑";
    const kindLabel = item.kind === "calendar" ? "Calendar event" : "Task due";
    // Presets in minutes. Tuned to TBird's defaults — covers the common
    // "give me 15 more minutes" through "remind me tomorrow morning."
    const presets = [
        { label: "5m", minutes: 5 },
        { label: "15m", minutes: 15 },
        { label: "30m", minutes: 30 },
        { label: "1h", minutes: 60 },
        { label: "2h", minutes: 120 },
        { label: "1d", minutes: 60 * 24 },
    ];
    const presetChips = presets.map(p => `<button type="button" class="chip" data-snooze-min="${p.minutes}">${p.label}</button>`).join("");
    const actionBtns = ["Dismiss"];
    if (item.htmlLink)
        actionBtns.push("Open");
    if (item.kind === "calendar")
        actionBtns.push("Delete");
    const actionHtml = actionBtns.map(b => `<button type="button" class="action" data-btn="${escapeHtml(b)}">${escapeHtml(b)}</button>`).join("");
    const html = `<!DOCTYPE html>
<html><head><meta charset="utf-8"><style>
  html, body { height: 100%; }
  body { font: 13px system-ui, sans-serif; margin: 0; padding: 14px 16px; background: #fff; color: #222; display:flex; flex-direction:column; box-sizing:border-box; gap: 8px; }
  .icon { display: inline-block; width: 20px; text-align: center; margin-right: 4px; }
  .title { font-size: 1.1em; font-weight: 600; }
  .when  { color: #555; font-size: 0.95em; }
  .kind  { color: #888; font-size: 0.85em; }
  .spacer { flex: 1; }
  .row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  .row-label { color: #555; font-size: 0.85em; margin-right: 2px; }
  button { font: 13px system-ui, sans-serif; cursor: pointer; }
  .chip { padding: 3px 9px; border: 1px solid #ccc; border-radius: 12px; background: #f7f7f7; color: #222; }
  .chip:hover { background: #e8f0fe; border-color: #0066cc; color: #0066cc; }
  .custom { display: flex; align-items: center; gap: 4px; }
  .custom input[type=number] { width: 48px; padding: 2px 4px; border: 1px solid #ccc; border-radius: 3px; font: inherit; }
  .custom select { padding: 2px 4px; border: 1px solid #ccc; border-radius: 3px; font: inherit; background: #fff; }
  .custom button.ok { padding: 2px 8px; border: 1px solid #0066cc; border-radius: 3px; background: #0066cc; color: #fff; }
  .custom button.ok:hover { background: #0052a3; }
  .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
  .actions button.action { padding: 5px 14px; border: none; border-radius: 4px; background: #0066cc; color: #fff; min-width: 70px; }
  .actions button.action:hover { background: #0052a3; }
  .actions button.action[data-btn="Delete"] { background: #b00; }
  .actions button.action[data-btn="Delete"]:hover { background: #800; }
</style></head><body>
  <div class="title"><span class="icon">${icon}</span>${escapeHtml(item.title)}</div>
  <div class="when">${escapeHtml(formatWhen(item.whenMs))}</div>
  <div class="kind">${kindLabel}</div>
  <div class="row">
    <span class="row-label">Snooze:</span>
    ${presetChips}
  </div>
  <div class="row custom">
    <span class="row-label">Custom:</span>
    <input type="number" id="customN" value="5" min="1" max="999">
    <select id="customUnit">
      <option value="1">minutes</option>
      <option value="60">hours</option>
      <option value="1440">days</option>
    </select>
    <button type="button" class="ok" id="customGo">Snooze</button>
  </div>
  <div class="spacer"></div>
  <div class="actions">${actionHtml}</div>
<script>
  function send(payload) {
    try { window.ipc.postMessage(JSON.stringify(payload)); }
    catch (e) { try { window.close(); } catch (_) {} }
  }
  // Preset chips → snooze N minutes
  document.querySelectorAll("button[data-snooze-min]").forEach(function(b) {
    b.addEventListener("click", function() {
      var m = parseInt(b.getAttribute("data-snooze-min"), 10) || 15;
      send({ button: "snooze", form: { minutes: m } });
    });
  });
  // Custom snooze form → snooze N * unit-multiplier minutes
  var customGo = document.getElementById("customGo");
  var customN = document.getElementById("customN");
  var customUnit = document.getElementById("customUnit");
  function submitCustom() {
    var n = parseInt(customN.value, 10);
    if (!isFinite(n) || n < 1) n = 5;
    var mult = parseInt(customUnit.value, 10) || 1;
    send({ button: "snooze", form: { minutes: n * mult } });
  }
  customGo.addEventListener("click", submitCustom);
  customN.addEventListener("keydown", function(e) { if (e.key === "Enter") submitCustom(); });
  // Action buttons (Dismiss/Open/Delete) → discriminate by label.
  document.querySelectorAll("button[data-btn]").forEach(function(b) {
    b.addEventListener("click", function() {
      send({ button: b.getAttribute("data-btn") });
    });
  });
</script>
</body></html>`;
    // Audible alert as the popup appears. Throttled so a burst of simultaneous
    // reminders is one chime, not a cacophony.
    void playReminderSound();
    const title = item.kind === "calendar" ? "Calendar reminder" : "Task reminder";
    const hasHost = !!window.mailxapi?.showReminderPopup;
    // Result now carries an optional `form` object — see msger's
    // MessageBoxResult.form (serde_json::Value passthrough). Snooze
    // chips/inputs send `{ button: "snooze", form: { minutes: N } }`.
    let r;
    try {
        if (hasHost) {
            r = await showReminderPopup({
                title, html, buttons,
                // Sized for the new snooze form (chips row + custom input
                // row + action buttons) plus title/when/kind. Width fits
                // "5m 15m 30m 1h 2h 1d" on one line in Segoe UI.
                size: { width: 560, height: 300 },
            });
        }
        else {
            r = await showInWebViewPopup({ title, html, buttons });
        }
    }
    catch (e) {
        // Popup IPC failed (msger crash, transport drop). Treat as Dismiss
        // so a re-fire doesn't haunt the user — they engaged with the popup;
        // the failure is on us, not on them (Bob 2026-05-24).
        alog("popup IPC failed — treating as Dismiss", { key: item.uuid, err: e?.message || String(e) });
        r = { button: "Dismiss" };
    }
    finally {
        openPopups.delete(item.uuid);
    }
    // Defensive: a null/undefined result (host returned nothing, window
    // closed without an event, race between snooze-restart and the dismiss
    // popup) used to fall straight to `switch (r.button)` and throw. The
    // throw was swallowed by pollAlarms' fire-and-forget catch, leaving NO
    // dismiss/snooze persisted — so a restart re-fired the same reminder.
    // Treat the void result as Dismiss: the popup HAD explicit buttons,
    // a click→empty-response means "user closed it" (Bob 2026-05-24 log:
    // 21:50:13→21:50:47 popup with no follow-up dismiss/snooze log line).
    if (!r || typeof r !== "object" || typeof r.button !== "string") {
        alog("popup returned empty / malformed — treating as Dismiss", { key: item.uuid, raw: r });
        r = { button: "Dismiss" };
    }
    else {
        alog("popup result", { key: item.uuid, button: r.button, form: r.form });
    }
    const api = window.mailxapi;
    const openLink = () => {
        if (!item.htmlLink)
            return;
        if (api?.openExternal)
            api.openExternal(item.htmlLink);
        else
            window.open(item.htmlLink, "_blank");
    };
    // Wrap the action handler so any throw inside (e.g., snoozeItem can't
    // write to localStorage in private mode, openLink fails) still falls
    // back to a persisted Dismiss. Without this catch a thrown handler
    // returned to pollAlarms' fire-and-forget .catch and the alarm came
    // back on the next session (Bob 2026-05-24: dismissed reminder
    // reappeared after restart).
    try {
        switch (r.button) {
            case "Open":
                openLink();
                // After opening for edit, treat the event as a brand-new
                // entry: clear all local alarm state so the next poll tick
                // re-fetches the (possibly edited) event and decides anew
                // whether to alarm. Edits can come from any client (web,
                // phone, another mailx instance, a co-organizer) — we just
                // re-evaluate against the synced data. Pushed start out →
                // alarm waits for the new lead time; pulled in → fires again.
                // No carryover.
                //
                // Brief 2-min snooze gives the calendar sync window to pick
                // up edits before the next alarm pass — without it, the
                // next 30-s poll reads the still-stale local cache and
                // re-fires immediately. After 2 min, re-evaluation is
                // against (hopefully) fresh data.
                {
                    const dm = loadDismissed();
                    delete dm[item.uuid];
                    saveDismissed(dm);
                }
                firedThisSession.delete(item.uuid);
                snoozeItem(item, 2);
                break;
            case "snooze": {
                // Generic snooze, duration in `form.minutes`. Falls back to
                // 15 min if the form is malformed or missing the value (the
                // chip / custom-form JS always populates it, so this is just
                // defense-in-depth).
                const m = Math.max(1, Math.floor(r.form?.minutes ?? 15));
                snoozeItem(item, m);
                break;
            }
            // Legacy literal labels — retained so a pre-upgrade popup whose
            // bundle still says "Snooze 15m" / "Snooze 1h" continues to work
            // through a daemon-side rebuild without a UI restart.
            case "Snooze 15m":
                snoozeItem(item, 15);
                break;
            case "Snooze 1h":
                snoozeItem(item, 60);
                break;
            case "Dismiss":
            case "dismissed": // msger reports window-close as r.dismissed=true → daemon
            // wrapper lowercases to "dismissed". Treat the same as
            // explicit Dismiss-button: the popup HAD a clear button
            // for that action, so any close that lands here is the
            // user's "I'm done with this reminder" — not a 15-min
            // snooze. Bob 2026-05-14: "I keep dismissing the
            // reminder but it keeps coming back" — the lowercase
            // path was hitting the snooze-15-min default, so the
            // popup re-fired every 15 min indefinitely.
            case "closed": // user closed via X / Esc with no labeled button —
                // popup HAS Dismiss right there; the user clearly
                // didn't want this. Same treatment.
                {
                    const m = loadDismissed();
                    m[item.uuid] = true;
                    saveDismissed(m);
                    alog("dismiss saved", { key: item.uuid, via: r.button });
                }
                break;
            case "Delete":
                // Remove the event server-side. Local row gets the pending-delete
                // mark inside the service; reconcile picks it up. Also dismiss
                // the alarm so it can't re-fire on a stale local row before the
                // delete propagates.
                {
                    const m = loadDismissed();
                    m[item.uuid] = true;
                    saveDismissed(m);
                }
                firedThisSession.delete(item.uuid);
                deleteCalendarEvent(item.uuid).catch((e) => console.error(`  [alarm] delete failed for ${item.uuid}: ${e?.message || e}`));
                break;
            default:
                // Unrecognized button label — shouldn't happen with the
                // current popup HTML. Log it so we can diagnose, then
                // dismiss permanently (the popup HAD explicit options; if
                // the user picked something we can't decode, don't keep
                // pestering them every 30 s).
                alog("unknown popup result", { button: r.button, form: r.form });
                {
                    const m = loadDismissed();
                    m[item.uuid] = true;
                    saveDismissed(m);
                }
                break;
        }
    }
    catch (e) {
        alog("action handler threw — forcing Dismiss", { key: item.uuid, err: e?.message || String(e) });
        try {
            const m = loadDismissed();
            m[item.uuid] = true;
            saveDismissed(m);
        }
        catch { /* */ }
    }
}
function snoozeItem(item, minutes) {
    const now = Date.now();
    const map = loadSnoozed();
    map[item.uuid] = now + minutes * 60_000;
    saveSnoozed(map);
    firedThisSession.delete(item.uuid);
    // Diagnostic — `key` here is the SAME per-occurrence key collectDueAlarms
    // reads back as `snoozed[key]`, so this line + the next "fire decision"
    // line confirm a snooze actually took (and for how long).
    alog("snooze saved", { key: item.uuid, minutes, untilMs: map[item.uuid] });
}
async function pollAlarms() {
    if (document.hidden)
        return; // don't tick while tab hidden
    const now = Date.now();
    pruneState(now);
    const due = await collectDueAlarms(now);
    // Two filters: skip uuids already fired this session (Snooze + an
    // expired snooze re-allows them; Dismiss permanently suppresses via
    // localStorage), and skip uuids whose popup is currently visible
    // (race guard for fire-and-forget OS popups).
    const fresh = due.filter(d => !firedThisSession.has(d.uuid) && !openPopups.has(d.uuid));
    if (fresh.length === 0)
        return;
    for (const d of fresh)
        firedThisSession.add(d.uuid);
    // One unified path. firePopupForItem picks render target (msger OS
    // window vs in-WebView DOM overlay) internally; same buttons,
    // same post-click logic, same dedup. Fire-and-forget per item —
    // errors logged but never block the poll loop.
    for (const item of fresh) {
        firePopupForItem(item).catch(e => console.error(`  [alarm] popup failed: ${e?.message || e}`));
    }
}
/** Start the alarm poller. Called from app.ts on startup. */
export function startAlarmPoller() {
    if (window.__mailxAlarmPollerRunning)
        return;
    window.__mailxAlarmPollerRunning = true;
    // First check after 5 s so startup isn't jittered by a modal.
    setTimeout(() => { pollAlarms().catch(() => { }); }, 5000);
    setInterval(() => { pollAlarms().catch(() => { }); }, POLL_INTERVAL_MS);
    // Re-check on visibility change — if the user comes back to the tab
    // after an hour away, they should see anything they missed immediately.
    document.addEventListener("visibilitychange", () => {
        if (!document.hidden)
            pollAlarms().catch(() => { });
    });
}
//# sourceMappingURL=alarms.js.map