/**
 * Composable for WebSocket connectivity to receive live updates.
 *
 * Connects to the backend WS endpoint and allows subscribing to
 * individual vote session channels for real-time tally updates.
 *
 * Production-grade features:
 * - Bearer token authentication via query param
 * - Heartbeat (ping every 25s, pong expected within 10s)
 * - Exponential backoff on reconnect (1s, 2s, 4s, 8s, max 30s)
 * - Max reconnect attempts: 10
 * - Typed event handlers for domain events
 * - Message queue during disconnection with replay on reconnect
 */

/** WebSocket event types from the backend. */
export type WsEventType =
  | 'vote_submitted'
  | 'decision_advanced'
  | 'mandate_updated'
  | 'document_changed'
  | 'sanctuary_archived'
  | 'pong'
  | 'error'

/** Typed WebSocket message from the backend. */
export interface WsMessage {
  event: WsEventType
  data: any
  timestamp?: string
}

/** Event handler callback type. */
type WsEventHandler = (data: any) => void

/** Maximum reconnect attempts before giving up. */
const MAX_RECONNECT_ATTEMPTS = 10

/** Base delay for reconnection backoff (ms). */
const RECONNECT_BASE_MS = 1_000

/** Maximum delay between reconnection attempts (ms). */
const RECONNECT_MAX_MS = 30_000

/** Interval between heartbeat pings (ms). */
const HEARTBEAT_INTERVAL_MS = 25_000

/** Maximum time to wait for a pong response (ms). */
const PONG_TIMEOUT_MS = 10_000

export function useWebSocket() {
  const config = useRuntimeConfig()
  const auth = useAuthStore()

  let ws: WebSocket | null = null
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null
  let heartbeatTimer: ReturnType<typeof setInterval> | null = null
  let pongTimeoutTimer: ReturnType<typeof setTimeout> | null = null
  let reconnectAttempts = 0
  let intentionalClose = false

  const connected = ref(false)
  const lastMessage = ref<WsMessage | null>(null)
  const error = ref<string | null>(null)

  /** Message queue: messages sent while disconnected are replayed on reconnect. */
  const messageQueue: string[] = []

  /** Typed event handlers registry. */
  const eventHandlers: Map<WsEventType, Set<WsEventHandler>> = new Map()

  /**
   * Open a WebSocket connection to the backend live endpoint.
   */
  function connect() {
    if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
      return
    }

    // Reset state
    error.value = null
    intentionalClose = false

    // Build WS URL with authentication token
    let wsUrl = config.public.apiBase
      .replace(/^http/, 'ws')
      .replace(/\/api\/v1$/, '/api/v1/ws/live')

    if (auth.token) {
      wsUrl += `?token=${encodeURIComponent(auth.token)}`
    }

    ws = new WebSocket(wsUrl)

    ws.onopen = () => {
      connected.value = true
      reconnectAttempts = 0
      error.value = null

      // Start heartbeat
      _startHeartbeat()

      // Replay queued messages
      _flushMessageQueue()
    }

    ws.onclose = (event: CloseEvent) => {
      connected.value = false
      _stopHeartbeat()

      if (!intentionalClose) {
        _scheduleReconnect()
      }
    }

    ws.onerror = () => {
      connected.value = false
    }

    ws.onmessage = (event: MessageEvent) => {
      try {
        const message: WsMessage = JSON.parse(event.data)
        lastMessage.value = message

        // Handle pong for heartbeat
        if (message.event === 'pong') {
          _onPongReceived()
          return
        }

        // Dispatch to typed event handlers
        _dispatchEvent(message)
      } catch {
        // Non-JSON message, store as-is
        lastMessage.value = { event: 'error', data: event.data }
      }
    }
  }

  /**
   * Subscribe to real-time updates for a vote session.
   */
  function subscribe(sessionId: string) {
    _send(JSON.stringify({ action: 'subscribe', session_id: sessionId }))
  }

  /**
   * Unsubscribe from a vote session's updates.
   */
  function unsubscribe(sessionId: string) {
    _send(JSON.stringify({ action: 'unsubscribe', session_id: sessionId }))
  }

  /**
   * Gracefully close the WebSocket connection.
   */
  function disconnect() {
    intentionalClose = true

    if (reconnectTimer) {
      clearTimeout(reconnectTimer)
      reconnectTimer = null
    }

    _stopHeartbeat()

    if (ws) {
      ws.onclose = null
      ws.onerror = null
      ws.onmessage = null
      ws.close()
      ws = null
    }

    connected.value = false
    reconnectAttempts = 0
    error.value = null
  }

  // ---- Typed event handler registration ----

  /**
   * Register a handler for when a vote is submitted.
   */
  function onVoteSubmitted(handler: WsEventHandler): () => void {
    return _addEventHandler('vote_submitted', handler)
  }

  /**
   * Register a handler for when a decision advances to the next step.
   */
  function onDecisionAdvanced(handler: WsEventHandler): () => void {
    return _addEventHandler('decision_advanced', handler)
  }

  /**
   * Register a handler for when a mandate is updated.
   */
  function onMandateUpdated(handler: WsEventHandler): () => void {
    return _addEventHandler('mandate_updated', handler)
  }

  /**
   * Register a handler for when a document is changed.
   */
  function onDocumentChanged(handler: WsEventHandler): () => void {
    return _addEventHandler('document_changed', handler)
  }

  /**
   * Register a handler for when a document is archived to the sanctuary.
   */
  function onSanctuaryArchived(handler: WsEventHandler): () => void {
    return _addEventHandler('sanctuary_archived', handler)
  }

  // ---- Internal helpers ----

  /**
   * Send a message via WebSocket, or queue it if disconnected.
   */
  function _send(message: string) {
    if (ws?.readyState === WebSocket.OPEN) {
      ws.send(message)
    } else {
      messageQueue.push(message)
    }
  }

  /**
   * Replay all queued messages after reconnection.
   */
  function _flushMessageQueue() {
    while (messageQueue.length > 0) {
      const message = messageQueue.shift()!
      if (ws?.readyState === WebSocket.OPEN) {
        ws.send(message)
      } else {
        // Put it back if connection dropped again
        messageQueue.unshift(message)
        break
      }
    }
  }

  /**
   * Register an event handler and return an unsubscribe function.
   */
  function _addEventHandler(event: WsEventType, handler: WsEventHandler): () => void {
    if (!eventHandlers.has(event)) {
      eventHandlers.set(event, new Set())
    }
    eventHandlers.get(event)!.add(handler)

    // Return unsubscribe function
    return () => {
      eventHandlers.get(event)?.delete(handler)
    }
  }

  /**
   * Dispatch a received message to all registered handlers for its event type.
   */
  function _dispatchEvent(message: WsMessage) {
    const handlers = eventHandlers.get(message.event)
    if (handlers) {
      for (const handler of handlers) {
        try {
          handler(message.data)
        } catch (err) {
          console.error(`[WS] Erreur dans le handler pour "${message.event}":`, err)
        }
      }
    }
  }

  /**
   * Start the heartbeat: send ping every 25s and expect pong within 10s.
   */
  function _startHeartbeat() {
    _stopHeartbeat()

    heartbeatTimer = setInterval(() => {
      if (ws?.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ action: 'ping' }))

        // Start pong timeout
        pongTimeoutTimer = setTimeout(() => {
          console.warn('[WS] Pong non recu dans le delai imparti, reconnexion...')
          // Force close and reconnect
          if (ws) {
            ws.close()
          }
        }, PONG_TIMEOUT_MS)
      }
    }, HEARTBEAT_INTERVAL_MS)
  }

  /**
   * Stop heartbeat timers.
   */
  function _stopHeartbeat() {
    if (heartbeatTimer) {
      clearInterval(heartbeatTimer)
      heartbeatTimer = null
    }
    if (pongTimeoutTimer) {
      clearTimeout(pongTimeoutTimer)
      pongTimeoutTimer = null
    }
  }

  /**
   * Handle pong response: cancel the pong timeout.
   */
  function _onPongReceived() {
    if (pongTimeoutTimer) {
      clearTimeout(pongTimeoutTimer)
      pongTimeoutTimer = null
    }
  }

  /**
   * Schedule a reconnection attempt with exponential backoff.
   */
  function _scheduleReconnect() {
    if (reconnectTimer) return

    if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
      error.value = 'Connexion au serveur perdue. Veuillez rafraichir la page.'
      console.error(`[WS] Nombre maximum de tentatives de reconnexion atteint (${MAX_RECONNECT_ATTEMPTS})`)
      return
    }

    const backoffMs = Math.min(
      RECONNECT_BASE_MS * Math.pow(2, reconnectAttempts),
      RECONNECT_MAX_MS,
    )

    reconnectAttempts++
    console.info(`[WS] Tentative de reconnexion ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} dans ${backoffMs}ms`)

    reconnectTimer = setTimeout(() => {
      reconnectTimer = null
      connect()
    }, backoffMs)
  }

  return {
    connected,
    lastMessage,
    error,
    connect,
    subscribe,
    unsubscribe,
    disconnect,
    onVoteSubmitted,
    onDecisionAdvanced,
    onMandateUpdated,
    onDocumentChanged,
    onSanctuaryArchived,
  }
}
