/** View tabs — multiple views over one mailbox / one backend.
 *
 *  See docs/multi-view.md. A tab is a self-contained *view descriptor*: which
 *  folder / unified-inbox / search it shows. The expensive things (DB, the
 *  live sync stream, folder counts, contacts) stay shared in the daemon and
 *  the one IPC channel — a tab is just the cheap "which folder, restored how"
 *  bundle. That bundle is also exactly what a future tear-off window needs,
 *  so this module is the seam for Stage 3.
 *
 *  Stage 1: snapshot/restore. The DOM has ONE three-pane; switching tabs
 *  re-invokes the existing load functions for the target tab's view, and the
 *  list's own `positionMemory` restores selection + scroll per view. Inactive
 *  tabs hold only their descriptor — no detached DOM, no second list state.
 */

export type TabView =
    | { kind: "unified" }
    | { kind: "folder"; accountId: string; folderId: number; specialUse: string }
    | { kind: "search"; query: string; scope: string; accountId: string; folderId: number; includeTrash: boolean };

export interface ViewTab {
    id: string;
    title: string;
    view: TabView;
}

const STORAGE_KEY = "mailx-view-tabs";
let tabs: ViewTab[] = [];
let activeId = "";
let stripEl: HTMLElement | null = null;
/** Applies a tab's view to the single three-pane. Set by initTabs; defined in
 *  app.ts because it must touch app-level view state + the load functions. */
let applyView: (tab: ViewTab) => void = () => { /* */ };
let _nextId = 1;
function newId(): string { return `t${_nextId++}`; }

function persist(): void {
    try {
        sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ tabs, activeId }));
    } catch { /* sessionStorage unavailable — tabs just won't survive reload */ }
}

function render(): void {
    if (!stripEl) return;
    stripEl.innerHTML = "";
    for (const tab of tabs) {
        const chip = document.createElement("div");
        chip.className = "view-tab" + (tab.id === activeId ? " active" : "");
        chip.dataset.tabId = tab.id;
        const label = document.createElement("span");
        label.className = "view-tab-label";
        label.textContent = tab.title || "(view)";
        chip.appendChild(label);
        // Close affordance — hidden when only one tab remains (can't close
        // the last one; the window always shows something).
        if (tabs.length > 1) {
            const close = document.createElement("button");
            close.className = "view-tab-close";
            close.textContent = "×";
            close.title = "Close tab";
            close.addEventListener("click", (e) => { e.stopPropagation(); closeTab(tab.id); });
            chip.appendChild(close);
        }
        chip.addEventListener("click", () => activate(tab.id));
        stripEl.appendChild(chip);
    }
    const plus = document.createElement("button");
    plus.className = "view-tab-new";
    plus.textContent = "+";
    plus.title = "New tab (Ctrl+T)";
    plus.addEventListener("click", () => openTab({ kind: "unified" }, "All Inboxes", true));
    stripEl.appendChild(plus);
    // Strip stays visible even with a single tab so the "+" button is a
    // discoverable affordance for opening another (Bob 2026-05-18: with the
    // strip hidden there was no hint that Ctrl+T creates a tab). Hidden only
    // before the first tab exists — nothing to show yet.
    stripEl.hidden = tabs.length < 1;
}

/** Wire the strip. `apply` is the app.ts callback that loads a tab's view. */
export function initTabs(strip: HTMLElement, apply: (tab: ViewTab) => void): void {
    stripEl = strip;
    applyView = apply;
    try {
        const raw = sessionStorage.getItem(STORAGE_KEY);
        if (raw) {
            const parsed = JSON.parse(raw) as { tabs: ViewTab[]; activeId: string };
            if (Array.isArray(parsed?.tabs) && parsed.tabs.length) {
                // Search is EPHEMERAL — it must not survive a restart/reload
                // (Bob 2026-06-26). A persisted "search" view is rewritten back
                // to the folder it was scoped to (or All Inboxes), so a restart
                // lands on real mail, never a stale query.
                tabs = parsed.tabs.map(t => {
                    if (t.view?.kind !== "search") return t;
                    const v = t.view;
                    return v.folderId
                        ? { id: t.id, title: "Mail", view: { kind: "folder", accountId: v.accountId, folderId: v.folderId, specialUse: "" } as TabView }
                        : { id: t.id, title: "All Inboxes", view: { kind: "unified" } as TabView };
                });
                activeId = parsed.activeId || tabs[0].id;
                for (const t of tabs) {
                    const n = Number(String(t.id).replace(/^t/, ""));
                    if (Number.isFinite(n) && n >= _nextId) _nextId = n + 1;
                }
            }
        }
    } catch { /* corrupt — start fresh */ }
    render();
}

/** The currently-active tab, or null before any tab exists. */
export function activeTab(): ViewTab | null {
    return tabs.find(t => t.id === activeId) || null;
}

/** Open a new tab for `view` and (by default) switch to it. */
export function openTab(view: TabView, title: string, activateIt = true): void {
    const tab: ViewTab = { id: newId(), title, view };
    tabs.push(tab);
    if (activateIt) activeId = tab.id;
    persist();
    render();
    if (activateIt) applyView(tab);
}

/** Switch to an existing tab and load its view. */
export function activate(id: string): void {
    const tab = tabs.find(t => t.id === id);
    if (!tab || id === activeId) return;
    activeId = id;
    persist();
    render();
    applyView(tab);
}

/** Close a tab. Never closes the last one. If the active tab is closed, the
 *  neighbour to its left (or right) becomes active. */
export function closeTab(id: string): void {
    if (tabs.length < 2) return;
    const idx = tabs.findIndex(t => t.id === id);
    if (idx < 0) return;
    const wasActive = id === activeId;
    tabs.splice(idx, 1);
    if (wasActive) {
        const next = tabs[Math.max(0, idx - 1)];
        activeId = next.id;
        persist();
        render();
        applyView(next);
    } else {
        persist();
        render();
    }
}

/** Record the view the user just navigated to in the CURRENT tab — called
 *  from the folder-tree / search handlers. Does NOT re-apply the view (the
 *  navigation already loaded it); only keeps the active tab's descriptor +
 *  title in sync so a later tab-switch restores the right thing. If no tab
 *  exists yet (first navigation after boot), this creates the first tab —
 *  so the very first folder/inbox selection seeds the strip. */
export function setActiveView(view: TabView, title: string): void {
    let tab = activeTab();
    if (!tab) {
        tab = { id: newId(), title, view };
        tabs.push(tab);
        activeId = tab.id;
    } else {
        tab.view = view;
        tab.title = title;
    }
    persist();
    render();
}
