import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'

import { scheduleLoginRedirect } from '../api/http'
import { useAgentChatStore } from '../stores/agentChatStore'
import type { AgentMessageNotification } from '../types/agentChat'
import { refreshTimelineLatestInCache } from './useTimelineCacheInjector'
import { usePageLifecycle, type PageLifecycleResumeReason, type PageLifecycleSuspendReason } from './usePageLifecycle'
import { TIMELINE_STALE_TIME_MS, timelineQueryKey } from './useAgentTimeline'
import {
  findActiveAgentChatSocketId,
  normalizeAgentChatSocketSubscriptions,
  syncAgentChatSocketSubscriptions,
  type AgentChatSocketContextOverride,
  type AgentChatSocketSubscription,
} from './agentChatSocketProtocol'
import { routeAgentChatSocketMessage } from './agentChatSocketMessageRouter'

const RECONNECT_BASE_DELAY_MS = 1000
const RECONNECT_MAX_DELAY_MS = 15000
const RESYNC_THROTTLE_MS = 4000
const BACKGROUND_SYNC_INTERVAL_MS = 30000
const PING_INTERVAL_MS = 20000
const PONG_TIMEOUT_MS = 8000
const CONNECT_TIMEOUT_MS = 10000

export type AgentChatSocketStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'offline' | 'error'

export type AgentChatSocketSnapshot = {
  status: AgentChatSocketStatus
  lastConnectedAt: number | null
  lastError: string | null
}

function describeCloseEvent(event: CloseEvent): string | null {
  if (event.code === 1000) {
    return null
  }
  if (event.code === 4401) {
    return 'Authentication required.'
  }
  if (event.reason) {
    return event.reason
  }
  return `WebSocket closed (code ${event.code}).`
}

function isAuthErrorMessage(message: string | null | undefined): boolean {
  if (!message) {
    return false
  }
  const normalized = message.toLowerCase()
  return normalized.includes('authentication') || normalized.includes('sign in') || normalized.includes('login')
}

function computeReconnectDelay(attempt: number): number {
  const exponent = Math.min(attempt, 6)
  const base = Math.min(RECONNECT_MAX_DELAY_MS, RECONNECT_BASE_DELAY_MS * 2 ** exponent)
  const jitter = Math.round(base * 0.2 * Math.random())
  return base + jitter
}

function isPageVisible(): boolean {
  if (typeof document === 'undefined') {
    return true
  }
  return document.visibilityState === 'visible'
}

export function useAgentChatSocket(
  desiredSubscriptionsInput: AgentChatSocketSubscription[],
  options: {
    contextOverride?: AgentChatSocketContextOverride
    onCreditEvent?: (payload: Record<string, unknown>) => void
    onAgentProfileEvent?: (payload: Record<string, unknown>) => void
    onMessageNotificationEvent?: (payload: AgentMessageNotification) => void
  } = {},
): AgentChatSocketSnapshot {
  const queryClient = useQueryClient()
  const desiredSubscriptions = useMemo(
    () => normalizeAgentChatSocketSubscriptions(desiredSubscriptionsInput),
    [desiredSubscriptionsInput],
  )
  const receiveEventRef = useRef(useAgentChatStore.getState().receiveRealtimeEvent)
  const updateProcessingRef = useRef(useAgentChatStore.getState().updateProcessing)
  const updateAgentIdentityRef = useRef(useAgentChatStore.getState().updateAgentIdentity)
  const receiveStreamRef = useRef(useAgentChatStore.getState().receiveStreamEvent)
  const creditEventRef = useRef<typeof options.onCreditEvent | null>(options.onCreditEvent ?? null)
  const profileEventRef = useRef<typeof options.onAgentProfileEvent | null>(options.onAgentProfileEvent ?? null)
  const messageNotificationEventRef = useRef<typeof options.onMessageNotificationEvent | null>(
    options.onMessageNotificationEvent ?? null,
  )

  useEffect(() =>
    useAgentChatStore.subscribe((state) => {
      receiveEventRef.current = state.receiveRealtimeEvent
      updateProcessingRef.current = state.updateProcessing
      updateAgentIdentityRef.current = state.updateAgentIdentity
      receiveStreamRef.current = state.receiveStreamEvent
    }),
  [])

  useEffect(() => {
    creditEventRef.current = options.onCreditEvent ?? null
    profileEventRef.current = options.onAgentProfileEvent ?? null
    messageNotificationEventRef.current = options.onMessageNotificationEvent ?? null
  }, [options.onCreditEvent, options.onAgentProfileEvent, options.onMessageNotificationEvent])

  const retryRef = useRef(0)
  const socketRef = useRef<WebSocket | null>(null)
  const timeoutRef = useRef<number | null>(null)
  const syncIntervalRef = useRef<number | null>(null)
  const pingIntervalRef = useRef<number | null>(null)
  const pongTimeoutRef = useRef<number | null>(null)
  const connectTimeoutRef = useRef<number | null>(null)
  const scheduleConnectRef = useRef<(delay: number) => void>(() => undefined)
  const closeSocketRef = useRef<() => void>(() => undefined)
  const closingSocketRef = useRef<WebSocket | null>(null)
  const pauseReasonRef = useRef<'offline' | null>(null)
  const lastSyncAtRef = useRef(0)
  const lastActivityAtRef = useRef(0)
  const desiredSubscriptionsRef = useRef<AgentChatSocketSubscription[]>(desiredSubscriptions)
  const contextOverrideRef = useRef<AgentChatSocketContextOverride>(options.contextOverride)
  const activeAgentIdRef = useRef<string | null>(findActiveAgentChatSocketId(desiredSubscriptions))
  const subscribedAgentsRef = useRef<Map<string, AgentChatSocketSubscription['mode']>>(new Map())
  const [snapshot, setSnapshot] = useState<AgentChatSocketSnapshot>({
    status: 'idle',
    lastConnectedAt: null,
    lastError: null,
  })

  useEffect(() => {
    desiredSubscriptionsRef.current = desiredSubscriptions
    activeAgentIdRef.current = findActiveAgentChatSocketId(desiredSubscriptions)
  }, [desiredSubscriptions])

  useEffect(() => {
    contextOverrideRef.current = options.contextOverride
  }, [options.contextOverride])

  const updateSnapshot = useCallback((updates: Partial<AgentChatSocketSnapshot>) => {
    setSnapshot((current) => ({ ...current, ...updates }))
  }, [])

  const sendSocketMessage = useCallback((payload: Record<string, unknown>) => {
    const socket = socketRef.current
    if (!socket || socket.readyState !== WebSocket.OPEN) {
      return false
    }
    try {
      socket.send(JSON.stringify(payload))
      return true
    } catch (error) {
      console.warn('Failed to send agent chat socket message', error)
      return false
    }
  }, [])

  const markActivity = useCallback(() => {
    lastActivityAtRef.current = Date.now()
  }, [])

  const clearPingTimers = useCallback(() => {
    if (pingIntervalRef.current !== null) {
      clearInterval(pingIntervalRef.current)
      pingIntervalRef.current = null
    }
    if (pongTimeoutRef.current !== null) {
      clearTimeout(pongTimeoutRef.current)
      pongTimeoutRef.current = null
    }
  }, [])

  const schedulePongTimeout = useCallback(
    (sentAt: number) => {
      if (pongTimeoutRef.current !== null) {
        clearTimeout(pongTimeoutRef.current)
      }
      pongTimeoutRef.current = window.setTimeout(() => {
        const socket = socketRef.current
        if (!socket || socket.readyState !== WebSocket.OPEN) {
          return
        }
        if (lastActivityAtRef.current >= sentAt) {
          return
        }
        updateSnapshot({ status: 'reconnecting', lastError: 'WebSocket keepalive timed out.' })
        socket.close()
      }, PONG_TIMEOUT_MS)
    },
    [updateSnapshot],
  )

  const sendPing = useCallback(() => {
    if (pauseReasonRef.current !== null) {
      return
    }
    const sentAt = Date.now()
    if (sendSocketMessage({ type: 'ping' })) {
      schedulePongTimeout(sentAt)
    }
  }, [schedulePongTimeout, sendSocketMessage])

  const startPingLoop = useCallback(() => {
    if (pingIntervalRef.current !== null) {
      return
    }
    pingIntervalRef.current = window.setInterval(() => {
      sendPing()
    }, PING_INTERVAL_MS)
    sendPing()
  }, [sendPing])

  const stopPingLoop = useCallback(() => {
    clearPingTimers()
  }, [clearPingTimers])

  const clearConnectTimeout = useCallback(() => {
    if (connectTimeoutRef.current !== null) {
      clearTimeout(connectTimeoutRef.current)
      connectTimeoutRef.current = null
    }
  }, [])

  const syncNow = useCallback((mode: 'fast' | 'contiguous' = 'fast') => {
    if (typeof navigator !== 'undefined' && navigator.onLine === false) {
      return
    }
    if (!desiredSubscriptionsRef.current.length) {
      return
    }
    const now = Date.now()
    if (now - lastSyncAtRef.current < RESYNC_THROTTLE_MS) {
      return
    }
    lastSyncAtRef.current = now

    desiredSubscriptionsRef.current.forEach(({ agentId }) => {
      const timelineState = queryClient.getQueryState(timelineQueryKey(agentId))
      if (!timelineState) {
        return
      }
      if (timelineState.fetchStatus === 'fetching') {
        return
      }
      if (timelineState.dataUpdatedAt && now - timelineState.dataUpdatedAt < TIMELINE_STALE_TIME_MS) {
        return
      }
      void refreshTimelineLatestInCache(queryClient, agentId, { mode })
    })
  }, [queryClient])

  const applySubscriptions = useCallback((nextSubscriptions: AgentChatSocketSubscription[]) => {
    const socket = socketRef.current
    if (!socket || socket.readyState !== WebSocket.OPEN) {
      return
    }

    syncAgentChatSocketSubscriptions({
      currentSubscriptions: subscribedAgentsRef.current,
      desiredSubscriptions: nextSubscriptions,
      contextOverride: contextOverrideRef.current,
      sendSocketMessage,
      handleSendFailure: () => {
        updateSnapshot({ status: 'reconnecting', lastError: 'WebSocket send failed.' })
        socket.close()
      },
    })
  }, [sendSocketMessage, updateSnapshot])

  useEffect(() => {
    applySubscriptions(desiredSubscriptions)
  }, [applySubscriptions, desiredSubscriptions])

  const handleResume = useCallback((reason: PageLifecycleResumeReason) => {
    if (pauseReasonRef.current === 'offline' && reason !== 'online') {
      return
    }
    pauseReasonRef.current = null
    retryRef.current = 0
    const existingSocket = socketRef.current
    if (existingSocket?.readyState === WebSocket.OPEN) {
      updateSnapshot({ status: 'connected', lastError: null })
      startPingLoop()
      applySubscriptions(desiredSubscriptionsRef.current)
      syncNow('contiguous')
      return
    }
    if (existingSocket?.readyState === WebSocket.CONNECTING) {
      updateSnapshot({ status: retryRef.current > 0 ? 'reconnecting' : 'connecting', lastError: null })
      syncNow('contiguous')
      return
    }
    updateSnapshot({ status: 'connecting', lastError: null })
    scheduleConnectRef.current(0)
    syncNow('contiguous')
  }, [applySubscriptions, startPingLoop, syncNow, updateSnapshot])

  const handleSuspend = useCallback((reason: PageLifecycleSuspendReason) => {
    if (reason === 'offline') {
      pauseReasonRef.current = 'offline'
      retryRef.current = 0
      updateSnapshot({ status: 'offline', lastError: 'Network connection lost.' })
      stopPingLoop()
      if (timeoutRef.current !== null) {
        clearTimeout(timeoutRef.current)
        timeoutRef.current = null
      }
      closeSocketRef.current()
      return
    }
  }, [stopPingLoop, updateSnapshot])

  usePageLifecycle({ onResume: handleResume, onSuspend: handleSuspend })

  useEffect(() => {
    retryRef.current = 0
    lastSyncAtRef.current = 0

    const scheduleConnect = (delay: number) => {
      if (timeoutRef.current !== null) {
        clearTimeout(timeoutRef.current)
      }
      timeoutRef.current = window.setTimeout(() => {
        openSocket()
      }, delay)
    }
    scheduleConnectRef.current = scheduleConnect

    const closeSocket = () => {
      if (socketRef.current) {
        stopPingLoop()
        clearConnectTimeout()
        closingSocketRef.current = socketRef.current
        try {
          socketRef.current.close()
        } catch (error) {
          closingSocketRef.current = null
          console.warn('Failed to close agent chat socket', error)
        }
        socketRef.current = null
        subscribedAgentsRef.current = new Map()
      }
    }
    closeSocketRef.current = closeSocket

    const openSocket = () => {
      if (pauseReasonRef.current !== null) {
        return
      }
      const existing = socketRef.current
      if (existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)) {
        return
      }
      const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
      const socket = new WebSocket(`${protocol}://${window.location.host}/ws/agents/chat/`)
      const socketInstance = socket
      socketRef.current = socket
      updateSnapshot({
        status: retryRef.current > 0 ? 'reconnecting' : 'connecting',
        lastError: null,
      })
      clearConnectTimeout()
      connectTimeoutRef.current = window.setTimeout(() => {
        if (socketRef.current !== socketInstance) {
          return
        }
        if (socketInstance.readyState === WebSocket.CONNECTING) {
          updateSnapshot({ status: 'reconnecting', lastError: 'WebSocket connection timed out.' })
          socketInstance.close()
        }
      }, CONNECT_TIMEOUT_MS)

      socket.onopen = () => {
        if (socketRef.current !== socketInstance) {
          return
        }
        clearConnectTimeout()
        retryRef.current = 0
        markActivity()
        updateSnapshot({
          status: 'connected',
          lastConnectedAt: Date.now(),
          lastError: null,
        })
        subscribedAgentsRef.current = new Map()
        applySubscriptions(desiredSubscriptionsRef.current)
        startPingLoop()
        syncNow('contiguous')
      }

      socket.onmessage = (event) => {
        if (socketRef.current !== socketInstance) {
          return
        }
        try {
          const payload = JSON.parse(event.data)
          markActivity()
          const outcome = routeAgentChatSocketMessage({
            payload,
            queryClient,
            activeAgentId: activeAgentIdRef.current,
            receiveRealtimeEvent: receiveEventRef.current,
            updateProcessing: updateProcessingRef.current,
            updateAgentIdentity: updateAgentIdentityRef.current,
            receiveStreamEvent: receiveStreamRef.current,
            onCreditEvent: creditEventRef.current,
            onAgentProfileEvent: profileEventRef.current,
            onMessageNotificationEvent: messageNotificationEventRef.current,
          })
          if (outcome.type === 'subscription_error') {
            if (outcome.agentId) {
              subscribedAgentsRef.current.delete(outcome.agentId)
            }
            if (!outcome.agentId || outcome.agentId === activeAgentIdRef.current) {
              updateSnapshot({ status: 'error', lastError: outcome.message })
              if (isAuthErrorMessage(outcome.message)) {
                scheduleLoginRedirect()
              }
              syncNow('contiguous')
            }
          }
        } catch (error) {
          console.error('Failed to process websocket message', error)
        }
      }

      socket.onclose = (event) => {
        if (socketRef.current !== socketInstance) {
          if (closingSocketRef.current === socketInstance) {
            closingSocketRef.current = null
          }
          return
        }
        clearConnectTimeout()
        socketRef.current = null
        subscribedAgentsRef.current = new Map()
        stopPingLoop()
        if (closingSocketRef.current === socketInstance) {
          closingSocketRef.current = null
          return
        }
        if (typeof navigator !== 'undefined' && navigator.onLine === false) {
          pauseReasonRef.current = 'offline'
          retryRef.current = 0
          updateSnapshot({ status: 'offline', lastError: 'Network connection lost.' })
          return
        }
        if (pauseReasonRef.current !== null) {
          return
        }
        const errorMessage = describeCloseEvent(event)
        if (event.code === 4401) {
          updateSnapshot({
            status: 'error',
            lastError: errorMessage || 'Authentication required.',
          })
          scheduleLoginRedirect()
          return
        }
        if (event.code >= 4400 && event.code < 4500) {
          updateSnapshot({
            status: 'error',
            lastError: errorMessage || 'WebSocket authorization failed.',
          })
          return
        }
        updateSnapshot({
          status: 'reconnecting',
          lastError: errorMessage,
        })
        const delay = computeReconnectDelay(retryRef.current)
        retryRef.current += 1
        scheduleConnect(delay)
      }

      socket.onerror = () => {
        if (socketRef.current !== socketInstance) {
          return
        }
        clearConnectTimeout()
        updateSnapshot({
          status: 'reconnecting',
          lastError: 'WebSocket connection error.',
        })
        socket.close()
      }
    }

    pauseReasonRef.current = null
    if (typeof navigator !== 'undefined' && navigator.onLine === false) {
      pauseReasonRef.current = 'offline'
      updateSnapshot({ status: 'offline', lastError: 'Network connection lost.' })
    } else if (pauseReasonRef.current === null) {
      scheduleConnect(0)
    }

    if (syncIntervalRef.current === null) {
      syncIntervalRef.current = window.setInterval(() => {
        if (pauseReasonRef.current !== null) {
          return
        }
        if (!isPageVisible()) {
          return
        }
        syncNow()
      }, BACKGROUND_SYNC_INTERVAL_MS)
    }

    return () => {
      if (timeoutRef.current !== null) {
        clearTimeout(timeoutRef.current)
        timeoutRef.current = null
      }
      if (syncIntervalRef.current !== null) {
        clearInterval(syncIntervalRef.current)
        syncIntervalRef.current = null
      }
      clearConnectTimeout()
      stopPingLoop()
      closeSocket()
    }
  }, [
    clearConnectTimeout,
    markActivity,
    startPingLoop,
    stopPingLoop,
    syncNow,
    updateSnapshot,
    applySubscriptions,
  ])

  return snapshot
}
