import React, { ChangeEvent, useEffect, useState } from 'react'
import { CheckInfo, ConnectionCheck } from './connectionHelper/ConnectionCheck'
import { CheckStatus, LogLevel, LogMessage } from './connectionHelper/checks/Checker'
import {
  ApiTokenResolverChain,
  Auth,
  createTokenProviderConfigurationDefaults,
  MediaClient,
  BaseApiTokenProvider,
} from '@zocks-communications/client-sdk-js'
import {
  CandidateTableProps,
  CheckResultProps,
  ConnectionTesterParams,
  ExportResultButtonProps,
  MediaDeviceTableProps,
  StartTestButtonProps,
} from './models'
import { setLogLevel, LogLevel as LiveKitLogLevel } from 'livekit-client'
import { logger } from './console'
import { getClassName, getClassNames, getMergedClassNames } from '../common'
import zocksStyle from './ConnectionTester.module.scss'
import { RoomJoinSelectOption } from '../JoinPage'

const defaultRoomApiBaseUrl = 'https://api.zocks.io/v1'

type LogFormatters = {
  [key: string]: (log: LogMessage) => JSX.Element | string
}

export const CandidateTable = ({ candidates }: CandidateTableProps) => {
  function formatPriority(priority: number): string {
    if (!priority) {
      return ''
    }
    return [priority >> 24, (priority >> 8) & 0xffff, priority & 0xff].join(' | ')
  }

  return (
    <table className={getClassName('log-message-table', zocksStyle)}>
      <tbody>
        <tr>
          <td>Time</td>
          <td>Type</td>
          <td>Foundation</td>
          <td>Protocol</td>
          <td>Address</td>
          <td>Port</td>
          <td>Priority</td>
          <td>URL</td>
          <td>Relay Protocol</td>
        </tr>
        {candidates.map((candidate) => {
          return (
            <tr key={`${candidate.elapsed}-${candidate.candidate.type}-${candidate.candidate.address}`}>
              <td>{candidate.elapsed}</td>
              <td>{candidate.candidate.type}</td>
              <td>{candidate.candidate.foundation}</td>
              <td>{candidate.candidate.protocol}</td>
              <td>{candidate.candidate.address}</td>
              <td>{candidate.candidate.port}</td>
              <td>{formatPriority(candidate.candidate.priority)}</td>
              <td>{candidate.candidate.url}</td>
              <td>{candidate.candidate.relayProtocol}</td>
            </tr>
          )
        })}
      </tbody>
    </table>
  )
}

export const MediaDeviceTable = ({ devices }: MediaDeviceTableProps) => {
  return (
    <table className={getClassName('log-message-table', zocksStyle)}>
      <tbody>
        <tr>
          <td>Kind</td>
          <td>Label</td>
        </tr>
        {devices.map((device, index) => {
          return (
            <tr key={`${index}-${device.deviceId}`}>
              <td>{device.kind}</td>
              <td>{device.label}</td>
            </tr>
          )
        })}
      </tbody>
    </table>
  )
}

export const ConnectionTester: React.FC<ConnectionTesterParams> = ({
  apiBaseUrl,
  apiTokenProvider,
  apiTokenKeyName,
  roomId,
  roomName = 'connection-tester',
  components,
  styles,
}) => {
  const [roomInputValue, setRoomNameInput] = useState(roomId ? roomId : roomName)
  const [roomInputSelectValue, setRoomInputSelectValue] = useState<RoomJoinSelectOption>(roomId ? 'id' : 'name')
  const [tokenSelectValue, setTokenSelectValue] = useState('my')
  const [tokenInputValue, setTokenInputValue] = useState('')
  const [isTokenError, setIsTokenError] = useState(false)
  const [testsInProgress, setTestsInProgress] = useState(false)
  const [exportInProgress, setExportInProgress] = useState(false)
  const [exportButtonVisible, setExportButtonVisible] = useState(false)
  const [exportSuccess, setExportSuccess] = useState<boolean | null>(null)
  const [errors, setErrors] = useState<string[]>([])
  const [checkResults, setCheckResults] = useState<Record<string, CheckInfo>>({})
  const [consoleLogger] = useState<any>(logger())
  const [originalConsole] = useState<Console>(window.console)
  const mergedStyles = getMergedClassNames(zocksStyle, styles)

  const [tokenProviderDefaults] = useState(createTokenProviderConfigurationDefaults({ apiTokenKeyName }))

  useEffect(() => {
    const apiTokenResolverChain = new ApiTokenResolverChain(apiTokenProvider ?? tokenProviderDefaults.apiTokenProvider)
    setTokenInputValue(apiTokenResolverChain.resolve() ?? '')
  }, [apiTokenProvider, tokenProviderDefaults])

  class ApiTokenProvider extends BaseApiTokenProvider {
    constructor(config: any = {}) {
      super(config)
    }

    getApiToken() {
      return tokenInputValue
    }
  }

  const checkToken = () => {
    if (!tokenInputValue) {
      throw Error('Token is empty.')
    }
  }
  const startTest = async () => {
    setExportButtonVisible(false)
    setTestsInProgress(true)
    setExportSuccess(null)
    setCheckResults({})
    setErrors([])
    setIsTokenError(false)
    window.console = consoleLogger
    console.clear()
    setLogLevel(LiveKitLogLevel.trace)

    let connectionChecker = null

    try {
      const apiConfig = {
        apiBaseUrl: apiBaseUrl ?? defaultRoomApiBaseUrl,
        apiTokenProvider: [new ApiTokenProvider()],
      }
      let parsedAuthToken = null

      try {
        checkToken()
        parsedAuthToken = Auth.parseJwt(tokenInputValue)
      } catch (e) {
        console.error('Invalid token error: ', e)
        console.log('Token input value: ', tokenInputValue)
        setIsTokenError(true)
        return
      }

      console.log('Token: ', parsedAuthToken)
      console.log('Test room: ', roomInputValue)
      const mediaClient = new MediaClient({ ...apiConfig, accountId: parsedAuthToken.account_id })
      const joinResponse = await mediaClient.joinRoom({
        autoCreate: roomInputSelectValue === 'name',
        ...(roomInputSelectValue === 'id' && { id: roomInputValue }),
        ...(roomInputSelectValue === 'name' && { name: roomInputValue }),
        config: { destroyPolicy: { type: 'manual' } },
      })
      const { token, webSocketUrl, turnServers } = joinResponse.joinInfo
      connectionChecker = new ConnectionCheck(webSocketUrl, token)

      connectionChecker.on('checkUpdate', (checkId, info) => {
        setCheckResults((prevState) => ({
          ...prevState,
          [`${info.name}`]: info,
        }))
      })

      const rtcIceServers: RTCIceServer[] = []

      turnServers.forEach((iceServer) => {
        const rtcIceServer: RTCIceServer = {
          urls: iceServer.urls,
        }
        if (iceServer.username) rtcIceServer.username = iceServer.username
        if (iceServer.password) {
          rtcIceServer.credential = iceServer.password
        }
        rtcIceServers.push(rtcIceServer)
      })

      const checkMethods = [
        'checkMediaDevices',
        'checkWebsocket',
        'checkWebRTC',
        'checkTURN',
        'checkTrickle',
        'checkPublishVideo',
        'checkPublishAudio',
      ]

      for (const method of checkMethods) {
        if (method in connectionChecker) {
          console.log(`Check ${method} started`)
          //@ts-ignore
          await connectionChecker[method as keyof typeof connectionChecker].apply(connectionChecker, [rtcIceServers])
          console.log(`Check ${method} finished`)
        }
      }
    } catch (e: any) {
      if (e.errors) {
        const apiErrors: string[] = []
        e.errors.forEach((error: any) => {
          apiErrors.push(error.message)
        })
        setErrors(apiErrors)
      }
      console.error(e)
    } finally {
      setTestsInProgress(false)
      setExportButtonVisible(true)
      connectionChecker?.off('checkUpdate', () => {})
      console.log(connectionChecker?.getResults())
      window.console = originalConsole
    }
  }

  const exportResult = async () => {
    setExportInProgress(true)
    window.console = consoleLogger

    try {
      // @ts-ignore
      const blob = new Blob([console.dump()], { type: 'text/csv' })

      const elem = window.document.createElement('a')
      elem.href = window.URL.createObjectURL(blob)
      const date = new Date()
      elem.download = `zocks-connection-tester-result-export-${date.toISOString().slice(0, 16)}.json`
      document.body.appendChild(elem)
      elem.click()
      document.body.removeChild(elem)
      setExportSuccess(true)
    } catch (e) {
      console.error(e)
      setExportSuccess(false)
    } finally {
      setExportInProgress(false)
    }
    window.console = originalConsole
  }

  const updateRoomInputValue = (roomName: string) => {
    setRoomNameInput(roomName)
  }

  const onRoomJoinSelectChange = (evt: ChangeEvent<HTMLSelectElement>) => {
    setRoomInputSelectValue(evt.target.value as RoomJoinSelectOption)
  }

  const updateTokenInputValue = (token: string) => {
    setTokenInputValue(token)
  }

  const tokenTypeChanged = (type: string) => {
    setTokenSelectValue(type)
    if (type === 'my') {
      setIsTokenError(false)
      resetToken()
    }
    if (type === 'other') {
      setTokenInputValue('')
      setIsTokenError(false)
    }
  }

  const resetToken = () => {
    const apiTokenResolverChain = new ApiTokenResolverChain(apiTokenProvider ?? tokenProviderDefaults.apiTokenProvider)
    setTokenInputValue(apiTokenResolverChain.resolve() ?? '')
  }

  const StartTestButtonElement = components?.StartTestButton ?? StartTestButton
  const ExportResultButtonElement = components?.ExportResultButton ?? ExportResultButton
  return (
    <div className={getClassName('connection-tester__container', mergedStyles)}>
      <header className={getClassName('connection-tester__header', mergedStyles)}>
        <h1>Zocks Connection Tester</h1>
      </header>

      <div className={getClassNames(['connection-tester__controls', 'card'], mergedStyles)}>
        <div className={getClassName('connection-tester__room-info-container', mergedStyles)}>
          <label htmlFor='room'>Test room</label>
          <div className={getClassName('connection-tester__room-input-container', mergedStyles)}>
            <input id='room' value={roomInputValue} onChange={(e) => updateRoomInputValue(e.target.value)} />
            <select
              aria-label='Room type'
              onChange={onRoomJoinSelectChange}
              className={getClassName('connection-tester__room-type-select', mergedStyles)}
              value={roomInputSelectValue}
            >
              <option value='name'>Name</option>
              <option value='id'>Id</option>
            </select>
          </div>
          <label htmlFor='token'>Token</label>
          <div className={getClassName('token-radio-buttons', mergedStyles)}>
            <div>
              <input
                type='radio'
                id='my-token'
                name='token-type'
                value='my'
                defaultChecked={true}
                onChange={() => tokenTypeChanged('my')}
              />
              <label htmlFor='my-token'>Use my token</label>
            </div>
            <div>
              <input
                type='radio'
                id='other-token'
                name='token-type'
                value='other'
                onChange={() => tokenTypeChanged('other')}
              />
              <label htmlFor='other-token'>Use other token</label>
            </div>
          </div>
          <div className={getClassName('connection-tester__token-input-container', mergedStyles)}>
            {tokenSelectValue === 'other' && (
              <div className={getClassName('connection-tester__token-input', mergedStyles)}>
                <input
                  data-testid='token'
                  placeholder={'Enter user token'}
                  id='token'
                  value={tokenInputValue}
                  onChange={(e) => updateTokenInputValue(e.target.value)}
                />
              </div>
            )}
          </div>

          {isTokenError && (
            <div className={getClassName('connection-tester__errors', mergedStyles)}>Invalid token.</div>
          )}
        </div>
        <div className={getClassName('connection-tester__control-buttons', mergedStyles)}>
          <StartTestButtonElement startTest={startTest} testsInProgress={testsInProgress} />
          {exportButtonVisible && (
            <ExportResultButtonElement
              exportResult={exportResult}
              exportInProgress={exportInProgress}
              exportSuccess={exportSuccess}
            />
          )}
        </div>
      </div>
      <div className={getClassNames(['connection-tester__result-container', 'card'], mergedStyles)}>
        <div className={getClassName('connection-tester__errors', mergedStyles)}>
          {errors.map((error) => {
            return <div key={error}>{error}</div>
          })}
        </div>
        {!errors.length && (
          <div>
            <div>
              <strong>User Agent:</strong> {navigator.userAgent}
            </div>
          </div>
        )}
        <div className={getClassName('connection-tester__check-result-container', mergedStyles)}>
          {Object.entries(checkResults).map(([key, value]) => {
            return <CheckResult key={key} checkInfo={value} />
          })}
        </div>
        <div className={getClassName('connection-tester__check-result-summary', mergedStyles)}>
          {Object.keys(checkResults).length > 0 &&
            Object.values(checkResults).every((checkInfo) => checkInfo.status === CheckStatus.SUCCESS) && (
              <div className={getClassName('connection-tester__check-result-summary--success', mergedStyles)}>
                All connection checks finished successfully.
              </div>
            )}
        </div>
      </div>
    </div>
  )
}

const StartTestButton: React.FC<StartTestButtonProps> = ({ startTest, testsInProgress, styles }) => {
  const mergedStyles = getMergedClassNames(zocksStyle, styles)
  return (
    <button
      onClick={startTest}
      disabled={testsInProgress}
      className={getClassName('connection-tester__start-test-button', mergedStyles)}
    >
      Start Test
    </button>
  )
}

const ExportResultButton: React.FC<ExportResultButtonProps> = ({
  exportResult,
  exportInProgress,
  exportSuccess,
  styles,
}) => {
  const mergedStyles = getMergedClassNames(zocksStyle, styles)
  return (
    <button
      onClick={exportResult}
      disabled={exportInProgress}
      className={getClassName('connection-tester__export-results-button', mergedStyles)}
    >
      Export Result
      {exportSuccess && <i className={`icon-check-mark ${getClassName('icon-success', mergedStyles)}`}></i>}
      {exportSuccess === false && <i className={`icon-close ${getClassName('icon-error', mergedStyles)}`}></i>}
    </button>
  )
}

const CheckResult: React.FC<CheckResultProps> = ({ checkInfo, components }) => {
  const isSimpleLogMessage = (log: LogMessage) => {
    return log && 'level' in log && 'message' in log && typeof log.message === 'string'
  }

  const isCandidateList = (log: LogMessage) => {
    return log && log.message.length > 0 && 'elapsed' in log.message[0] && 'candidate' in log.message[0]
  }

  const isMediaDeviceList = (log: LogMessage) => {
    return log && log.message.length > 0
  }

  const MediaDeviceTableElement = components?.MediaDeviceTable ?? MediaDeviceTable
  const CandidateTableElement = components?.CandidateTable ?? CandidateTable
  const formatters: LogFormatters = {
    TrickleCheck: (log: LogMessage) => {
      if (isSimpleLogMessage(log)) {
        return (
          <div className={getClassName('log-message', zocksStyle)}>
            <LogMessageEl log={log} />
          </div>
        )
      }

      if (isCandidateList(log)) {
        return <CandidateTableElement candidates={log.message} />
      }
      return ''
    },
    MediaDevicesCheck: (log: LogMessage) => {
      if (isSimpleLogMessage(log)) {
        return (
          <div className={getClassName('log-message', zocksStyle)}>
            <LogMessageEl log={log} />
          </div>
        )
      }
      if (isMediaDeviceList(log)) {
        return <MediaDeviceTableElement devices={[...log.message]} />
      }

      return ''
    },
  }

  return (
    <div key={checkInfo.name}>
      <div>
        {checkInfo.status === CheckStatus.RUNNING && <div className={getClassName('loader', zocksStyle)}></div>}
        <span
          className={
            checkInfo.status === CheckStatus.SUCCESS
              ? getClassName('check-info-success', zocksStyle)
              : checkInfo.status === CheckStatus.FAILED
              ? getClassName('check-info-failed', zocksStyle)
              : getClassName('check-info-normal', zocksStyle)
          }
        >
          {checkInfo.description}
        </span>
      </div>
      {checkInfo.logs.map((log: LogMessage, index) => {
        return checkInfo.name in formatters && formatters[checkInfo.name as keyof LogFormatters] ? (
          <div key={index}> {formatters[checkInfo.name as keyof LogFormatters](log)}</div>
        ) : (
          <div key={index} className={getClassName('log-message', zocksStyle)}>
            <LogMessageEl log={log} />
          </div>
        )
      })}
    </div>
  )
}

interface LogMessageProps {
  log: LogMessage
}

const LogMessageEl: React.FC<LogMessageProps> = ({ log }) => {
  let prefix = ''
  let className = 'log-message-info'
  if (log.level === LogLevel.WARNING) {
    prefix = 'WARNING:'
    className = 'log-message-warning'
  }
  if (log.level === LogLevel.ERROR) {
    prefix = 'ERROR:'
    className = 'log-message-error'
  }
  return (
    <span className={getClassName(className, zocksStyle)}>
      {prefix} {log.message}
    </span>
  )
}
