import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
} from 'react'
import type { ReactNode } from 'react'
import type { ServerFrame } from '@platform-modules/realtime'
import type {
  RealtimeClient,
  RealtimeClientStatus,
  RealtimeSendData,
} from '@platform-modules/realtime/client'

/**
 * `@platform-modules/realtime-react` — headless React binding for the framework-neutral
 * `@platform-modules/realtime/client` capability. React owns subscription lifecycle only;
 * socket sharing, reconnect/backoff, frame fan-out, send, and reference ownership live upstream.
 */

export type ConnectionStatus = RealtimeClientStatus
export type RealtimeReactErrorCode = 'missing-provider'

export class RealtimeReactError extends Error {
  readonly code: RealtimeReactErrorCode

  constructor(code: RealtimeReactErrorCode) {
    super(
      code === 'missing-provider'
        ? 'useRealtimeClient must be used within <RealtimeProvider client={...}>'
        : `Realtime React error: ${code}`,
    )
    this.name = 'RealtimeReactError'
    this.code = code
  }
}

const RealtimeContext = createContext<RealtimeClient<ServerFrame> | null>(null)

export interface RealtimeProviderProps<F extends ServerFrame = ServerFrame> {
  client: RealtimeClient<F>
  children: ReactNode
}

/** Provide one explicitly-created realtime client to a React subtree. */
export function RealtimeProvider<F extends ServerFrame = ServerFrame>({
  client,
  children,
}: RealtimeProviderProps<F>): ReactNode {
  return (
    <RealtimeContext.Provider value={client as RealtimeClient<ServerFrame>}>
      {children}
    </RealtimeContext.Provider>
  )
}

/** Return the subtree client. Fails contextfully rather than silently opening another socket. */
export function useRealtimeClient<F extends ServerFrame = ServerFrame>(): RealtimeClient<F> {
  const client = useContext(RealtimeContext)
  if (client === null) {
    throw new RealtimeReactError('missing-provider')
  }
  return client as RealtimeClient<F>
}

export interface UseChannelOptions<F extends ServerFrame> {
  /** Invoked for every parsed frame. Callback identity changes do not reconnect the client. */
  onFrame?: (frame: F) => void
  /** Invoked on every status transition. Callback identity changes do not reconnect the client. */
  onStatus?: (status: ConnectionStatus) => void
}

export interface ChannelState<F extends ServerFrame> {
  status: ConnectionStatus
  lastFrame: F | null
  send: (data: RealtimeSendData) => boolean
}

/**
 * Bind to the provided shared realtime client. Mount acquires one client retain; unmount releases it.
 * Multiple hooks under the same provider therefore share one underlying connection.
 */
export function useChannel<F extends ServerFrame = ServerFrame>(
  options: UseChannelOptions<F> = {},
): ChannelState<F> {
  const client = useRealtimeClient<F>()
  const [status, setStatus] = useState<ConnectionStatus>(() => client.getStatus())
  const [lastFrame, setLastFrame] = useState<F | null>(() => client.getLastFrame())
  const onFrameRef = useRef(options.onFrame)
  const onStatusRef = useRef(options.onStatus)

  useEffect(() => {
    onFrameRef.current = options.onFrame
    onStatusRef.current = options.onStatus
  })

  useEffect(() => {
    setStatus(client.getStatus())
    setLastFrame(client.getLastFrame())

    const unsubscribeFrame = client.subscribe((frame) => {
      setLastFrame(frame)
      onFrameRef.current?.(frame)
    })
    const unsubscribeStatus = client.subscribeStatus((next) => {
      setStatus(next)
      onStatusRef.current?.(next)
    })
    const release = client.retain()

    // `retain()` can synchronously transition idle→connecting before the subscription effect
    // finishes; subscriptions are installed first, and this read also covers custom clients.
    setStatus(client.getStatus())

    return () => {
      unsubscribeFrame()
      unsubscribeStatus()
      release()
    }
  }, [client])

  const send = useCallback<ChannelState<F>['send']>((data) => client.send(data), [client])

  return { status, lastFrame, send }
}

export interface UsePresenceOptions<F extends ServerFrame> {
  presenceType?: string
  select?: (frame: F) => readonly string[]
}

export interface Presence {
  online: readonly string[]
  status: ConnectionStatus
}

const defaultPresenceSelect = (frame: ServerFrame): readonly string[] => {
  const online = (frame as { online?: unknown }).online
  return Array.isArray(online) ? online.filter((id): id is string => typeof id === 'string') : []
}

/** Host-configurable presence derivation over generic realtime frames. */
export function usePresence<F extends ServerFrame = ServerFrame>(
  options: UsePresenceOptions<F> = {},
): Presence {
  const client = useRealtimeClient<F>()
  const {
    presenceType = 'presence',
    select = defaultPresenceSelect as (frame: F) => readonly string[],
  } = options
  const [online, setOnline] = useState<readonly string[]>([])

  // A different provider/client is a different stream; never carry a stale roster across it.
  useEffect(() => setOnline([]), [client])

  const { status } = useChannel<F>({
    onFrame: (frame) => {
      if (frame.type === presenceType) setOnline(select(frame))
    },
  })

  return { online, status }
}

export interface ChannelProps<F extends ServerFrame> extends UseChannelOptions<F> {
  render: (channel: ChannelState<F>) => ReactNode
}

/** Headless render-prop wrapper; renders no markup of its own. */
export function Channel<F extends ServerFrame = ServerFrame>({
  render,
  ...options
}: ChannelProps<F>): ReactNode {
  return render(useChannel<F>(options))
}
