import { apiFetch } from "./api";

export interface SseEvent
{
    type: string;
    data: unknown;
}

export type SseHandler = (event: SseEvent) => void;

// connectSSE establishes a long-lived /api/hub/events stream and pumps every
// parsed frame to onEvent.  Resolves only when the body ends or signal aborts.
//
// Why not EventSource: native EventSource can't attach Authorization or
// X-Device-Name headers, both of which are mandatory (PROJECT_BRIEF.md §2 / §5).
// We use fetch + ReadableStream + TextDecoder({stream:true}) + '\n\n' framing,
// matching FRONTEND_DESIGN.md §8.
export async function connectSSE(
    url: string,
    onEvent: SseHandler,
    signal?: AbortSignal,
    onOpen?: () => void,
): Promise<void>
{
    const resp = await apiFetch(url, {
        method: "GET",
        cache: "no-store",
        signal,
    });

    if (!resp.ok)
    {
        throw new Error(`SSE ${resp.status}: ${resp.statusText}`);
    }
    if (!resp.body)
    {
        throw new Error("SSE: response has no body");
    }

    // The stream is established (status + body ready). Signal "connected" now,
    // before any frame: a freshly-connected client may not receive its first
    // event for a while (it only gets `presence` when the device set changes),
    // and on Windows the first frame can sit in a downstream buffer. The
    // connection itself is what the indicator should reflect.
    onOpen?.();

    const reader = resp.body.getReader();
    const decoder = new TextDecoder("utf-8");
    let buf = "";

    try
    {
        while (true)
        {
            const { value, done } = await reader.read();
            if (done) { break; }
            buf += decoder.decode(value, { stream: true });

            let sep = buf.indexOf("\n\n");
            while (sep !== -1)
            {
                const raw = buf.substring(0, sep);
                buf = buf.substring(sep + 2);
                const ev = parseFrame(raw);
                if (ev) { onEvent(ev); }
                sep = buf.indexOf("\n\n");
            }
        }
    }
    finally
    {
        try { reader.cancel(); } catch { /* fine */ }
    }
}

// parseFrame interprets one '\n\n'-terminated SSE block.
// Lines starting with ':' are comments (server's `: hi` probe lands here).
// Multiple `data:` lines are concatenated with '\n' per the SSE spec.
// JSON `data` is auto-decoded; anything else is passed through as a string.
function parseFrame(raw: string): SseEvent | null
{
    let event = "message";
    let data = "";

    for (const line of raw.split("\n"))
    {
        if (line.length === 0 || line.startsWith(":")) { continue; }
        const colon = line.indexOf(":");
        if (colon === -1) { continue; }

        const field = line.substring(0, colon);
        const value = line.substring(colon + 1).trimStart();

        if (field === "event") { event = value; }
        else if (field === "data")
        {
            data = data.length === 0 ? value : `${data}\n${value}`;
        }
    }

    if (event === "message" && data.length === 0)
    {
        return null;
    }

    let parsed: unknown = data;
    if (data.length > 0 && (data[0] === "{" || data[0] === "["))
    {
        try { parsed = JSON.parse(data); }
        catch { /* leave as string */ }
    }
    return { type: event, data: parsed };
}
