/**
 * Shared message store — the messages currently rendered in the list.
 *
 * Selection state used to live here too, with a broadcast-subscribe model
 * that let the viewer subscribe to "selected changed" events independently
 * of the list. That made drift between the highlighted row and the preview
 * pane possible: two subscribers, two render paths, two opportunities for
 * a missed update or stale read. Selection is now owned by message-list
 * (the only thing that has DOM rows in the first place); the viewer is a
 * passive `show(msg)` / `clear()` API the list calls directly. With one
 * code path, drift is structurally impossible.
 *
 * What remains here: the array of currently-loaded messages, plus the
 * removeMessages helper that filters the array and lets the caller decide
 * which row to focus next (if any) — that policy is the list's, not the
 * store's.
 */

export interface ListMessage {
    accountId: string;
    uid: number;
    folderId: number;
    subject: string;
    from: { name: string; address: string };
    to: { name: string; address: string }[];
    cc: { name: string; address: string }[];
    date: number;
    flags: string[];
    size: number;
    preview: string;
    hasAttachments: boolean;
    [key: string]: any;
}

let messages: ListMessage[] = [];

/** Subscribers fire when the messages array is replaced or filtered.
 *  Selection state is NOT broadcast — that lives in message-list and
 *  reaches the viewer through a single direct call, never through this
 *  fan-out. Subscribers here are observers only (status bar, thread
 *  filter, etc.); they don't drive the preview pane. */
type MessagesListener = () => void;
const listeners: MessagesListener[] = [];

export function subscribe(fn: MessagesListener): () => void {
    listeners.push(fn);
    return () => {
        const i = listeners.indexOf(fn);
        if (i >= 0) listeners.splice(i, 1);
    };
}
function notify(): void {
    for (const fn of listeners) {
        try { fn(); } catch { /* don't let one subscriber break others */ }
    }
}

/** Replace the entire message list (folder load, search, unified inbox). */
export function setMessages(msgs: ListMessage[]): void {
    messages = msgs;
    notify();
}

export function getMessages(): ListMessage[] {
    return messages;
}

/** Result of a removeMessages call: the list-controller uses this to
 *  decide what to focus after a delete/move. */
export interface RemovalOutcome {
    /** True if any of the removed messages was previously focused — caller
     *  must transition the focus (to nextSurvivor or clear the viewer). */
    focusedWasRemoved: boolean;
    /** Identity of the message that should become the new focus, or null
     *  when the list is now empty / no survivor exists. The list looks up
     *  its row by this identity and calls its own focus-row method. */
    nextSurvivor: { accountId: string; uid: number; folderId: number } | null;
}

/**
 * Remove messages from the list (after move or delete).
 *
 * Returns a RemovalOutcome the caller uses to drive focus handoff. We
 * pre-capture the next-survivor *by identity* (not by index) before the
 * filter mutates the array, so subsequent index shifts don't matter — if
 * every deleted row had a survivor below it, the chosen survivor is the
 * same regardless of how many got deleted. Falls back to walking
 * backward when the deletion is at the bottom.
 */
export function removeMessages(
    uids: { accountId: string; uid: number; folderId?: number }[],
    currentlyFocused: { accountId: string; uid: number; folderId?: number } | null,
): RemovalOutcome {
    // Key on (account, FOLDER, uid) when a folderId is supplied. IMAP UIDs are
    // per-folder, so in a mixed view (e.g. a search spanning INBOX + Trash) a
    // Trash row can share a uid with a SURVIVING INBOX message — keying on
    // (account, uid) alone then kept the deleted Trash row visible (Bob
    // 2026-06-12: "the trash ones did not disappear"). Legacy callers that
    // don't pass folderId fall back to the loose (account, uid) match — those
    // are single-folder contexts with no collision risk.
    const fullSet = new Set<string>();
    const looseSet = new Set<string>();
    for (const u of uids) {
        if (u.folderId != null) fullSet.add(`${u.accountId}:${u.folderId}:${u.uid}`);
        else looseSet.add(`${u.accountId}:${u.uid}`);
    }
    const isRemoved = (m: { accountId: string; uid: number; folderId?: number }): boolean =>
        (m.folderId != null && fullSet.has(`${m.accountId}:${m.folderId}:${m.uid}`)) ||
        looseSet.has(`${m.accountId}:${m.uid}`);
    const focusedWasRemoved = currentlyFocused !== null && isRemoved(currentlyFocused);

    let nextSurvivor: ListMessage | null = null;
    if (focusedWasRemoved) {
        let lastRemovedIdx = -1;
        for (let i = 0; i < messages.length; i++) {
            if (isRemoved(messages[i])) lastRemovedIdx = i;
        }
        for (let i = lastRemovedIdx + 1; i < messages.length; i++) {
            if (!isRemoved(messages[i])) {
                nextSurvivor = messages[i];
                break;
            }
        }
        if (!nextSurvivor) {
            for (let i = lastRemovedIdx - 1; i >= 0; i--) {
                if (!isRemoved(messages[i])) {
                    nextSurvivor = messages[i];
                    break;
                }
            }
        }
    }

    // dupeCount fix-up: if we removed one half of a unified-inbox duplicate
    // pair, the surviving row's ⇆ marker should drop now rather than wait
    // for the next server fetch. Decrement once per remaining row whose
    // messageId matches a removed one.
    const removedIds = new Set<string>();
    for (const m of messages) {
        if (isRemoved(m) && m.messageId) {
            removedIds.add(m.messageId);
        }
    }
    messages = messages.filter(m => !isRemoved(m));
    if (removedIds.size > 0) {
        for (const m of messages) {
            if (m.messageId && removedIds.has(m.messageId) && typeof (m as any).dupeCount === "number") {
                (m as any).dupeCount = Math.max(0, (m as any).dupeCount - 1);
            }
        }
    }
    notify();

    return {
        focusedWasRemoved,
        nextSurvivor: nextSurvivor
            ? { accountId: nextSurvivor.accountId, uid: nextSurvivor.uid, folderId: nextSurvivor.folderId }
            : null,
    };
}

/** Update flags on a message in the list. List re-renders the row's
 *  flag/unread classes inline; no broadcast. */
export function updateMessageFlags(accountId: string, uid: number, flags: string[]): void {
    const msg = messages.find(m => m.uid === uid && m.accountId === accountId);
    if (msg) msg.flags = flags;
}

import { subscribeStore } from "./api-client.js";

/** Subscribe to Store-bus flag changes so any source — UI click, server-side
 *  sync, another window — updates this in-memory list. The IPC click→render
 *  path was already optimistic (caller patches `msg.flags` before awaiting),
 *  so this listener is the redundancy that catches the cases where the local
 *  patch and the canonical Store row diverge (server pushed a different flag
 *  set during the round trip, sync rebound the row, etc.). */
subscribeStore("*", (event) => {
    if (event.kind !== "flagsChanged") return;
    const accountId = event.accountId as string | undefined;
    const uid = event.uid as number | undefined;
    const flags = event.flags as string[] | undefined;
    if (!accountId || typeof uid !== "number" || !Array.isArray(flags)) return;
    const msg = messages.find(m => m.uid === uid && m.accountId === accountId);
    if (!msg) return;
    const before = JSON.stringify(msg.flags);
    msg.flags = flags;
    if (before !== JSON.stringify(flags)) notify();
});
