/**
 * No-op {@link ActivityBus}. The default when no bus is injected (Node, tests, headless
 * server use) — publishing goes nowhere and nothing is left open to tear down.
 *
 * @beta
 */
export const NOOP_ACTIVITY_BUS = { publish: () => { } };
/**
 * Typed in-process event bus for AI assistant activity events.
 *
 * Dispatches events synchronously to all local subscribers in the same JS module instance
 * (i.e. the same browser tab). Events listed in `crossTabEvents` are additionally forwarded
 * via a shared `BroadcastChannel` so that other tabs on the same origin receive them.
 *
 * @remarks
 * The pre-instantiated {@link agenticActivityBus} singleton covers all platform use cases.
 * App developers can extend the event map using TypeScript declaration merging on
 * {@link AgenticActivityEvents} and subscribe/publish custom events without any platform changes.
 *
 * @beta
 */
export class AgenticActivityBus {
    /**
     * @param config - Optional configuration. Omit to create an in-memory-only bus.
     * @beta
     */
    constructor(config) {
        var _a;
        this.subscribers = new Map();
        this.channelInitialised = false;
        this.crossTabEvents = new Set((_a = config === null || config === void 0 ? void 0 : config.crossTabEvents) !== null && _a !== void 0 ? _a : []);
        this.channelNameFactory =
            (config === null || config === void 0 ? void 0 : config.channelName) != null ? () => config.channelName : config === null || config === void 0 ? void 0 : config.channelNameFactory;
    }
    /**
     * Lazily open the cross-tab `BroadcastChannel` on first use. Deferred (rather than
     * created in the constructor) so that importing this module, or constructing the bus
     * off-browser (Node, tests, headless server), never opens a live channel or reads
     * `sessionStorage` — both would keep a Node event loop alive and hang the process.
     * Cross-tab forwarding is inherently browser-only, so channel creation is gated on a
     * browser environment; elsewhere the bus stays purely in-process.
     */
    ensureChannel() {
        if (this.channelInitialised)
            return;
        this.channelInitialised = true;
        const inBrowser = typeof window !== 'undefined' && typeof BroadcastChannel !== 'undefined';
        if (this.crossTabEvents.size === 0 || !this.channelNameFactory || !inBrowser)
            return;
        this.channel = new BroadcastChannel(this.channelNameFactory());
        this.channel.onmessage = (ev) => {
            var _a;
            const { type, detail } = ((_a = ev.data) !== null && _a !== void 0 ? _a : {});
            if (type)
                this.notifyLocal(type, detail);
        };
    }
    /**
     * Subscribe to an event. Returns an unsubscribe function — call it in
     * `disconnectedCallback` to avoid memory leaks.
     *
     * @beta
     */
    subscribe(type, handler) {
        // Open the cross-tab channel (browser only) so this subscriber receives events
        // forwarded from other tabs; no-op off-browser.
        this.ensureChannel();
        let handlers = this.subscribers.get(type);
        if (!handlers) {
            handlers = new Set();
            this.subscribers.set(type, handlers);
        }
        const h = handler;
        handlers.add(h);
        return () => handlers.delete(h);
    }
    /**
     * Publish an event to all local subscribers and, if configured, to other tabs
     * via BroadcastChannel.
     *
     * @beta
     */
    publish(type, detail) {
        var _a;
        this.notifyLocal(type, detail);
        if (this.crossTabEvents.has(type)) {
            this.ensureChannel();
            (_a = this.channel) === null || _a === void 0 ? void 0 : _a.postMessage({ type, detail });
        }
    }
    /**
     * Add an event type to the cross-tab forwarding set at runtime.
     * Must be called before the first {@link AgenticActivityBus.publish} of that event type.
     *
     * @beta
     */
    addCrossTabEvent(type) {
        this.crossTabEvents.add(type);
    }
    /**
     * Close the underlying BroadcastChannel. Call once on application teardown if needed.
     *
     * @beta
     */
    close() {
        var _a;
        (_a = this.channel) === null || _a === void 0 ? void 0 : _a.close();
        this.channel = undefined;
    }
    notifyLocal(type, detail) {
        var _a;
        (_a = this.subscribers.get(type)) === null || _a === void 0 ? void 0 : _a.forEach((handler) => handler(detail));
    }
}
/**
 * Returns a `BroadcastChannel` name that is scoped to the current tab and any popout windows
 * it opens, while remaining isolated from unrelated tabs on the same origin.
 *
 * **How it works:**
 * A UUID is generated once and stored in `sessionStorage`. `sessionStorage` persists across
 * page refreshes (so the channel name stays stable after an F5), but is cleared when the tab
 * is closed. Crucially, when a page opens a child window via `window.open()`, the browser
 * initialises the new window's `sessionStorage` as a copy of the opener's — so the child
 * inherits the same UUID and therefore the same channel name.
 *
 * Result:
 * - **Same tab + its `window.open()` popouts** share one channel → events flow as intended.
 * - **Independent tabs** (opened from the address bar, bookmarks, or links) each get a fresh
 *   `sessionStorage` and therefore a distinct channel → no cross-talk between unrelated app
 *   instances on the same origin.
 */
function getTabScopedChannelName(base) {
    // Belt-and-braces: this only runs in a browser (see `ensureChannel`), but guard anyway
    // so a direct call off-browser never throws a `sessionStorage is not defined` ReferenceError.
    if (typeof sessionStorage === 'undefined')
        return `${base}-nostorage`;
    const key = `${base}-tab-id`;
    let id = sessionStorage.getItem(key);
    if (!id) {
        id = crypto.randomUUID();
        sessionStorage.setItem(key, id);
    }
    return `${base}-${id}`;
}
/**
 * Pre-instantiated singleton event bus for AI assistant activity.
 *
 * - `halo-start` / `halo-stop` are forwarded via a tab-scoped `BroadcastChannel` so that
 *   popout windows opened from this tab show halo animations, while remaining isolated from
 *   unrelated tabs on the same origin (tab-scoped channel name).
 * - `tool-loop-start` / `tool-loop-end` are cross-tab for the same reason: they describe the
 *   assistant's lifecycle (a turn beginning / ending), and the most common subscribers
 *   (workspace transitions, post-turn deferred recomputation, UI gates tied to the assistant
 *   being idle) need the event regardless of whether the user is interacting with the chat
 *   in the main tab or a popout window. Tab-local-only telemetry can subscribe and filter.
 * - `chat-popout` / `chat-popin` are in-memory only — they must only affect the tab where the
 *   user clicked.
 *
 * @beta
 */
export const agenticActivityBus = new AgenticActivityBus({
    crossTabEvents: ['halo-start', 'halo-stop', 'tool-loop-start', 'tool-loop-end'],
    // Lazy: the tab-scoped name (and its `sessionStorage` read) is computed on first
    // cross-tab use in a browser, not at module-eval — so importing this module is
    // side-effect-free and Node/test-safe.
    channelNameFactory: () => getTabScopedChannelName('foundation-ai-activity'),
});
