// @ts-nocheck

import { Checker } from './Checker'

export type Candidate = {
  type: string
  foundation: string
  protocol: string
  address: string
  port: string
  priority: number
  url: string
  relayProtocol: string
}

export type CandidateInfo = {
  candidate: Candidate
  elapsed: string
}

export class TrickleCheck extends Checker {
  private begin = 0
  private candidates: Array<CandidateInfo> = []
  offerDescription: string = ''
  name = 'TrickleCheck'

  get description(): string {
    return 'Establishing ICE Candidate connection'
  }

  private async iceCallback(event, pc) {
    const elapsed = ((window.performance.now() - this.begin) / 1000).toFixed(3)
    if (event.candidate) {
      if (event.candidate.candidate === '') {
        return
      }

      const { candidate } = event
      let url
      if (['srflx', 'relay'].includes(candidate.type) && !candidate.url) {
        const stats = await pc.getStats()
        stats.forEach((report) => {
          if (
            !url &&
            report.type === 'local-candidate' &&
            report.address === candidate.address &&
            report.port === candidate.port
          ) {
            url = report.url
          }
        })
      }
      candidate.url = candidate.url || url || ''
      this.candidates.push({
        candidate,
        elapsed,
      })
    }
  }

  private getFinalResult() {
    let errorMessage = ''
    const elapsed = ((window.performance.now() - this.begin) / 1000).toFixed(3)
    const iceServers = this.connectOptions?.rtcConfig?.iceServers
    // if more than one server is used, it can not be determined
    // which server failed.
    if (iceServers.length === 1) {
      const server = iceServers[0]
      // get the candidates types (host, srflx, relay)
      const types = [...new Set(this.candidates.map((cand) => cand.candidate.type))]
      if (server.urls[0].indexOf('turn:') === 0 && server.urls[0].indexOf('?transport=tcp') === -1) {
        if (types.indexOf('relay') === -1) {
          if (types.indexOf('srflx') > -1) {
            // a binding response but no relay candidate suggests auth failure.
            errorMessage = 'Authentication failed?'
          } else {
            // either the TURN server is down or the clients access is blocked.
            errorMessage = 'Not reachable?'
          }
        }
      }
    }
    if (errorMessage) {
      this.appendError(errorMessage)
    } else {
      this.appendMessage(this.candidates)
      this.appendMessage(`ICECandidate connection takes ${elapsed} seconds.`)
    }
  }

  private gatheringStateChange(pc, stream) {
    if (pc.iceGatheringState !== 'complete') {
      return
    }
    pc.close()
    pc = null
    if (stream) {
      stream.getTracks().forEach((track) => track.stop())
      stream = null
    }
    this.getFinalResult()
  }

  private iceCandidateError(e: RTCPeerConnectionIceErrorEvent) {
    if (e.errorText === 'STUN server address is incompatible.') {
      this.appendWarning(`${e.errorCode} - ${e.errorText}`)
    } else if (e.errorText === 'TURN host lookup received error.') {
      this.appendWarning(`${e.errorCode} - ${e.errorText}`)
    } else {
      this.appendError(`${e.errorCode} - ${e.errorText}`)
    }
  }

  protected async perform(): Promise<void> {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
      const offerOptions = { offerToReceiveAudio: 1 }
      const pc = new RTCPeerConnection({
        iceServers: this.connectOptions?.rtcConfig?.iceServers,
        iceTransportPolicy: 'all',
      })
      pc.onicecandidate = (event) => {
        this.iceCallback(event, pc)
      }
      pc.onicegatheringstatechange = (_) => {
        this.gatheringStateChange(pc, stream)
      }
      pc.onicecandidateerror = (e) => {
        this.iceCandidateError(e)
      }
      if (stream) {
        stream.getTracks().forEach((track) => pc.addTrack(track, stream))
      }
      this.offerDescription = await pc.createOffer(offerOptions)
      this.begin = window.performance.now()
      await pc.setLocalDescription(this.offerDescription)
    } catch (err) {
      throw err
    }
  }
}
