/**
 * Calendar sidebar — Thunderbird Lightning "Events and Tasks" pane.
 *
 * Right-docked vertical panel showing:
 *   - Day-grouped upcoming events (Today / Tomorrow / Friday Apr 24 / …)
 *   - Tasks list (no due date — just a checkable to-do list)
 *
 * Toggled by the View menu's "Calendar sidebar" checkbox. Reads from the
 * primary account's Google Calendar / Tasks via the existing OAuth token.
 * Sidebar and the full-screen calendar modal (calendar.ts) read the SAME
 * underlying data — two views onto one source.
 *
 * All storage goes through the service-side two-way cache (calendar_events
 * and tasks tables); this file does not use localStorage for data.
 */
import { getCalendarEvents, createCalendarEvent, getTasks, createTask, updateTask, deleteTask, reauthGoogleScopes, } from "../lib/api-client.js";
import { showContextMenu } from "./context-menu.js";
const SIDEBAR_PREF = "mailx-calendar-sidebar-on";
const SHOW_RECURRING_PREF = "mailx-cal-show-recurring";
const SHOW_DONE_PREF = "mailx-task-show-done";
const HORIZON_DAYS_PREF = "mailx-cal-horizon-days";
const HORIZON_DEFAULT_DAYS = 30;
let viewYear = new Date().getFullYear();
let viewMonth = new Date().getMonth();
let viewDay = new Date().getDate();
let lastEvents = [];
function getHorizonDays() {
    try {
        const v = localStorage.getItem(HORIZON_DAYS_PREF);
        const n = v ? parseInt(v, 10) : NaN;
        if (Number.isFinite(n) && n > 0 && n <= 365)
            return n;
    }
    catch { /* */ }
    return HORIZON_DEFAULT_DAYS;
}
function getShowRecurring() {
    try {
        return localStorage.getItem(SHOW_RECURRING_PREF) !== "false";
    }
    catch {
        return true;
    }
}
/** Fetch events from the local two-way cache; service returns local rows
 *  immediately and kicks a background refresh from Google. Next render
 *  (view-nav or user action) picks up the refreshed rows. No localStorage
 *  — everything lives in the service-side DB so phone / desktop share
 *  the same events. */
async function fetchUpcoming(from) {
    const horizon = from.getTime() + getHorizonDays() * 86400_000;
    const rows = await getCalendarEvents(from.getTime(), horizon);
    const showRecurring = getShowRecurring();
    const filtered = showRecurring ? rows : rows.filter((r) => !r.recurringEventId);
    return filtered.map((r) => ({
        id: r.uuid,
        title: r.title,
        start: r.startMs,
        end: r.endMs,
        allDay: !!r.allDay,
        location: r.location,
        notes: r.notes,
        source: r.providerId ? "google" : "local",
        recurringEventId: r.recurringEventId,
        htmlLink: r.htmlLink,
    }));
}
function formatDayHeader(d, today, tomorrow) {
    const sameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
    if (sameDay(d, today))
        return "Today";
    if (sameDay(d, tomorrow))
        return "Tomorrow";
    return d.toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" });
}
function formatTime(e) {
    if (e.allDay)
        return "all day";
    return new Date(e.start).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false });
}
function escapeHtml(s) {
    return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[c]));
}
function renderHead() {
    const dateEl = document.getElementById("cal-side-date");
    if (!dateEl)
        return;
    const d = new Date(viewYear, viewMonth, viewDay);
    dateEl.innerHTML = `<strong>${d.getDate()}</strong> ${d.toLocaleDateString(undefined, { weekday: "short" })} <span class="cal-side-date-month">${d.toLocaleDateString(undefined, { month: "short", year: "numeric" })}</span>`;
}
function renderEvents(events) {
    const body = document.getElementById("cal-side-body");
    if (!body)
        return;
    if (events.length === 0) {
        body.innerHTML = `<div class="cal-side-empty">No upcoming events. Click + New event to add one.</div>`;
        return;
    }
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const tomorrow = new Date(today.getTime() + 86400_000);
    // Daily-recurring detection: group expanded instances by recurringEventId,
    // pull out series that fire (roughly) every day at the same time of day,
    // and render them once at the top instead of cluttering each per-day
    // section. "Daily" = same recurringEventId + same H:MM + appears on at
    // least 80% of the days in the rendered horizon (allows for the
    // occasional skipped day without losing the grouping).
    const horizonDays = getHorizonDays();
    const dailyThreshold = Math.max(2, Math.floor(horizonDays * 0.8));
    const byRecurId = new Map();
    for (const e of events) {
        if (!e.recurringEventId)
            continue;
        let arr = byRecurId.get(e.recurringEventId);
        if (!arr) {
            arr = [];
            byRecurId.set(e.recurringEventId, arr);
        }
        arr.push(e);
    }
    const dailyKeys = new Set();
    const dailyHeads = [];
    for (const [recId, instances] of byRecurId) {
        if (instances.length < dailyThreshold)
            continue;
        // Same time-of-day across all instances?
        const hm = (e) => {
            if (e.allDay)
                return "all";
            const d = new Date(e.start);
            return `${d.getHours()}:${d.getMinutes()}`;
        };
        const firstHm = hm(instances[0]);
        const allSameTime = instances.every(e => hm(e) === firstHm);
        if (!allSameTime)
            continue;
        dailyKeys.add(recId);
        // Use the earliest instance as the "head" entry (its htmlLink is
        // identical across instances of the same series).
        dailyHeads.push(instances.reduce((a, b) => a.start < b.start ? a : b));
    }
    let html = "";
    if (dailyHeads.length > 0) {
        html += `<div class="cal-side-day cal-side-day-daily">Daily</div>`;
        for (const e of dailyHeads) {
            const link = e.htmlLink || "";
            html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
                <span class="cal-side-event-dot ${e.source === "google" ? "g" : "l"}"></span>
                <span class="cal-side-event-time">${escapeHtml(formatTime(e))}</span>
                <span class="cal-side-event-title">${escapeHtml(e.title)}<span class="cal-side-event-recur" title="Daily">↻</span></span>
            </div>`;
        }
    }
    let lastDayKey = "";
    for (const e of events) {
        // Filter out daily-grouped instances — they're shown once at top.
        if (e.recurringEventId && dailyKeys.has(e.recurringEventId))
            continue;
        const d = new Date(e.start);
        const dayKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
        if (dayKey !== lastDayKey) {
            html += `<div class="cal-side-day">${escapeHtml(formatDayHeader(d, today, tomorrow))}</div>`;
            lastDayKey = dayKey;
        }
        const recurMark = e.recurringEventId ? `<span class="cal-side-event-recur" title="Recurring event">↻</span>` : "";
        const link = e.htmlLink || "";
        html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
            <span class="cal-side-event-dot ${e.source === "google" ? "g" : "l"}"></span>
            <span class="cal-side-event-time">${escapeHtml(formatTime(e))}</span>
            <span class="cal-side-event-title">${escapeHtml(e.title)}${recurMark}</span>
        </div>`;
    }
    body.innerHTML = html;
    // Click-to-open — interim per user 2026-04-23: route to Google Calendar's
    // web UI via openExternal until we build an in-app event editor.
    // Right-click gives a context menu with "View in browser" explicit.
    // Item 17: on Android, openCalendarEvent redirects to the native Calendar
    // app via ACTION_VIEW; desktop still falls through to the browser.
    const openInCalendar = (url) => {
        const api = window.mailxapi;
        if (api?.openCalendarEvent) {
            api.openCalendarEvent(url);
            return;
        }
        if (api?.openExternal)
            api.openExternal(url);
        else
            window.open(url, "_blank");
    };
    const openInBrowser = (url) => {
        const api = window.mailxapi;
        if (api?.openExternal)
            api.openExternal(url);
        else
            window.open(url, "_blank");
    };
    body.querySelectorAll(".cal-side-event").forEach(el => {
        el.addEventListener("click", () => {
            const link = el.dataset.link;
            if (link)
                openInCalendar(link);
        });
        el.addEventListener("contextmenu", (e) => {
            e.preventDefault();
            const link = el.dataset.link;
            const items = [];
            if (link)
                items.push({
                    label: "View in browser",
                    action: () => openInBrowser(link),
                });
            items.push({
                label: "Open Google Calendar",
                action: () => openInBrowser("https://calendar.google.com/"),
            });
            if (items.length > 0)
                showContextMenu(e.clientX, e.clientY, items);
        });
    });
}
async function renderTasks() {
    const cb = document.getElementById("cal-side-show-done");
    const showDone = cb?.checked ?? false;
    const tasks = await getTasks(showDone);
    const host = document.getElementById("cal-side-tasks");
    if (!host)
        return;
    if (tasks.length === 0) {
        host.innerHTML = `<div class="cal-side-empty">No tasks.</div>`;
        return;
    }
    let html = "<div class='cal-side-task-head'>Title</div>";
    for (const t of tasks) {
        const done = !!t.completedMs;
        html += `<div class="cal-side-task" data-uuid="${t.uuid}">
            <input type="checkbox" ${done ? "checked" : ""} class="cal-side-task-check">
            <span class="cal-side-task-title${done ? " done" : ""}">${escapeHtml(t.title)}</span>
            <button class="cal-side-task-delete" title="Delete task" aria-label="Delete task">×</button>
        </div>`;
    }
    host.innerHTML = html;
    const openInBrowser = (url) => {
        const api = window.mailxapi;
        if (api?.openExternal)
            api.openExternal(url);
        else
            window.open(url, "_blank");
    };
    host.querySelectorAll(".cal-side-task").forEach(row => {
        const uuid = row.dataset.uuid;
        row.querySelector(".cal-side-task-check")?.addEventListener("change", async (e) => {
            const checked = e.target.checked;
            await updateTask(uuid, { completedMs: checked ? Date.now() : null });
            renderTasks();
        });
        row.querySelector(".cal-side-task-delete")?.addEventListener("click", async () => {
            await deleteTask(uuid);
            renderTasks();
        });
        row.addEventListener("contextmenu", (e) => {
            e.preventDefault();
            // Google Tasks doesn't have a per-task web URL; open the list view.
            showContextMenu(e.clientX, e.clientY, [
                { label: "View in Google Tasks", action: () => openInBrowser("https://tasks.google.com/") },
                { label: "", action: () => { }, separator: true },
                { label: "Delete task", action: async () => { await deleteTask(uuid); renderTasks(); } },
            ]);
        });
    });
}
async function refresh() {
    renderHead();
    const from = new Date(viewYear, viewMonth, viewDay);
    lastEvents = await fetchUpcoming(from);
    renderEvents(lastEvents);
    renderTasks();
}
/** Show the sidebar (called from the View menu toggle). Idempotent. */
export async function showCalendarSidebar() {
    const el = document.getElementById("calendar-sidebar");
    if (!el)
        return;
    el.hidden = false;
    document.body.classList.add("calendar-sidebar-on");
    try {
        localStorage.setItem(SIDEBAR_PREF, "true");
    }
    catch { /* */ }
    await refresh();
}
export function hideCalendarSidebar() {
    const el = document.getElementById("calendar-sidebar");
    if (!el)
        return;
    el.hidden = true;
    document.body.classList.remove("calendar-sidebar-on");
    try {
        localStorage.setItem(SIDEBAR_PREF, "false");
    }
    catch { /* */ }
}
export function isCalendarSidebarOn() {
    // Default is ON — user-reported 2026-04-23 that the sidebar should be
    // visible by default, not hidden. An explicit "false" in localStorage
    // still hides it (the user's stored preference wins).
    try {
        const v = localStorage.getItem(SIDEBAR_PREF);
        return v === null ? true : v !== "false";
    }
    catch {
        return true;
    }
}
/** Wire one-time event handlers + restore from localStorage. Safe to call
 *  multiple times — handlers are idempotent because the elements are stable. */
export function initCalendarSidebar() {
    const wireOnce = (id, fn) => {
        const el = document.getElementById(id);
        if (!el || el.__wired)
            return;
        el.__wired = true;
        el.addEventListener("click", fn);
    };
    wireOnce("cal-side-prev", () => { const d = new Date(viewYear, viewMonth, viewDay - 1); viewYear = d.getFullYear(); viewMonth = d.getMonth(); viewDay = d.getDate(); refresh(); });
    wireOnce("cal-side-next", () => { const d = new Date(viewYear, viewMonth, viewDay + 1); viewYear = d.getFullYear(); viewMonth = d.getMonth(); viewDay = d.getDate(); refresh(); });
    wireOnce("cal-side-today", () => { const t = new Date(); viewYear = t.getFullYear(); viewMonth = t.getMonth(); viewDay = t.getDate(); refresh(); });
    wireOnce("cal-side-new", async () => {
        const title = prompt("Event title:");
        if (!title)
            return;
        const dateStr = prompt("Date and time (YYYY-MM-DD HH:MM, or YYYY-MM-DD for all-day):", new Date(viewYear, viewMonth, viewDay).toISOString().slice(0, 10) + " 09:00");
        if (!dateStr)
            return;
        const allDay = !/\d{1,2}:\d{2}/.test(dateStr);
        const start = Date.parse(dateStr.replace(" ", "T"));
        if (isNaN(start)) {
            alert("Couldn't parse that date.");
            return;
        }
        // Two-way cache: commit locally, service queues the push to Google.
        await createCalendarEvent({
            title, startMs: start, endMs: start + (allDay ? 86400_000 : 3600_000), allDay,
        });
        await refresh();
    });
    wireOnce("cal-side-new-task", async () => {
        const title = prompt("Task title:");
        if (!title)
            return;
        await createTask({ title });
        renderTasks();
    });
    // Manual refresh — getTasks on the service side already fires a Google
    // pull under the hood on every call, but the visible feedback (spinning
    // glyph for ~600 ms, disabled while in flight) makes it explicit that
    // the user asked for a fresh pull, not a cached redraw.
    wireOnce("cal-side-refresh-tasks", async () => {
        const btn = document.getElementById("cal-side-refresh-tasks");
        if (btn?.classList.contains("cal-side-refreshing"))
            return;
        btn?.classList.add("cal-side-refreshing");
        btn.disabled = true;
        try {
            // renderTasks() calls getTasks() on the service which triggers
            // the async Google pull; the subsequent tasksUpdated event
            // re-renders with the merged result.
            await renderTasks();
        }
        finally {
            setTimeout(() => {
                btn?.classList.remove("cal-side-refreshing");
                if (btn)
                    btn.disabled = false;
            }, 600);
        }
    });
    // Manual events refresh — same pattern as tasks. fetchUpcoming hits the
    // service which kicks a Google pull under the hood.
    wireOnce("cal-side-refresh-events", async () => {
        const btn = document.getElementById("cal-side-refresh-events");
        if (btn?.classList.contains("cal-side-refreshing"))
            return;
        btn?.classList.add("cal-side-refreshing");
        if (btn)
            btn.disabled = true;
        try {
            await refresh();
        }
        finally {
            setTimeout(() => {
                btn?.classList.remove("cal-side-refreshing");
                if (btn)
                    btn.disabled = false;
            }, 600);
        }
    });
    const showDoneCb = document.getElementById("cal-side-show-done");
    if (showDoneCb && !showDoneCb.__wired) {
        showDoneCb.__wired = true;
        // Sticky: restore prior state and persist on change. Default off.
        try {
            showDoneCb.checked = localStorage.getItem(SHOW_DONE_PREF) === "true";
        }
        catch { /* */ }
        showDoneCb.addEventListener("change", () => {
            try {
                localStorage.setItem(SHOW_DONE_PREF, String(showDoneCb.checked));
            }
            catch { /* */ }
            renderTasks();
        });
    }
    // Recurring-events filter toggle — hides expanded recurring-series
    // instances when unchecked. Default on so new users see everything.
    const recurCb = document.getElementById("cal-side-show-recurring");
    if (recurCb && !recurCb.__wired) {
        recurCb.__wired = true;
        recurCb.checked = getShowRecurring();
        recurCb.addEventListener("change", () => {
            try {
                localStorage.setItem(SHOW_RECURRING_PREF, String(recurCb.checked));
            }
            catch { /* */ }
            refresh();
        });
    }
    // Horizon input — how many days ahead to list events. Bounded 1..365.
    const horizonInput = document.getElementById("cal-side-horizon");
    if (horizonInput && !horizonInput.__wired) {
        horizonInput.__wired = true;
        horizonInput.value = String(getHorizonDays());
        const commit = () => {
            const n = parseInt(horizonInput.value, 10);
            const clamped = Number.isFinite(n) && n > 0 ? Math.min(365, Math.max(1, n)) : HORIZON_DEFAULT_DAYS;
            horizonInput.value = String(clamped);
            try {
                localStorage.setItem(HORIZON_DAYS_PREF, String(clamped));
            }
            catch { /* */ }
            refresh();
        };
        horizonInput.addEventListener("change", commit);
        horizonInput.addEventListener("blur", commit);
    }
    // Subscribe to service events — when a background Google pull finishes
    // and upserted/reconciled rows, re-render without waiting for a nav
    // click. Uses the mailxapi.onEvent bus. Idempotent flag keeps multiple
    // initCalendarSidebar calls from dup-subscribing.
    if (!window.__mailxCalEventsWired) {
        const api = window.mailxapi;
        if (api?.onEvent) {
            window.__mailxCalEventsWired = true;
            api.onEvent((event) => {
                if (event?.type === "calendarUpdated")
                    refresh();
                else if (event?.type === "tasksUpdated")
                    renderTasks();
                else if (event?.type === "quotaError") {
                    // Surface a non-clickable banner — daily quota will reset
                    // on its own, no user action helps. Idempotent so the
                    // banner doesn't flash on repeat poll attempts.
                    const host = event.feature === "tasks"
                        ? document.getElementById("cal-side-tasks")
                        : document.getElementById("cal-side-body");
                    if (host && !host.querySelector(".cal-side-quota-error")) {
                        const msg = event.message || `Google ${event.feature} quota exceeded — try again later.`;
                        host.innerHTML = `<div class="cal-side-empty cal-side-quota-error">${escapeHtml(msg)}</div>`;
                    }
                }
                else if (event?.type === "authScopeError") {
                    // Surface a visible hint right in the affected pane so the
                    // user doesn't stare at an empty list wondering why. Only
                    // writes to the matching pane; other panes keep rendering.
                    //
                    // Idempotent: if the banner is already shown for this
                    // feature (class `.cal-side-auth-error` present), don't
                    // re-write the DOM — stops the "flashing on and off
                    // continually" effect when the service re-emits on every
                    // 5-min poll or sidebar-nav click.
                    const host = event.feature === "tasks"
                        ? document.getElementById("cal-side-tasks")
                        : document.getElementById("cal-side-body");
                    if (host && !host.querySelector(".cal-side-auth-error")) {
                        const msg = event.message || "Google access needs re-consent.";
                        host.innerHTML = `<div class="cal-side-empty cal-side-auth-error">
                            <div style="margin-bottom:0.6em">${escapeHtml(msg)}</div>
                            <button type="button" class="cal-side-reauth-btn" style="padding:0.3em 0.8em;border-radius:4px;border:1px solid currentColor;background:transparent;color:inherit;cursor:pointer;font-size:0.9em">Re-authenticate Now</button>
                        </div>`;
                        const btn = host.querySelector(".cal-side-reauth-btn");
                        btn?.addEventListener("click", async () => {
                            btn.disabled = true;
                            btn.textContent = "Opening browser…";
                            try {
                                await reauthGoogleScopes();
                                btn.textContent = "Consent opened — complete it in the browser";
                            }
                            catch (err) {
                                btn.disabled = false;
                                btn.textContent = `Failed: ${err?.message || err}. Click to retry.`;
                            }
                        });
                    }
                }
            });
        }
    }
}
//# sourceMappingURL=calendar-sidebar.js.map