import { configMap } from './configMap.js'
import { isJSFile } from './util.js'
import { getCSSDependenciesFor, getViteManifests } from './manifests.js'
import * as cache from './cache.js'
import { logger } from './logger.js'
import { isVersionMismatchError, NotFoundError, VersionMismatchError } from './errors.js'
import zlib from 'node:zlib'
import { promisify } from 'node:util'
import { getVersionInfo, updateVersionProcessor } from './version.js'

const gzip = promisify(zlib.gzip)
const brotliCompress = promisify(zlib.brotliCompress)

const compressFileSize = Number(process.env.COMPRESS_FILE_SIZE)
const compressionMimeTypes = (process.env.COMPRESS_FILE_TYPES || '').replace(/([.+*?^$()[\]{}|])/g, '\\$1').split(' ')
const compressionWhitelistRegex = new RegExp(`^(${compressionMimeTypes.join('|')})($|;)`, 'i')

export function createWritable (body) {
  if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(body)
  return body
}

async function createFileBuffer (response, dependencies) {
  const cssString = dependencies && dependencies.map(file => `"${file}"`).join(',')
  const appendix = cssString && `\n/*injected by ui-middleware*/document.dispatchEvent(new CustomEvent("load-css",{detail:{css:[${cssString}]}}))`
  const resBuffer = await response.arrayBuffer()
  const appendixLength = appendix?.length || 0
  const buffer = Buffer.alloc(resBuffer.byteLength + appendixLength)

  buffer.fill(Buffer.from(resBuffer), 0, resBuffer.byteLength)
  if (appendix) buffer.write(appendix, resBuffer.byteLength)

  return buffer
}

export async function fetchFileWithHeadersFromBaseUrl ({ path, baseUrl, version }) {
  const [response, dependencies] = await Promise.all([
    fetch(new URL(path, baseUrl), { cache: 'no-store' }),
    isJSFile(path) && getCSSDependenciesFor({ file: path.substr(1), version })
  ])

  if (!response.ok) {
    if (response.status === 404) logger.trace(`[Files] "${path}" could not be found on "${baseUrl}". Responded with: ${response.status}`)
    else logger.error(`[Files] Unexpected result from file retrieval "${path}" on "${baseUrl}", responded with: ${response.status}`)
    throw new NotFoundError(`Error fetching file: ${path}`, { status: response.status })
  }

  if (response.headers.get('version')) {
    const requestedVersion = (await getVersionInfo()).details[baseUrl]
    const receivedVersion = response.headers.get('version')
    if (requestedVersion !== receivedVersion) {
      throw new VersionMismatchError(`${path} does not contain the right version. Needs ${requestedVersion} but received ${receivedVersion}`)
    }
  }

  const result = {
    body: await createFileBuffer(response, dependencies),
    headers: {
      'content-type': response.headers.get('content-type'),
      dependencies
    }
  }

  if (result.body.length > compressFileSize && compressionWhitelistRegex.test(result.headers['content-type'])) {
    if (path === '/index.html') {
      result.body = await gzip(result.body)
      result.headers['content-encoding'] = 'gzip'
    } else {
      result.body = await brotliCompress(result.body)
      result.headers['content-encoding'] = 'br'
    }
  }

  return result
}

export async function fetchFileWithHeaders ({ path, version }) {
  const viteManifests = await getViteManifests({ version })

  const module = viteManifests[path.substr(1)]
  if (module?.meta?.baseUrl) {
    const baseUrl = module?.meta?.baseUrl
    try {
      return fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version })
    } catch (err) {
      logger.debug(`[Files] File ${path} had a baseUrl but could not be found on that server: ${err}`)
      if (err instanceof VersionMismatchError) throw err
    }
  }

  return Promise.any(configMap.urls.map(baseUrl => fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version })))
}

export function getFile ({ version, path }) {
  return cache.getFile({ name: path, version }, ({ name: path, version }) => {
    return fetchFileWithHeaders({ version, path }).catch((err) => {
      if (!isVersionMismatchError(err)) throw err
      logger.warn(`[Files] The file ${path} has been delivered with the wrong version from the UI container.`)
      updateVersionProcessor({ immediate: true })

      throw err
    })
  })
}

export async function warmCache ({ version }) {
  const start = +new Date()
  logger.info('[File] start warming up the cache')
  const viteManifests = await getViteManifests({ version })

  for (const key of Object.keys(viteManifests)) {
    const path = `/${viteManifests[key].file}`
    if (!path) continue
    try {
      await getFile({ version, path })
    } catch (err) {
      if (isVersionMismatchError(err)) {
        logger.info(`[File] Cache warming has been canceled because of a version mismatch at "${path}". Canceled after ${Math.floor((+new Date() - start) / 1000)}s`)
        return
      }
      logger.info(`[File] could not prefetch file ${path}`)
    }
  }

  logger.info(`[File] finished warming up the cache in ${Math.floor((+new Date() - start) / 1000)}s`)
}