/**
 * Telemetry beacon interop (Phase 16 §2.2). A thin wrapper over navigator.sendBeacon for fire-and-forget
 * play/share events, plus a page-unload handler that lets the player close an open play session as the
 * tab goes away. sendBeacon (not fetch) is the load-bearing choice: it survives page unload, where a
 * fetch would be cancelled — exactly the tab-close edge case the play metric must still record.
 *
 * Exposed on window.DeepDrftBeacon; imported once in App.razor alongside the audio engine.
 */

// .NET interop type — a DotNetObjectReference the unload handler invokes back into.
interface DotNetObjectReference {
    invokeMethodAsync(methodName: string, ...args: unknown[]): Promise<unknown>;
    invokeMethod(methodName: string, ...args: unknown[]): unknown;
}

// Registered unload listeners. Holding the handler lets us detach on dispose so a torn-down player
// circuit does not get called into.
type UnloadEntry = { dotNetRef: DotNetObjectReference; methodName: string };
const unloadHandlers = new Map<string, UnloadEntry>();

let unloadWired = false;

// Fire every registered unload handler synchronously. invokeMethod (sync) — not invokeMethodAsync — is
// required here: in pagehide/visibilitychange→hidden the event loop will not pump a microtask before the
// page is frozen, so an awaited call would never run. The .NET side does only synchronous beacon work.
function fireUnloadHandlers(): void {
    for (const { dotNetRef, methodName } of unloadHandlers.values()) {
        try {
            dotNetRef.invokeMethod(methodName);
        } catch {
            // A torn-down circuit or a transient interop failure must never block unload.
        }
    }
}

function wireUnloadOnce(): void {
    if (unloadWired) return;
    unloadWired = true;

    // pagehide is the canonical "page is going away" signal (covers tab close, navigation, and the
    // bfcache freeze). visibilitychange→hidden additionally covers the mobile case where the tab is
    // backgrounded and may be discarded without a pagehide. Both funnel to the same close path; the
    // .NET side is idempotent, so a double-fire closes the session at most once.
    window.addEventListener('pagehide', fireUnloadHandlers);
    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') fireUnloadHandlers();
    });
}

const DeepDrftBeacon = {
    /**
     * Queue a fire-and-forget POST of a small JSON body. Returns false if the browser refused to queue
     * the beacon (e.g. over the per-origin byte budget) — callers ignore it; a dropped telemetry event
     * is acceptable by design.
     */
    send: (url: string, json: string): boolean => {
        try {
            const blob = new Blob([json], { type: 'application/json' });
            return navigator.sendBeacon(url, blob);
        } catch {
            return false;
        }
    },

    /**
     * Register a .NET callback to run on page unload (and on visibility→hidden). Keyed so a given player
     * registers once and can replace/detach cleanly across its lifecycle.
     */
    registerUnload: (key: string, dotNetRef: DotNetObjectReference, methodName: string): void => {
        wireUnloadOnce();
        unloadHandlers.set(key, { dotNetRef, methodName });
    },

    /** Detach a previously-registered unload callback (player dispose). */
    unregisterUnload: (key: string): void => {
        unloadHandlers.delete(key);
    },
};

declare global {
    interface Window {
        DeepDrftBeacon: typeof DeepDrftBeacon;
    }
}

window.DeepDrftBeacon = DeepDrftBeacon;

export { DeepDrftBeacon };
