/* eslint-disable react-hooks/refs, react-hooks/set-state-in-effect -- pre-existing pattern flagged by the React
   Compiler rules added in eslint-plugin-react-hooks v7; refactoring it is
   out of scope for the lint toolchain upgrade. */
import { StreamChatService } from '@linktr.ee/messaging-core'
import React, {
  createContext,
  useContext,
  useEffect,
  useState,
  useRef,
  useCallback,
} from 'react'
import { StreamChat } from 'stream-chat'
import { Chat } from 'stream-chat-react'

import { MessagingLoggerProvider, resolveLogger } from '../logging'
import type { MessagingProviderProps, MessagingCapabilities } from '../types'

/**
 * Context value for messaging state and service
 */
export interface MessagingContextValue {
  service: StreamChatService | null
  client: StreamChat | null // Stream Chat client
  isConnected: boolean
  isLoading: boolean
  error: string | null
  capabilities: MessagingCapabilities
  refreshConnection: () => Promise<void>
  debug: boolean
}

const MessagingContext = createContext<MessagingContextValue>({
  service: null,
  client: null,
  isConnected: false,
  isLoading: false,
  error: null,
  capabilities: {},
  refreshConnection: async () => {},
  debug: false,
})

/**
 * Hook to access messaging context
 */
export const useMessagingContext = () => useContext(MessagingContext)

/**
 * Provider component that wraps messaging-core with React state management
 */
export const MessagingProvider: React.FC<MessagingProviderProps> = ({
  children,
  user,
  serviceConfig,
  apiKey,
  capabilities = {},
  debug = false,
  client: injectedClient,
  logger,
}) => {
  // Create debug logger that respects the debug prop
  const debugLog = useCallback(
    (message: string, ...args: unknown[]) => {
      if (debug) {
        console.log(`🔥 [MessagingProvider] ${message}`, ...args)
      }
    },
    [debug]
  )

  debugLog('🔄 RENDER START', {
    userId: user?.id,
    apiKey: apiKey?.substring(0, 8) + '...',
    serviceConfig: !!serviceConfig,
    capabilities: Object.keys(capabilities),
  })

  const [service, setService] = useState<StreamChatService | null>(null)
  const [client, setClient] = useState<StreamChat | null>(null)
  const [isConnected, setIsConnected] = useState(false)
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  // Prevent multiple concurrent connection attempts
  const connectingRef = useRef(false)

  // The single service instance, for teardown in the client-cleanup effect.
  const serviceRef = useRef<StreamChatService | null>(null)

  // serviceConfig / debug are read at service-creation time via refs so that
  // changing them does not recreate the service (and re-adopt the live client).
  // The service is created once per client lifetime (apiKey); the connection is
  // driven by `user`. Real consumers pass a stable, memoized serviceConfig.
  const serviceConfigRef = useRef(serviceConfig)
  serviceConfigRef.current = serviceConfig
  const debugRef = useRef(debug)
  debugRef.current = debug
  const loggerRef = useRef(logger)
  loggerRef.current = logger

  // Track renders and prop changes
  const prevPropsRef = useRef({
    userId: user?.id,
    apiKey,
    serviceConfig,
    capabilities,
  })
  const renderCountRef = useRef(0)
  renderCountRef.current++

  debugLog('📊 RENDER INFO', {
    renderCount: renderCountRef.current,
    currentProps: { userId: user?.id, apiKey: apiKey?.substring(0, 8) + '...' },
    propChanges: {
      userChanged: prevPropsRef.current.userId !== user?.id,
      apiKeyChanged: prevPropsRef.current.apiKey !== apiKey,
      serviceConfigChanged:
        prevPropsRef.current.serviceConfig !== serviceConfig,
      capabilitiesChanged: prevPropsRef.current.capabilities !== capabilities,
    },
  })

  prevPropsRef.current = {
    userId: user?.id,
    apiKey,
    serviceConfig,
    capabilities,
  }

  // Construct the Stream client synchronously so <Chat> can render against a
  // stable instance on the very first render. Without this the subtree mounted
  // as a bare `children` while connecting, then remounted inside <Chat> once
  // connectUser resolved (~0.8s later) because the children's parent element
  // changed — re-running every mount effect (refetches, Stream listeners,
  // page-view events) and resetting transient state. This client is injected
  // into the service below, so connectUser connects this exact instance.
  //
  // Stored in a ref (not useMemo) so its identity is *guaranteed* stable for the
  // apiKey's lifetime. useMemo is unsafe here: React may discard a memo and
  // recompute it, which would mint a new client, re-run the service-init effect,
  // and tear down + rebuild the live connection — the very remount this fix
  // exists to prevent. Both effects below key on `chatClient`, so it must not
  // depend on memo retention. One client per provider mount (keyed on apiKey) —
  // not the process-wide StreamChat.getInstance, which is shared across keys and
  // provider instances.
  const clientRef = useRef<{ apiKey: string; client: StreamChat } | null>(null)
  if (!injectedClient && apiKey && clientRef.current?.apiKey !== apiKey) {
    clientRef.current = { apiKey, client: new StreamChat(apiKey) }
  }
  // An injected client takes precedence and short-circuits the apiKey-derived
  // client, the service, and the connect/disconnect lifecycle below — the
  // caller owns that instance (offline/dev via `createMockMessagingClient`, or
  // a test). Otherwise fall back to the apiKey-derived client as before.
  const chatClient = injectedClient
    ? injectedClient
    : apiKey
      ? (clientRef.current?.client ?? null)
      : null

  // Create the service once per client lifetime (apiKey). Keyed on `chatClient`
  // — NOT on serviceConfig/debug — so those changing does not spin up a second
  // service that would re-adopt the live client and race the in-flight connect.
  // serviceConfig/debug are read from refs at creation. There is therefore a
  // single service per client, which owns the connection lifecycle and tracks
  // the active user; `user` changes reconnect through it.
  useEffect(() => {
    const currentRender = renderCountRef.current
    const currentConfig = serviceConfigRef.current

    // An injected client owns its own connection — never create a service or
    // connect for it. Reset all apiKey-path connection state (service, client,
    // isConnected, error, isLoading): while injected these are masked by the
    // context, but on a later return to apiKey mode they would leak back —
    // before the fresh service reconnects — feeding the user-connection effect
    // a stale service and MessagingShell (which gates its load on
    // `client && isConnected`) a disconnected client to queryChannels on.
    if (injectedClient) {
      serviceRef.current = null
      setService(null)
      setClient(null)
      setIsConnected(false)
      setError(null)
      setIsLoading(false)
      return
    }

    if (!apiKey || !chatClient || !currentConfig) {
      debugLog('⚠️ SERVICE INIT SKIPPED', {
        renderCount: currentRender,
        reason: 'Missing apiKey, client, or serviceConfig',
      })
      return
    }

    debugLog('🚀 CREATING SERVICE', {
      renderCount: currentRender,
      apiKey: apiKey?.substring(0, 8) + '...',
    })

    const newService = new StreamChatService({
      ...currentConfig,
      apiKey,
      debug: debugRef.current,
      client: chatClient,
      // An explicit serviceConfig.logger wins — existing injectors keep working.
      // Resolved, because the service swaps its console defaults out wholesale.
      ...(currentConfig.logger || !loggerRef.current
        ? {}
        : { logger: resolveLogger(loggerRef.current) }),
    })

    serviceRef.current = newService
    setService(newService)
    debugLog('✅ SERVICE SET', {
      renderCount: currentRender,
      serviceInstance: !!newService,
    })

    // No disconnect on cleanup here — disconnection is owned by the
    // client-cleanup effect below, keyed on `chatClient` (apiKey) and unmount.
    // eslint-disable-next-line react-hooks/exhaustive-deps -- serviceConfig/debug/debugLog are read via refs by design (see above); recreating on them would re-adopt the live client
  }, [apiKey, chatClient, injectedClient])

  // Track if we've already connected this user with this service to prevent duplicate connections
  const connectedUserRef = useRef<{
    serviceId: StreamChatService
    userId: string
  } | null>(null)

  // Connect user when service and user are available
  useEffect(() => {
    debugLog('🔗 USER CONNECTION EFFECT TRIGGERED', {
      hasService: !!service,
      hasUser: !!user,
      userId: user?.id,
      isConnecting: connectingRef.current,
      isConnected: isConnected,
      dependencies: { service: !!service, userId: user?.id },
    })

    // The injected client owns its own connection — never connect a service for
    // it. Without this guard a `user` change in injected-client mode would run
    // service.connectUser on a stale apiKey-path service (the toggle case).
    if (injectedClient) {
      return
    }

    if (!service || !user) {
      debugLog('⚠️ USER CONNECTION SKIPPED', 'Missing service or user')
      return
    }

    if (connectingRef.current) {
      debugLog('⚠️ USER CONNECTION SKIPPED', 'Already connecting')
      return
    }

    // Check if we've already connected this exact user with this exact service instance
    if (
      connectedUserRef.current?.serviceId === service &&
      connectedUserRef.current?.userId === user.id
    ) {
      debugLog(
        '⚠️ USER CONNECTION SKIPPED',
        'Already connected this user with this service'
      )
      return
    }

    const connectUser = async () => {
      debugLog('🚀 STARTING USER CONNECTION', { userId: user.id })
      connectingRef.current = true
      setIsLoading(true)
      setError(null)

      try {
        debugLog('📞 CALLING SERVICE.CONNECTUSER', { userId: user.id })
        const streamClient = await service.connectUser(user)
        setClient(streamClient)
        setIsConnected(true)
        connectedUserRef.current = { serviceId: service, userId: user.id } // Mark as connected
        debugLog('✅ USER CONNECTION SUCCESS', {
          userId: user.id,
          clientId: streamClient.userID,
        })
      } catch (err) {
        const errorMessage =
          err instanceof Error ? err.message : 'Connection failed'
        setError(errorMessage)
        debugLog('❌ USER CONNECTION ERROR', {
          userId: user.id,
          error: errorMessage,
        })
      } finally {
        setIsLoading(false)
        connectingRef.current = false
        debugLog('🔄 USER CONNECTION FINISHED', {
          userId: user.id,
          isConnected,
        })
      }
    }

    connectUser()
  }, [service, user, debugLog, isConnected, injectedClient])

  // Disconnect tied to the client's lifetime: only when `chatClient` changes
  // (i.e. `apiKey` changes) or the provider unmounts — never on a mere
  // serviceConfig/debug change, which reuses the same client. Uses `serviceRef`
  // so the disconnect goes through the latest service instance. Deliberately
  // keyed on `chatClient` only: depending on `debugLog` (which changes identity
  // when the `debug` prop toggles) would tear down the live client just for a
  // logging change.
  useEffect(() => {
    const client = chatClient
    return () => {
      if (!client) return
      connectedUserRef.current = null // Reset connection tracking
      serviceRef.current?.disconnectUser().catch(console.error)
    }
  }, [chatClient])

  const refreshConnection = useCallback(async () => {
    debugLog('🔄 REFRESH CONNECTION CALLED', {
      hasService: !!service,
      hasUser: !!user,
    })

    // The injected client owns its own connection lifecycle — refresh is a
    // no-op. Guarding here (not just the context fields) stops a stale
    // apiKey-path `service` from being disconnected/reconnected if the same
    // provider previously connected via apiKey and then received a client.
    if (injectedClient) {
      return
    }

    if (!service || !user) {
      debugLog('⚠️ REFRESH CONNECTION SKIPPED', 'Missing service or user')
      return
    }

    debugLog('🚀 STARTING CONNECTION REFRESH', { userId: user.id })
    setIsLoading(true)
    try {
      debugLog('🔌 DISCONNECTING FOR REFRESH')
      await service.disconnectUser()
      debugLog('📞 RECONNECTING FOR REFRESH')
      const streamClient = await service.connectUser(user)
      setClient(streamClient)
      setIsConnected(true)
      setError(null)
      debugLog('✅ CONNECTION REFRESH SUCCESS', { userId: user.id })
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Refresh failed'
      setError(errorMessage)
      debugLog('❌ CONNECTION REFRESH ERROR', {
        userId: user.id,
        error: errorMessage,
      })
    } finally {
      setIsLoading(false)
      debugLog('🔄 CONNECTION REFRESH FINISHED', { userId: user.id })
    }
  }, [service, user, debugLog, injectedClient])

  // Memoize context value to prevent unnecessary re-renders
  const contextValue: MessagingContextValue = React.useMemo(() => {
    debugLog('💫 CONTEXT VALUE MEMOIZATION', {
      hasService: !!service,
      hasClient: !!client,
      isConnected,
      isLoading,
      hasError: !!error,
      capabilitiesKeys: Object.keys(capabilities),
    })

    return {
      // With an injected client, expose no service — the injected client owns
      // its lifecycle and there is no backend to reach. Suppressing it stops
      // service-dependent paths (channel creation, block/unblock) from calling
      // a stale apiKey-path service if the same provider previously connected.
      service: injectedClient ? null : service,
      // An injected client is already connected and owns its lifecycle, so
      // surface it directly and report a clean connected state — the apiKey
      // path's `client`/`isLoading`/`error` (from connectUser) must not leak
      // through if the same provider previously attempted an apiKey connect.
      client: injectedClient ?? client,
      isConnected: injectedClient ? true : isConnected,
      isLoading: injectedClient ? false : isLoading,
      error: injectedClient ? null : error,
      capabilities,
      refreshConnection,
      debug,
    }
  }, [
    service,
    client,
    injectedClient,
    isConnected,
    isLoading,
    error,
    capabilities,
    refreshConnection,
    debug,
    debugLog,
  ])

  debugLog('🔄 RENDER END', {
    renderCount: renderCountRef.current,
    willRenderChat: !!chatClient,
    contextValueReady: !!contextValue,
  })

  return (
    <MessagingLoggerProvider logger={logger}>
      <MessagingContext.Provider value={contextValue}>
        {chatClient ? (
          <Chat
            client={chatClient}
            customClasses={{
              channelList:
                'str-chat__channel-list str-chat__channel-list-react bg-transparent lg:border-r-2 border-r-0 border-[#0000000A]',
            }}
          >
            {children}
          </Chat>
        ) : (
          children
        )}
      </MessagingContext.Provider>
    </MessagingLoggerProvider>
  )
}
