'use client'

/**
 * Singleton WebSocket provider.
 *
 * The entire app shares ONE connection to /ws/prices. Components subscribe
 * to specific message types via `useWsSubscribe` — no component owns the socket.
 *
 * Why: previously DashboardClient (via usePriceSocket) and SignalMonitor each
 * opened their own WebSocket, giving the server two connections per page load
 * and causing each component to receive every broadcast twice.
 */

import {
  createContext,
  useContext,
  useEffect,
  useRef,
  type ReactNode,
} from 'react'

type Handler = (msg: unknown) => void

interface WsContextValue {
  subscribe: (type: string, fn: Handler) => () => void
}

const WsContext = createContext<WsContextValue | null>(null)

const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000'

export function WsProvider({ children }: { children: ReactNode }) {
  // Stable map: message-type → set of handlers. Never replaced, only mutated.
  const subs = useRef<Map<string, Set<Handler>>>(new Map())

  useEffect(() => {
    let dead = false
    let retryTimer: ReturnType<typeof setTimeout> | null = null
    // Hold the current socket at useEffect scope so cleanup can close it.
    // Previously `ws` lived inside connect() and cleanup couldn't reach it,
    // leaking one connection per StrictMode remount and one per [locale]
    // layout swap (which remounts WsProvider).
    let socket: WebSocket | null = null

    function connect() {
      if (dead) return
      socket = new WebSocket(`${WS_URL}/ws/prices`)
      const ws = socket   // local alias for handler closures

      ws.onmessage = (e) => {
        try {
          const msg = JSON.parse(e.data) as { type?: string }
          const t = msg.type ?? '__unknown__'
          subs.current.get(t)?.forEach((fn) => fn(msg))
        } catch {
          // ignore malformed frames
        }
      }

      ws.onclose = () => {
        if (!dead) retryTimer = setTimeout(connect, 3000)
      }
      ws.onerror = () => ws.close()
    }

    connect()
    return () => {
      dead = true
      if (retryTimer) clearTimeout(retryTimer)
      // Actively close any live socket so the OS-level connection releases
      // immediately on unmount instead of waiting for the next server keepalive.
      if (socket && socket.readyState <= WebSocket.OPEN) {
        socket.onclose = null   // suppress the auto-reconnect we'd otherwise queue
        socket.close()
      }
      socket = null
    }
  }, [])

  function subscribe(type: string, fn: Handler): () => void {
    if (!subs.current.has(type)) subs.current.set(type, new Set())
    subs.current.get(type)!.add(fn)
    return () => subs.current.get(type)?.delete(fn)
  }

  return <WsContext.Provider value={{ subscribe }}>{children}</WsContext.Provider>
}

/**
 * Subscribe to a specific WebSocket message type from the shared connection.
 *
 * @param type    The `msg.type` string to listen for (e.g. 'price', 'new_post', 'funding_signal')
 * @param handler Called with the full parsed message object on every matching frame.
 *                Always receives the latest handler reference — no stale-closure issues.
 *
 * Usage:
 *   useWsSubscribe('price', (msg) => { ... })
 */
export function useWsSubscribe(type: string, handler: Handler): void {
  const ctx = useContext(WsContext)
  // Keep a ref so the subscription closure never goes stale even if handler
  // is an inline function that changes every render.
  const handlerRef = useRef<Handler>(handler)
  handlerRef.current = handler

  useEffect(() => {
    if (!ctx) return
    return ctx.subscribe(type, (msg) => handlerRef.current(msg))
  }, [ctx, type]) // type is stable in practice; ctx never changes
}
