/**
 * useSSE — React hook wrapper around `SSEManager`.
 * @countermeasure-platform/web-components/services/sse-react
 *
 * Wires `SSEManager` to component lifecycle: connects on mount, disconnects
 * on unmount, and re-attaches when `url` or `withCredentials` change.
 * `events` is read via a ref so updating handler identities does NOT cause
 * the underlying EventSource to be torn down.
 *
 * Usage:
 *   const { connected, lastError } = useSSE({
 *     url: `/api/runs/${id}/stream`,
 *     withCredentials: true,
 *     events: {
 *       log:    (data) => append(data.line),
 *       phase:  (data) => setPhase(data.key),
 *     },
 *   })
 */

import { useEffect, useRef, useState } from 'react'
import { SSEManager, type SSEEventHandler } from './sse'

export interface UseSSEOptions {
  url: string
  events?: Record<string, (data: unknown) => void>
  withCredentials?: boolean
}

export interface UseSSEResult {
  connected: boolean
  lastError: Error | null
}

export function useSSE(options: UseSSEOptions): UseSSEResult {
  const { url, withCredentials } = options
  const eventsRef = useRef<Record<string, (data: unknown) => void>>(options.events ?? {})
  const [connected, setConnected] = useState(false)
  const [lastError, setLastError] = useState<Error | null>(null)

  const managerRef = useRef<SSEManager | null>(null)
  const registeredKeysRef = useRef<Set<string>>(new Set())
  const offFnsRef = useRef<Array<() => void>>([])

  const registerEventType = (eventType: string) => {
    const manager = managerRef.current
    if (!manager) return
    if (registeredKeysRef.current.has(eventType)) return

    const handler: SSEEventHandler = data => {
      const fn = eventsRef.current[eventType]
      if (fn) {
        try {
          fn(data)
        } catch (err) {
          setLastError(err instanceof Error ? err : new Error(String(err)))
        }
      }
    }
    offFnsRef.current.push(manager.on(eventType, handler))
    registeredKeysRef.current.add(eventType)
  }

  // Keep latest event handlers in a ref so updating callbacks doesn't
  // tear down the EventSource. Also register any newly-appeared keys.
  useEffect(() => {
    eventsRef.current = options.events ?? {}
    for (const key of Object.keys(eventsRef.current)) {
      registerEventType(key)
    }
  }, [options.events])

  useEffect(() => {
    let cancelled = false
    let pollTimer: ReturnType<typeof setInterval> | null = null

    const manager = new SSEManager({
      url,
      withCredentials: withCredentials === true,
    })
    managerRef.current = manager
    registeredKeysRef.current = new Set()
    offFnsRef.current = []

    // Register a stable forwarding handler per event type that delegates
    // to the latest entry in eventsRef.
    for (const eventType of Object.keys(eventsRef.current)) {
      registerEventType(eventType)
    }

    manager.connect()

    // Poll connection state — `SSEManager` doesn't expose change events,
    // and EventSource readyState transitions silently on reconnect.
    const updateConnected = () => {
      if (cancelled) return
      const next = manager.isConnected()
      setConnected(prev => (prev === next ? prev : next))
    }
    updateConnected()
    pollTimer = setInterval(updateConnected, 750)

    return () => {
      cancelled = true
      if (pollTimer !== null) clearInterval(pollTimer)
      for (const off of offFnsRef.current) off()
      offFnsRef.current = []
      registeredKeysRef.current = new Set()
      managerRef.current = null
      manager.disconnect()
      setConnected(false)
    }
    // Note: `events` is intentionally captured via eventsRef rather than
    // included in deps — consumers can freely change handler identity
    // without forcing the EventSource to be torn down and reconnected.
  }, [url, withCredentials])

  return { connected, lastError }
}
