import { jellyfinClient } from '../api/jellyfin'

/**
 * Lightweight Jellyfin WebSocket wrapper scoped to SyncPlay messages.
 * The socket is opened on demand (when a SyncPlay group is joined),
 * pushes JSON-decoded messages to subscribers, and reconnects with
 * exponential backoff if the server drops us.
 *
 * We intentionally don't ship a full WS client - other features in the
 * app (push notifications, library updates) would need different
 * subscription scopes, so a generic event bus would do more harm than
 * good. When those land they can graduate this into a shared singleton.
 */

export type SyncPlayMessage =
  | { MessageType: 'SyncPlayGroupUpdate'; Data: SyncPlayGroupUpdate }
  | { MessageType: 'SyncPlayCommand'; Data: SyncPlayCommand }
  | { MessageType: string; Data?: unknown }

export interface SyncPlayGroupUpdate {
  GroupId?: string
  Type?: string
  Data?: unknown
}

export interface SyncPlayCommand {
  GroupId?: string
  Command?: 'Pause' | 'Unpause' | 'Stop' | 'Seek' | string
  PositionTicks?: number
  When?: string
  EmittedAt?: string
}

type Listener = (msg: SyncPlayMessage) => void

let socket: WebSocket | null = null
const listeners = new Set<Listener>()
let retry = 0
let retryTimer: number | null = null
let pingTimer: number | null = null
let stopped = false

function wsUrl(): string | null {
  const auth = jellyfinClient.getAuthState()
  const api = jellyfinClient.getApi()
  if (!auth || !api) return null
  const base = auth.serverUrl.replace(/^http/, 'ws')
  const deviceId = (api.deviceInfo?.id) || 'jf-desktop'
  return `${base}/socket?api_key=${encodeURIComponent(auth.token)}&deviceId=${encodeURIComponent(deviceId)}`
}

function clearRetryTimer() {
  if (retryTimer != null) {
    clearTimeout(retryTimer)
    retryTimer = null
  }
}

function clearPingTimer() {
  if (pingTimer != null) {
    clearInterval(pingTimer)
    pingTimer = null
  }
}

function connect() {
  if (stopped) return
  const url = wsUrl()
  if (!url) return
  try {
    socket = new WebSocket(url)
  } catch {
    scheduleReconnect()
    return
  }
  socket.onopen = () => {
    retry = 0
    // Server keeps the socket alive while it sees periodic KeepAlive
    // pings. We send the start-keepalive sentinel, then heartbeat every
    // 30s ourselves so we look active to the gateway in case the server
    // skips its own pings.
    try {
      socket?.send(JSON.stringify({ MessageType: 'KeepAlive' }))
    } catch { /* noop */ }
    clearPingTimer()
    pingTimer = window.setInterval(() => {
      try {
        socket?.send(JSON.stringify({ MessageType: 'KeepAlive' }))
      } catch { /* noop */ }
    }, 30_000)
  }
  socket.onmessage = ev => {
    try {
      const data = JSON.parse(ev.data) as SyncPlayMessage
      if (!data.MessageType) return
      if (data.MessageType.startsWith('SyncPlay')) {
        for (const l of listeners) l(data)
      }
    } catch { /* skip malformed frames */ }
  }
  socket.onclose = () => {
    clearPingTimer()
    socket = null
    scheduleReconnect()
  }
  socket.onerror = () => {
    try { socket?.close() } catch { /* noop */ }
  }
}

function scheduleReconnect() {
  if (stopped) return
  clearRetryTimer()
  const delay = Math.min(30_000, 1000 * 2 ** Math.min(retry, 5))
  retry++
  retryTimer = window.setTimeout(connect, delay)
}

export function startSyncPlaySocket() {
  stopped = false
  if (socket) return
  connect()
}

export function stopSyncPlaySocket() {
  stopped = true
  listeners.clear()
  clearRetryTimer()
  clearPingTimer()
  if (socket) {
    try { socket.close() } catch { /* noop */ }
    socket = null
  }
}

export function subscribeSyncPlay(listener: Listener): () => void {
  listeners.add(listener)
  return () => {
    listeners.delete(listener)
  }
}
