const CHUNK_SIZE = 1_048_576 // 1 MiB
const PING_INTERVAL = 30_000

export default class KoreaiTransport {
  constructor (id, name, url) {
    const [language, country] = document.documentElement.lang.toLowerCase().split('-')
    this.socket = null
    this.history = []
    this.pingInterval = null
    this.jwtUrl = new URL(url)
    this.botInfo = {
      chatBot: name,
      taskBotId: id,
      customData: { botlanguage: language, countryCode: country }
    }
  }

  async connect (onStateChange, params = '') {
    // Get the websocket URL.
    const res = await fetch('https://de-bots.kore.ai/api/rtm/start', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `Bearer ${await this._getAccessToken()}`
      },
      body: JSON.stringify({
        botInfo: this.botInfo,
        language: this.botInfo.customData.botlanguage,
        bot_title: this.botInfo.chatBot
      })
    })
    const { url } = await res.json()

    // Create web socket connection.
    this.socket = new WebSocket(url + params)

    // Reconnect on close.
    this.socket.addEventListener('close', () => {
      onStateChange()
      clearInterval(this.pingInterval)
      this.connect(onStateChange, '&isReconnect=true&ConnectionMode=Reconnect')
    })

    this.socket.addEventListener('error', event => {
      console.error('WebSocket error:', event)
    })

    // Handle incoming messages.
    this.socket.addEventListener('message', event => {
      const data = JSON.parse(event.data)
      this.history.push(data)
      onStateChange()
    })

    // Handle ping.
    clearInterval(this.pingInterval)
    this.pingInterval ||= setInterval(() => this.sendMessage({ type: 'ping' }), PING_INTERVAL)
  }

  async disconnect () {
    return this.sendMessage({
      customEvent: { web_sdk: 'reconnect_button_event' },
      resourceid: '/bot.clientEvent',
      message: { body: 'reconnect_button_event', type: 'text' },
    })
  }

  async sendMessage (message) {
    if (!this.isOpen()) {
      return
    }

    const date = new Date()

    const data = {
      clientMessageId: date.getTime(),
      resourceid: '/bot.message',
      botInfo: this.botInfo,
      client: 'sdk',
      meta: {
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
        locale: document.documentElement.lang // Use site locale rather than browser locale.
      },
      id: date.getTime(),
      ...message
    }

    this.history.push(data)
    this.socket.send(JSON.stringify(data))
  }

  async sendFile (file) {
    const chunks = await this._getBase64Chunks(file)
    const fileToken = await this._getFileToken()
    const messageToken = await this._getFileToken()
    const boundary = `--------MultipartData${this._getRandomToken()}`

    // Send chunk requests.
    for (const [ii, chunk] of chunks.entries()) {
      const formData = new FormData()
      formData.append('chunkNo', ii)
      formData.append('messageToken', messageToken)
      formData.append('chunk', chunk)
      await fetch(`https://de-bots.kore.ai/api/1.1/attachment/file/${fileToken}/chunk`, {
        method: 'POST',
        headers: {
          authorization: `Bearer ${await this._getAccessToken()}`,
          'content-type': `multipart/form-data; boundary=${boundary}`
        },
        body: this._getCustomMultipartBody(boundary, { chunkNo: ii, messageToken, chunk })
      })
    }

    // Send completion request.
    const res = await fetch(`https://de-bots.kore.ai/api/1.1/attachment/file/${fileToken}`, {
      method: 'PUT',
      headers: {
        accept: '*/*',
        authorization: `Bearer ${await this._getAccessToken()}`,
        'content-type': `multipart/form-data; boundary=${boundary}`
      },
      body: this._getCustomMultipartBody(boundary, {
        totalChunks: chunks.length,
        messageToken,
        fileExtension: file.name.split('.').pop(),
        fileContext: 'workflows',
        thumbnailUpload: false,
        filename: file.name
      })
    })

    const { fileId } = await res.json()

    await this.sendMessage({
      message: {
        body: file.name,
        attachments: [{ fileName: file.name, fileType: file.type, fileId }]
      }
    })
  }

  isOpen () {
    return this?.socket?.readyState === WebSocket.OPEN
  }

  async _getBase64Chunks (file) {
    const chunks = []
    for (let ii = 0; ii < file.size; ii += CHUNK_SIZE) {
      const blob = file.slice(ii, ii + CHUNK_SIZE)
      const chunk = await new Promise(resolve => {
        const reader = new FileReader()
        reader.onloadend = event => {
          resolve(event.target.result.split(',').pop())
        }
        reader.readAsDataURL(blob)
      })
      chunks.push(chunk)
    }

    return chunks
  }

  // Why the hell aren't Kore.ai following standards?
  _getCustomMultipartBody (boundary, data) {
    const lines = []
    for (const key of Object.keys(data)) {
      lines.push(`--${boundary}`)
      if (key === 'chunk') {
        lines.push(`Content-Disposition: form-data; name="${key}"; filename="${data[key].name}"`, 'Content-Transfer-Encoding: base64')
      } else {
        lines.push(`Content-Disposition: form-data; name="${key}";`)
      }
      lines.push('', data[key])
    }
    lines.push(`--${boundary}--`)
    return lines.join('\r\n')
  }

  async _getJwt () {
    const res = await fetch(`${this.jwtUrl.origin}/acinguiux-botkit/generate-token?taskBotId=${this.botInfo.taskBotId}`)
    const json = await res.json()
    return json.jwt
  }

  async _getAccessToken () {
    if (!this.accessToken) {
      const res = await fetch('https://de-bots.kore.ai/api/oAuth/token/jwtgrant', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ assertion: await this._getJwt(), botInfo: this.botInfo, token: {} })
      })
      const json = await res.json()
      this.accessToken = json.authorization.accessToken
    }

    return this.accessToken
  }

  async _getFileToken () {
    const res = await fetch('https://de-bots.kore.ai/api/1.1/attachment/file/token', {
      method: 'POST',
      headers: {
        accept: 'application/json',
        authorization: `Bearer ${await this._getAccessToken()}`
      }
    })

    const json = await res.json()
    return json.fileToken
  }

  _getRandomToken () {
    const buffer = new Uint8Array(16)
    crypto.getRandomValues(buffer)
    return Array.from(buffer, byte => byte.toString(16).padStart(2, '0')).join('')
  }
}
