'use client'

import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useWebRTC } from './WebRTCProvider'
import {
  browserName,
  browserVersion,
  osName,
  osVersion,
  mobileVendor,
  mobileModel,
} from 'react-device-detect'
import * as t from 'io-ts'
import { ChunkMessage, decodeMessage, Message, MessageType } from '../messages'
import { createZipStream } from '../zip-stream'
import { DataConnection } from 'peerjs'
import PasswordField from './PasswordField'
import UnlockButton from './UnlockButton'
import Loading from './Loading'
import UploadFileList from './UploadFileList'
import DownloadButton from './DownloadButton'
import StopButton from './StopButton'
import ProgressBar from './ProgressBar'

const baseURL = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'

// eslint-disable-next-line @typescript-eslint/no-var-requires
if (typeof window !== 'undefined') require('web-streams-polyfill/ponyfill')

// eslint-disable-next-line @typescript-eslint/no-var-requires
const streamSaver =
  typeof window !== 'undefined' ? require('streamsaver') : null
if (typeof window !== 'undefined') {
  streamSaver.mitm = baseURL + '/stream.html'
}

function getZipFilename(): string {
  return `filepizza-download-${Date.now()}.zip`
}

function cleanErrorMessage(errorMessage: string): string {
  if (errorMessage.startsWith('Could not connect to peer')) {
    return 'Could not connect to the uploader. Did they close their browser?'
  } else {
    return errorMessage
  }
}

type DownloadFileStream = {
  name: string
  size: number
  stream: () => ReadableStream
}

export async function streamDownloadSingleFile(
  file: DownloadFileStream,
): Promise<void> {
  const fileStream = streamSaver.createWriteStream(file.name, {
    size: file.size,
  })

  const writer = fileStream.getWriter()
  const reader = file.stream().getReader()

  const pump = async () => {
    const res = await reader.read()
    return res.done ? writer.close() : writer.write(res.value).then(pump)
  }
  await pump()
}

export function streamDownloadMultipleFiles(
  files: Array<DownloadFileStream>,
): Promise<void> {
  const filename = getZipFilename()
  const totalSize = files.reduce((acc, file) => acc + file.size, 0)
  const fileStream = streamSaver.createWriteStream(filename, {
    size: totalSize,
  })

  const readableZipStream = createZipStream({
    start(ctrl) {
      for (const file of files) {
        ctrl.enqueue(file)
      }
      ctrl.close()
    },
    async pull(_ctrl) {
      // Gets executed everytime zip-stream asks for more data
    },
  })

  return readableZipStream.pipeTo(fileStream)
}

export default function Downloader({
  uploaderPeerID,
}: {
  uploaderPeerID: string
}): JSX.Element {
  const peer = useWebRTC()

  const [password, setPassword] = useState('')
  const [dataConnection, setDataConnection] = useState<DataConnection | null>(
    null,
  )
  const [filesInfo, setFilesInfo] = useState<Array<{
    fullPath: string
    size: number
    type: string
  }> | null>(null)
  const processChunk = useRef<
    ((message: t.TypeOf<typeof ChunkMessage>) => void) | null
  >(null)
  const [shouldAttemptConnection, setShouldAttemptConnection] = useState(false)
  const [open, setOpen] = useState(false)
  const [downloading, setDownloading] = useState(false)
  const [bytesDownloaded, setBytesDownloaded] = useState(0)
  const [done, setDone] = useState(false)
  const [errorMessage, setErrorMessage] = useState<string | null>(null)

  useEffect(() => {
    if (!shouldAttemptConnection) {
      return
    }

    const conn = peer.connect(uploaderPeerID, {
      reliable: true,
    })

    setDataConnection(conn)

    const handleOpen = () => {
      setOpen(true)

      const request: t.TypeOf<typeof Message> = {
        type: MessageType.RequestInfo,
        browserName: browserName,
        browserVersion: browserVersion,
        osName: osName,
        osVersion: osVersion,
        mobileVendor: mobileVendor,
        mobileModel: mobileModel,
        password,
      }

      conn.send(request)
    }

    const handleData = (data: unknown) => {
      try {
        const message = decodeMessage(data)
        switch (message.type) {
          case MessageType.Info:
            console.log(message.files)
            setFilesInfo(message.files)
            break

          case MessageType.Chunk:
            if (processChunk.current) processChunk.current(message)
            break

          case MessageType.Error:
            console.error(message.error)
            setErrorMessage(message.error)
            conn.close()
            break
        }
      } catch (err) {
        console.error(err)
      }
    }

    const handleClose = () => {
      setDataConnection(null)
      setOpen(false)
      setDownloading(false)
      setShouldAttemptConnection(false)
    }

    const handlePeerError = (err: Error) => {
      console.error(err)
      setErrorMessage(cleanErrorMessage(err.message))
      if (conn.open) {
        conn.close()
      } else {
        handleClose()
      }
    }

    const handleConnectionError = (err: Error) => {
      console.error(err)
      setErrorMessage(cleanErrorMessage(err.message))
      if (conn.open) conn.close()
    }

    conn.on('open', handleOpen)
    conn.on('data', handleData)
    conn.on('error', handleConnectionError)
    conn.on('close', handleClose)
    peer.on('error', handlePeerError)

    return () => {
      if (conn.open) conn.close()
      conn.off('open', handleOpen)
      conn.off('data', handleData)
      conn.off('error', handleConnectionError)
      conn.off('close', handleClose)
      peer.off('error', handlePeerError)
    }
  }, [peer, password, shouldAttemptConnection])

  const handleSubmitPassword = useCallback((ev) => {
    ev.preventDefault()
    setShouldAttemptConnection(true)
  }, [])

  const handleStartDownload = useCallback(() => {
    setDownloading(true)

    const fileStreamByPath: Record<
      string,
      {
        stream: ReadableStream
        enqueue: (chunk: any) => void
        close: () => void
      }
    > = {}
    const fileStreams = filesInfo.map((info) => {
      let enqueue: ((chunk: any) => void) | null = null
      let close: (() => void) | null = null
      const stream = new ReadableStream({
        start(ctrl) {
          enqueue = (chunk: any) => ctrl.enqueue(chunk)
          close = () => ctrl.close()
        },
      })
      fileStreamByPath[info.fullPath] = {
        stream,
        enqueue,
        close,
      }
      return stream
    })

    let nextFileIndex = 0
    const startNextFileOrFinish = (): void => {
      if (nextFileIndex >= filesInfo.length) {
        return
      }

      const request: t.TypeOf<typeof Message> = {
        type: MessageType.Start,
        fullPath: filesInfo[nextFileIndex].fullPath,
        offset: 0,
      }
      dataConnection.send(request)
      nextFileIndex++
    }

    const processChunkFunc = (message: t.TypeOf<typeof ChunkMessage>): void => {
      const fileStream = fileStreamByPath[message.fullPath]
      if (!fileStream) {
        console.error('no stream found for ' + message.fullPath)
        return
      }

      setBytesDownloaded((bd) => bd + (message.bytes as ArrayBuffer).byteLength)
      const uInt8 = new Uint8Array(message.bytes as ArrayBuffer)
      fileStream.enqueue(uInt8)
      if (message.final) {
        fileStream.close()
        startNextFileOrFinish()
      }
    }
    processChunk.current = processChunkFunc

    const downloads = filesInfo.map((info, i) => ({
      name: info.fullPath.replace(/^\//, ''),
      size: info.size,
      stream: () => fileStreams[i],
    }))

    let downloadPromise: Promise<void> | null = null
    if (downloads.length > 1) {
      downloadPromise = streamDownloadMultipleFiles(downloads)
    } else if (downloads.length === 1) {
      downloadPromise = streamDownloadSingleFile(downloads[0])
    } else {
      throw new Error('no files to download')
    }

    downloadPromise
      .then(() => {
        const request: t.TypeOf<typeof Message> = {
          type: MessageType.Done,
        }
        dataConnection.send(request)
        setDone(true)
      })
      .catch((err) => {
        console.error(err)
      })

    startNextFileOrFinish()
  }, [dataConnection, filesInfo])

  const handleStopDownload = useCallback(() => {
    // TODO(@kern): Implement me
  }, [])

  const totalSize = filesInfo
    ? filesInfo.reduce((acc, info) => acc + info.size, 0)
    : 0

  if (done && filesInfo) {
    return (
      <div className="flex flex-col space-y-5 w-full">
        <p className="text-description">
          You downloaded {filesInfo.length} files.
        </p>
        <UploadFileList files={filesInfo} />
        <div className="w-full">
          <ProgressBar value={bytesDownloaded} max={totalSize} />
        </div>
      </div>
    )
  }

  if (downloading && filesInfo) {
    return (
      <div className="flex flex-col space-y-5 w-full">
        <p className="text-description">
          You are about to start downloading {filesInfo.length} files.
        </p>
        <UploadFileList files={filesInfo} />
        <div className="w-full">
          <ProgressBar value={bytesDownloaded} max={totalSize} />
        </div>
        <StopButton onClick={handleStopDownload} isDownloading />
      </div>
    )
  }

  if (open && filesInfo) {
    return (
      <div className="flex flex-col space-y-5 w-full">
        <p className="text-description">
          You are about to start downloading {filesInfo.length} files.
        </p>
        <UploadFileList files={filesInfo} />
        <DownloadButton onClick={handleStartDownload} />
      </div>
    )
  }

  if (open) {
    return <Loading text="Listing uploaded files" />
  }

  // TODO(@kern): Connect immediately, then have server respond if password is needed.
  if (shouldAttemptConnection) {
    return <Loading text="Connecting to uploader" />
  }

  return (
    <form
      action="#"
      method="post"
      onSubmit={handleSubmitPassword}
      className="w-full"
    >
      <div className="flex flex-col space-y-5 w-full">
        {errorMessage ? (
          <p className="text-description-error">{errorMessage}</p>
        ) : (
          <p className="text-description">This download requires a password.</p>
        )}
        <PasswordField
          value={password}
          onChange={setPassword}
          isRequired
          isInvalid={Boolean(errorMessage)}
        />
        <UnlockButton onClick={handleSubmitPassword} />
      </div>
    </form>
  )
}
