/**
 * StoreBus — typed pub/sub for Store events.
 *
 * Postmessage-style: subscribers register a topic string + handler; publishers
 * emit a {topic, kind, ...} object. The exact same shape works inside Node,
 * across worker_threads (postMessage), and across the WebView⇄host boundary
 * (also postMessage). That's the load-bearing property: one mental model from
 * DB write to UI re-render, on any platform.
 *
 * Topics
 * ──────
 *   `message:<uuid>`   — a specific message changed (flags, body, removal)
 *   `folder:<id>`      — anything inside folder <id> changed; the bus fans
 *                        out per-message events to the parent folder topic
 *                        automatically (see fanOutToFolder())
 *   `account:<id>`     — account-scoped event (sync status, auth, quota)
 *   `*`                — wildcard; receives every event. Use for the IPC
 *                        forwarder that pushes to the WebView, and for
 *                        diagnostics. Avoid in normal UI subscribers.
 *
 * Batching
 * ────────
 * Bulk writes (sync round inserts 200 envelopes) would emit 200 events. The
 * `withBatch(fn)` wrapper buffers events fired during fn() and coalesces
 * per-topic into one {kind:"batch", changedUuids, summary} event at the end.
 * Subscribers either iterate `changedUuids` or treat batch as "rerender this
 * topic." Nested withBatch() is supported; inner scopes just append to the
 * outer's buffer.
 */

export type StoreEventKind =
    | "messageInserted"
    | "messageUpdated"
    | "messageRemoved"
    | "messageMoved"
    | "flagsChanged"
    | "bodyAvailable"
    | "bodyFetchError"
    | "folderCountsChanged"
    | "draftSaved"
    | "draftSaveDeferred"
    | "batch";

export interface StoreEvent {
    topic: string;
    kind: StoreEventKind;
    accountId?: string;
    folderId?: number;
    targetFolderId?: number;
    uid?: number;
    msgUuid?: string;
    flags?: string[];
    error?: string;
    /** Set on `batch` events. UUIDs of every message touched in the scope. */
    changedUuids?: string[];
    /** Set on `batch` events. */
    summary?: { inserted: number; updated: number; deleted: number };
    /** Arbitrary additional payload for kind-specific data the typed fields
     *  above don't cover (e.g. flag-set details, body path). Keep small. */
    [extra: string]: unknown;
}

export type StoreEventHandler = (event: StoreEvent) => void;

export class StoreBus {
    private subscribers = new Map<string, Set<StoreEventHandler>>();
    private batchDepth = 0;
    private buffer: StoreEvent[] = [];

    subscribe(topic: string, handler: StoreEventHandler): () => void {
        let set = this.subscribers.get(topic);
        if (!set) {
            set = new Set();
            this.subscribers.set(topic, set);
        }
        set.add(handler);
        return () => {
            const s = this.subscribers.get(topic);
            if (s) {
                s.delete(handler);
                if (s.size === 0) this.subscribers.delete(topic);
            }
        };
    }

    /** Publish an event. If a folderId is set and the topic is a message
     *  topic, the bus also publishes a copy to the parent folder topic so
     *  list views (subscribed to folder:<id>) wake without subscribing to
     *  every message individually. */
    publish(event: StoreEvent): void {
        if (this.batchDepth > 0) {
            this.buffer.push(event);
            // Folder fan-out also buffers; coalesces at flush time.
            const fanned = this.fanOutToFolder(event);
            if (fanned) this.buffer.push(fanned);
            return;
        }
        this.deliver(event);
        const fanned = this.fanOutToFolder(event);
        if (fanned) this.deliver(fanned);
    }

    /** Run `fn` with publishes buffered. On exit, coalesces per-topic and
     *  delivers either the single event (when only one fired for that topic)
     *  or a synthetic batch event summarizing the changes. */
    withBatch<T>(fn: () => T): T {
        this.batchDepth++;
        try {
            return fn();
        } finally {
            this.batchDepth--;
            if (this.batchDepth === 0) this.flush();
        }
    }

    private flush(): void {
        const buf = this.buffer;
        this.buffer = [];
        if (buf.length === 0) return;
        const byTopic = new Map<string, StoreEvent[]>();
        for (const e of buf) {
            let arr = byTopic.get(e.topic);
            if (!arr) { arr = []; byTopic.set(e.topic, arr); }
            arr.push(e);
        }
        for (const [topic, events] of byTopic) {
            if (events.length === 1) { this.deliver(events[0]); continue; }
            const summary = { inserted: 0, updated: 0, deleted: 0 };
            const uuids = new Set<string>();
            for (const e of events) {
                if (e.kind === "messageInserted") summary.inserted++;
                else if (e.kind === "messageRemoved") summary.deleted++;
                else summary.updated++;
                if (e.msgUuid) uuids.add(e.msgUuid);
            }
            this.deliver({ topic, kind: "batch", changedUuids: [...uuids], summary });
        }
    }

    private deliver(event: StoreEvent): void {
        const exact = this.subscribers.get(event.topic);
        if (exact) for (const h of exact) {
            try { h(event); } catch (e) { console.error("[store-bus]", e); }
        }
        const wild = this.subscribers.get("*");
        if (wild) for (const h of wild) {
            try { h(event); } catch (e) { console.error("[store-bus]", e); }
        }
    }

    /** When a message-topic event fires and carries a folderId, generate
     *  the parallel folder-topic event so folder subscribers don't have to
     *  also subscribe to every message individually. Returns null when no
     *  fan-out applies. */
    private fanOutToFolder(event: StoreEvent): StoreEvent | null {
        if (event.kind === "batch") return null;
        if (!event.topic.startsWith("message:")) return null;
        if (event.folderId == null) return null;
        return { ...event, topic: `folder:${event.folderId}` };
    }
}

/** Singleton bus shared by every Store consumer in the process. Workers
 *  get their own; the worker boundary serializes events via postMessage. */
export const storeBus = new StoreBus();
