import { useEffect, useState } from 'react'
import {
  ConnectingType,
  ConnectingTypeGroup,
  DisconnectingType,
  EngineCommandManagerEvents,
  EngineConnectionEvents,
  EngineConnectionStateType,
  EngineConnectionState,
  ErrorType,
  initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import { engineCommandManager } from '../lib/singletons'

export enum NetworkHealthState {
  Ok,
  Weak,
  Issue,
  Disconnected,
}

export interface NetworkStatus {
  immediateState: EngineConnectionState
  hasIssues: boolean | undefined
  overallState: NetworkHealthState
  internetConnected: boolean
  steps: typeof initialConnectingTypeGroupState
  issues: Record<ConnectingTypeGroup, boolean | undefined>
  error: ErrorType | undefined
  setHasCopied: (b: boolean) => void
  hasCopied: boolean
  pingPongHealth: undefined | 'OK' | 'TIMEOUT'
}

// Must be called from one place in the application.
// We've chosen the <Router /> component for this.
export function useNetworkStatus() {
  const [immediateState, setImmediateState] = useState<EngineConnectionState>({
    type: EngineConnectionStateType.Disconnected,
  })
  const [steps, setSteps] = useState(
    structuredClone(initialConnectingTypeGroupState)
  )
  const [internetConnected, setInternetConnected] = useState<boolean>(true)
  const [overallState, setOverallState] = useState<NetworkHealthState>(
    NetworkHealthState.Disconnected
  )
  const [pingPongHealth, setPingPongHealth] = useState<
    undefined | 'OK' | 'TIMEOUT'
  >(undefined)
  const [hasCopied, setHasCopied] = useState<boolean>(false)

  const [error, setError] = useState<ErrorType | undefined>(undefined)

  const hasIssue = (i: [ConnectingType, boolean | undefined]) =>
    i[1] === undefined ? i[1] : !i[1]

  const [issues, setIssues] = useState<
    Record<ConnectingTypeGroup, boolean | undefined>
  >({
    [ConnectingTypeGroup.WebSocket]: undefined,
    [ConnectingTypeGroup.ICE]: undefined,
    [ConnectingTypeGroup.WebRTC]: undefined,
  })

  const [hasIssues, setHasIssues] = useState<boolean | undefined>(undefined)
  useEffect(() => {
    setOverallState(
      !internetConnected
        ? NetworkHealthState.Disconnected
        : hasIssues || hasIssues === undefined
        ? NetworkHealthState.Issue
        : pingPongHealth === 'TIMEOUT'
        ? NetworkHealthState.Weak
        : NetworkHealthState.Ok
    )
  }, [hasIssues, internetConnected, pingPongHealth])

  useEffect(() => {
    const onlineCallback = () => {
      setInternetConnected(true)
    }
    const offlineCallback = () => {
      setInternetConnected(false)
      setSteps(structuredClone(initialConnectingTypeGroupState))
    }
    window.addEventListener('online', onlineCallback)
    window.addEventListener('offline', offlineCallback)
    return () => {
      window.removeEventListener('online', onlineCallback)
      window.removeEventListener('offline', offlineCallback)
    }
  }, [])

  useEffect(() => {
    const issues = {
      [ConnectingTypeGroup.WebSocket]: steps[
        ConnectingTypeGroup.WebSocket
      ].reduce(
        (acc: boolean | undefined, a) =>
          acc === true || acc === undefined ? acc : hasIssue(a),
        false
      ),
      [ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].reduce(
        (acc: boolean | undefined, a) =>
          acc === true || acc === undefined ? acc : hasIssue(a),
        false
      ),
      [ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].reduce(
        (acc: boolean | undefined, a) =>
          acc === true || acc === undefined ? acc : hasIssue(a),
        false
      ),
    }
    setIssues(issues)
  }, [steps])

  useEffect(() => {
    setHasIssues(
      issues[ConnectingTypeGroup.WebSocket] ||
        issues[ConnectingTypeGroup.ICE] ||
        issues[ConnectingTypeGroup.WebRTC]
    )
  }, [issues])

  useEffect(() => {
    const onPingPongChange = ({ detail: state }: CustomEvent) => {
      setPingPongHealth(state)
    }

    const onConnectionStateChange = ({
      detail: engineConnectionState,
    }: CustomEvent) => {
      setImmediateState(engineConnectionState)
      setSteps((steps) => {
        let nextSteps = structuredClone(steps)

        if (
          engineConnectionState.type === EngineConnectionStateType.Connecting
        ) {
          const groups = Object.values(nextSteps)
          for (let group of groups) {
            for (let step of group) {
              if (step[0] !== engineConnectionState.value.type) continue
              step[1] = true
            }
          }
        }

        if (
          engineConnectionState.type === EngineConnectionStateType.Disconnecting
        ) {
          const groups = Object.values(nextSteps)
          for (let group of groups) {
            for (let step of group) {
              if (
                engineConnectionState.value.type === DisconnectingType.Error
              ) {
                if (
                  engineConnectionState.value.value.lastConnectingValue
                    ?.type === step[0]
                ) {
                  step[1] = false
                }
              }
            }

            if (engineConnectionState.value.type === DisconnectingType.Error) {
              setError(engineConnectionState.value.value)
            }
          }
        }

        // Reset the state of all steps if we have disconnected.
        if (
          engineConnectionState.type === EngineConnectionStateType.Disconnected
        ) {
          return structuredClone(initialConnectingTypeGroupState)
        }

        return nextSteps
      })
    }

    const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
      engineConnection.addEventListener(
        EngineConnectionEvents.PingPongChanged,
        onPingPongChange as EventListener
      )
      engineConnection.addEventListener(
        EngineConnectionEvents.ConnectionStateChanged,
        onConnectionStateChange as EventListener
      )

      // Tell EngineConnection to start firing events.
      window.dispatchEvent(new CustomEvent('use-network-status-ready', {}))
    }

    engineCommandManager.addEventListener(
      EngineCommandManagerEvents.EngineAvailable,
      onEngineAvailable as EventListener
    )

    return () => {
      engineCommandManager.removeEventListener(
        EngineCommandManagerEvents.EngineAvailable,
        onEngineAvailable as EventListener
      )

      // When the component is unmounted these should be assigned, but it's possible
      // the component mounts and unmounts before engine is available.
      engineCommandManager.engineConnection?.addEventListener(
        EngineConnectionEvents.PingPongChanged,
        onPingPongChange as EventListener
      )
      engineCommandManager.engineConnection?.addEventListener(
        EngineConnectionEvents.ConnectionStateChanged,
        onConnectionStateChange as EventListener
      )
    }
  }, [])

  return {
    immediateState,
    hasIssues,
    overallState,
    internetConnected,
    steps,
    issues,
    error,
    setHasCopied,
    hasCopied,
    pingPongHealth,
  }
}
