/* global React */
// =============================================================================
// useStream — React hook for consuming SSE endpoints from this app's backend.
//
// Wraps a fetch()-based SSE reader (NOT browser EventSource) so we can read
// arbitrary `event: <type>` named frames; EventSource only triggers onmessage
// for the default "message" type and forces per-type addEventListener calls.
//
// Backend contract (sse_hub.py / sse_routes.py):
//   - First frame on connect: `event: snapshot` with `{ type, ts, hub, data }`
//   - Live frames: `event: <type>` with `{ type, ts, ...fields }`
//   - Idle frames: `event: heartbeat` (silently swallowed; just keeps conn alive)
//   - Terminal failure: `event: error` and stream closes
//
// Usage:
//   const { snapshot, events, lastEvent, status, error } = useStream(
//     "/api/news/stream",
//     { onEvent: (ev) => { ... }, bufferSize: 200 }
//   );
//
// Returns:
//   snapshot   — the most recent snapshot payload (`data` field), or null
//   events     — append-only ring buffer of live events (capped at bufferSize)
//   lastEvent  — most recent event for quick reactive cases
//   status     — "idle" | "connecting" | "open" | "reconnecting" | "closed" | "error"
//   error      — human-readable string when status is reconnecting/error/closed
// =============================================================================

function useStream(url, opts) {
  opts = opts || {};
  const onEvent       = opts.onEvent;
  const bufferSize    = opts.bufferSize    != null ? opts.bufferSize    : 200;
  const autoReconnect = opts.autoReconnect != null ? opts.autoReconnect : true;
  const enabled       = opts.enabled       != null ? opts.enabled       : true;

  const [status,    setStatus]    = React.useState("idle");
  const [snapshot,  setSnapshot]  = React.useState(null);
  const [events,    setEvents]    = React.useState([]);
  const [lastEvent, setLastEvent] = React.useState(null);
  const [error,     setError]     = React.useState(null);

  // Refs so the effect doesn't re-trigger on every render.
  const onEventRef = React.useRef(onEvent);
  React.useEffect(() => { onEventRef.current = onEvent; }, [onEvent]);

  React.useEffect(() => {
    if (!enabled || !url) return;

    let cancelled = false;
    let abortCtrl = null;
    let retryTimer = null;
    let backoff = 0;

    const open = async () => {
      if (cancelled) return;
      setStatus(prev => (prev === "idle" || prev === "connecting") ? "connecting" : "reconnecting");

      abortCtrl = new AbortController();
      try {
        const res = await fetch(url, {
          headers: { Accept: "text/event-stream" },
          signal: abortCtrl.signal,
        });
        if (!res.ok || !res.body) {
          throw new Error("HTTP " + res.status);
        }
        backoff = 0;
        setStatus("open");
        setError(null);

        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";

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

          let idx;
          while ((idx = buffer.indexOf("\n\n")) >= 0) {
            const frame = buffer.slice(0, idx);
            buffer = buffer.slice(idx + 2);
            if (!frame) continue;

            let evType = "message";
            let evData = "";
            for (const line of frame.split("\n")) {
              if (line.startsWith("event:"))      evType = line.slice(6).trim();
              else if (line.startsWith("data:"))  evData += line.slice(5).trim();
            }
            if (!evData) continue;

            let payload;
            try { payload = JSON.parse(evData); }
            catch (_) { continue; }

            if (evType === "snapshot") {
              setSnapshot(payload.data != null ? payload.data : payload);
              continue;
            }
            if (evType === "heartbeat") {
              // No state mutation — heartbeat is just a transport keep-alive.
              continue;
            }

            const ev = Object.assign({ type: evType }, payload);
            setLastEvent(ev);
            setEvents(prev => {
              const next = prev.length >= bufferSize
                ? prev.slice(prev.length - bufferSize + 1).concat(ev)
                : prev.concat(ev);
              return next;
            });
            const cb = onEventRef.current;
            if (cb) {
              try { cb(ev); }
              catch (cbErr) { console.error("useStream onEvent error", cbErr); }
            }
          }
        }

        // Server closed the stream cleanly.
        if (!cancelled) {
          if (autoReconnect) {
            scheduleReconnect("server closed stream");
          } else {
            setStatus("closed");
          }
        }
      } catch (err) {
        if (cancelled || (err && err.name === "AbortError")) return;
        if (!autoReconnect) {
          setStatus("error");
          setError(String(err && err.message || err));
          return;
        }
        scheduleReconnect(err && err.message || String(err));
      }
    };

    const scheduleReconnect = (reason) => {
      const wait = Math.min(30000, 500 * Math.pow(2, backoff));
      backoff += 1;
      setStatus("reconnecting");
      setError(reason + " — retry in " + Math.round(wait / 1000) + "s (attempt " + backoff + ")");
      retryTimer = setTimeout(open, wait);
    };

    open();

    return () => {
      cancelled = true;
      if (abortCtrl)  { try { abortCtrl.abort(); } catch (_) {} }
      if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
    };
  }, [url, bufferSize, autoReconnect, enabled]);

  return { snapshot, events, lastEvent, status, error };
}

// Make available globally (no module system in this SPA — every .jsx file is
// loaded as a top-level script via <script type="text/babel"> in index.html).
window.useStream = useStream;
