// SSE consumer using fetch + ReadableStream (supports POST)
import { useCallback, useRef, useState, useMemo } from "react";

export interface SSEEvent<T = unknown> {
  event: string;
  data: T;
}

// 0702 T-042: When the server returns 401 with the structured invalid_api_key
// body, dispatch a studio:api-key-error event so the app-level
// useApiKeyErrorToast hook can surface a toast pointing at SettingsModal.
// The error-message branch still fires; this is an additive signal.
function maybeDispatchApiKeyError(status: number, body: unknown): void {
  if (status !== 401 || typeof window === "undefined") return;
  if (!body || typeof body !== "object") return;
  const b = body as { error?: unknown; provider?: unknown };
  if (b.error !== "invalid_api_key") return;
  if (b.provider !== "anthropic" && b.provider !== "openai" && b.provider !== "openrouter") return;
  window.dispatchEvent(
    new CustomEvent("studio:api-key-error", { detail: { provider: b.provider } }),
  );
}

/**
 * Safe JSON serializer that handles circular references and non-serializable
 * objects (DOM nodes, React fiber refs, class instances) without throwing.
 */
export function safeStringify(body: unknown): string | undefined {
  if (body === undefined) return undefined;
  try {
    return JSON.stringify(body);
  } catch (err) {
    console.error("Failed to serialize request body:", err);
    // Attempt fallback with circular ref replacement
    try {
      const seen = new WeakSet();
      return JSON.stringify(body, (_key, value) => {
        if (typeof value === "object" && value !== null) {
          if (seen.has(value)) return "[Circular]";
          seen.add(value);
        }
        return value;
      });
    } catch {
      return JSON.stringify({ error: "Request body could not be serialized" });
    }
  }
}

export function useSSE<T = unknown>() {
  const [events, setEvents] = useState<SSEEvent<T>[]>([]);
  const [running, setRunning] = useState(false);
  const [done, setDone] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const abortRef = useRef<AbortController | null>(null);

  const start = useCallback(async (url: string, body?: unknown) => {
    setEvents([]);
    setRunning(true);
    setDone(false);
    setError(null);

    const controller = new AbortController();
    abortRef.current = controller;

    try {
      const serializedBody = safeStringify(body);
      const res = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: serializedBody,
        signal: controller.signal,
      });

      if (!res.ok || !res.body) {
        let msg = `HTTP ${res.status}`;
        let parsed: unknown = undefined;
        try {
          parsed = await res.json();
          if (parsed && typeof parsed === "object" && "error" in parsed && typeof (parsed as { error: unknown }).error === "string") {
            msg = (parsed as { error: string }).error;
          }
        } catch {}
        maybeDispatchApiKeyError(res.status, parsed);
        throw new Error(msg);
      }

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

      while (true) {
        const { done: readerDone, value } = await reader.read();
        if (readerDone) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop() || "";

        for (const line of lines) {
          if (line.startsWith("event: ")) {
            currentEvent = line.slice(7).trim();
          } else if (line.startsWith("data: ")) {
            try {
              const data = JSON.parse(line.slice(6));
              const evt: SSEEvent<T> = { event: currentEvent, data };
              if (currentEvent === "done") {
                setDone(true);
                setEvents((prev) => [...prev, evt]);
              } else {
                setEvents((prev) => [...prev, evt]);
              }
            } catch {
              // skip malformed data
            }
            currentEvent = "";
          }
        }
      }
    } catch (err) {
      if ((err as Error).name !== "AbortError") {
        setError((err as Error).message);
      }
    } finally {
      setRunning(false);
      abortRef.current = null;
    }
  }, []);

  const stop = useCallback(() => {
    abortRef.current?.abort();
  }, []);

  return { events, running, done, error, start, stop };
}

// ---------------------------------------------------------------------------
// Multi-stream SSE hook — callback-based, zero accumulation
//
// Events are NOT stored in React state. Instead, the caller provides
// callbacks (onEvent, onDone, onError) that fire exactly once per SSE event.
// Only the running-state Set lives in React state (for re-render triggers).
// ---------------------------------------------------------------------------

export interface MultiSSECallbacks {
  onEvent: (caseId: number, evt: SSEEvent) => void;
  onDone: (caseId: number) => void;
  onError: (caseId: number, error: string) => void;
}

export function useMultiSSE(callbacks: MultiSSECallbacks) {
  // Stable ref for callbacks — avoids recreating streams when callbacks change
  const cbRef = useRef(callbacks);
  cbRef.current = callbacks;

  const [runningSet, setRunningSet] = useState<Set<number>>(new Set());
  const abortRefs = useRef<Map<number, AbortController>>(new Map());

  const startCase = useCallback(async (caseId: number, url: string, body?: unknown) => {
    // Abort existing stream for this case if any
    abortRefs.current.get(caseId)?.abort();

    const controller = new AbortController();
    abortRefs.current.set(caseId, controller);

    setRunningSet((prev) => {
      const next = new Set(prev);
      next.add(caseId);
      return next;
    });

    try {
      const serializedBody = safeStringify(body);
      const res = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: serializedBody,
        signal: controller.signal,
      });

      if (!res.ok || !res.body) {
        let msg = `HTTP ${res.status}`;
        let parsed: unknown = undefined;
        try {
          parsed = await res.json();
          if (parsed && typeof parsed === "object" && "error" in parsed && typeof (parsed as { error: unknown }).error === "string") {
            msg = (parsed as { error: string }).error;
          }
        } catch {}
        maybeDispatchApiKeyError(res.status, parsed);
        throw new Error(msg);
      }

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

      while (true) {
        const { done: readerDone, value } = await reader.read();
        if (readerDone) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop() || "";

        for (const line of lines) {
          if (line.startsWith("event: ")) {
            currentEvent = line.slice(7).trim();
          } else if (line.startsWith("data: ")) {
            try {
              const data = JSON.parse(line.slice(6));
              const evt: SSEEvent = { event: currentEvent, data };
              cbRef.current.onEvent(caseId, evt);
            } catch {
              // skip malformed data
            }
            currentEvent = "";
          }
        }
      }

      // Stream ended cleanly — notify done
      cbRef.current.onDone(caseId);
    } catch (err) {
      if ((err as Error).name !== "AbortError") {
        cbRef.current.onError(caseId, (err as Error).message);
      }
    } finally {
      setRunningSet((prev) => {
        const next = new Set(prev);
        next.delete(caseId);
        return next;
      });
      abortRefs.current.delete(caseId);
    }
  }, []);

  const stopCase = useCallback((caseId: number) => {
    abortRefs.current.get(caseId)?.abort();
  }, []);

  const stopAll = useCallback(() => {
    for (const controller of abortRefs.current.values()) {
      controller.abort();
    }
  }, []);

  const isAnyCaseRunning = runningSet.size > 0;

  return { runningSet, startCase, stopCase, stopAll, isAnyCaseRunning };
}
