import express from 'express'
import {
  fetchJson,
  fetchNothing,
  fetchStream,
  fetchString,
} from '@overleaf/fetch-utils'
import fs from 'node:fs'
import fsPromises from 'node:fs/promises'
import Settings from '@overleaf/settings'
import FormData from 'form-data'

const host = Settings.apis.clsi.url

function randomId() {
  // Avoid ids starting with 0, which get a dummy PDF served.
  return 'a' + Math.random().toString(16).slice(2)
}

function compile(projectId, data) {
  if (data) {
    // Enable pdf caching unless disabled explicitly.
    data.options = Object.assign({}, { enablePdfCaching: true }, data.options)
  }
  return fetchJson(`${host}/project/${projectId}/compile`, {
    method: 'POST',
    json: {
      compile: data,
    },
  })
}

async function convertDocument(path, type) {
  const formData = new FormData()
  formData.append('qqfile', fs.createReadStream(path))
  try {
    const stream = await fetchStream(
      `${host}/convert/document-to-latex?type=${type}`,
      {
        method: 'POST',
        body: formData,
      }
    )
    return { status: 200, stream, body: null }
  } catch (err) {
    if (!err.response) throw err
    let body = err.body
    const contentType = err.response.headers.get?.('content-type') ?? ''
    if (contentType.includes('application/json')) {
      body = JSON.parse(body)
    }
    return { status: err.response.status, stream: null, body }
  }
}

async function convertPdfToJpeg(path, mode) {
  const formData = new FormData()
  formData.append('qqfile', await fsPromises.readFile(path), 'input.pdf')
  return await fetch(`${host}/convert/pdf-to-jpeg?mode=${mode}`, {
    method: 'POST',
    headers: formData.getHeaders(),
    body: formData.getBuffer(),
  })
}

async function convertProjectToDocument(
  projectId,
  userId,
  type,
  request,
  responseFormat
) {
  const url = new URL(
    `${host}/project/${projectId}/user/${userId}/download/project-to-document`
  )
  url.searchParams.set('type', type)
  if (responseFormat) {
    url.searchParams.set('responseFormat', responseFormat)
  }
  const opts = { method: 'POST', json: { compile: request } }
  if (responseFormat === 'json') {
    return await fetchJson(url.href, opts)
  }
  return await fetchStream(url.href, opts)
}

async function stopCompile(projectId) {
  return await fetchNothing(`${host}/project/${projectId}/compile/stop`, {
    method: 'POST',
  })
}

async function clearCache(projectId) {
  return await fetchNothing(`${host}/project/${projectId}`, {
    method: 'DELETE',
  })
}

function getOutputFile(response, type) {
  for (const file of response.compile.outputFiles) {
    if (file.type === type && file.url.match(`output.${type}`)) {
      return file
    }
  }
  return null
}

function runFakeFilestoreService(directory) {
  const app = express()
  app.use(express.static(directory))
  this.startFakeFilestoreApp(app)
}

function startFakeFilestoreApp(app) {
  let server
  before(function (done) {
    server = app.listen(error => {
      if (error) {
        done(new Error('error starting server: ' + error.message))
      } else {
        const addr = server.address()
        Settings.filestoreDomainOveride = `http://127.0.0.1:${addr.port}`
        done()
      }
    })
  })
  after(function (done) {
    server.close(done)
  })
}

function syncFromCode(projectId, file, line, column) {
  return syncFromCodeWithImage(projectId, file, line, column, '')
}

async function syncFromCodeWithImage(projectId, file, line, column, imageName) {
  const url = new URL(`${host}/project/${projectId}/sync/code`)
  url.searchParams.append('imageName', imageName)
  url.searchParams.append('file', file)
  url.searchParams.append('line', line)
  url.searchParams.append('column', column)
  return await fetchJson(url)
}

function syncFromPdf(projectId, page, h, v) {
  return syncFromPdfWithImage(projectId, page, h, v, '')
}

function syncFromPdfWithImage(projectId, page, h, v, imageName) {
  const url = new URL(`${host}/project/${projectId}/sync/pdf`)
  url.searchParams.append('imageName', imageName)
  url.searchParams.append('page', page)
  url.searchParams.append('h', h)
  url.searchParams.append('v', v)
  return fetchJson(url)
}

function wordcount(projectId, file) {
  const image = undefined
  return wordcountWithImage(projectId, file, image)
}

async function wordcountWithImage(projectId, file, image) {
  const url = new URL(`${host}/project/${projectId}/wordcount`)
  if (image) {
    url.searchParams.append('image', image)
  }
  url.searchParams.append('file', file)
  return await fetchJson(url)
}

async function compileDirectory(projectId, baseDirectory, directory) {
  const resources = []
  let entities = fs.readdirSync(`${baseDirectory}/${directory}`)
  let rootResourcePath = 'main.tex'
  while (entities.length > 0) {
    const entity = entities.pop()
    const stat = fs.statSync(`${baseDirectory}/${directory}/${entity}`)
    if (stat.isDirectory()) {
      entities = entities.concat(
        fs
          .readdirSync(`${baseDirectory}/${directory}/${entity}`)
          .map(subEntity => {
            if (subEntity === 'main.tex') {
              rootResourcePath = `${entity}/${subEntity}`
            }
            return `${entity}/${subEntity}`
          })
      )
    } else if (stat.isFile() && entity !== 'output.pdf') {
      const extension = entity.split('.').pop()
      if (
        [
          'tex',
          'bib',
          'cls',
          'sty',
          'pdf_tex',
          'Rtex',
          'ist',
          'md',
          'Rmd',
          'Rnw',
        ].indexOf(extension) > -1
      ) {
        resources.push({
          path: entity,
          content: fs
            .readFileSync(`${baseDirectory}/${directory}/${entity}`)
            .toString(),
        })
      } else if (
        ['eps', 'ttf', 'png', 'jpg', 'pdf', 'jpeg'].indexOf(extension) > -1
      ) {
        resources.push({
          path: entity,
          url: `http://filestore/${directory}/${entity}`,
          modified: stat.mtime,
        })
      }
    }
  }

  const req = {
    resources,
    rootResourcePath,
  }

  try {
    const options = await fsPromises.readFile(
      `${baseDirectory}/${directory}/options.json`
    )
    req.options = JSON.parse(options)
  } catch (error) {
    // noop
  }

  return await compile(projectId, req)
}

function smokeTest() {
  return fetchString(`${host}/smoke_test_force`, {
    method: 'GET',
  })
}

export default {
  randomId,
  compile,
  convertProjectToDocument,
  convertDocument,
  convertPdfToJpeg,
  stopCompile,
  clearCache,
  getOutputFile,
  smokeTest,
  runFakeFilestoreService,
  startFakeFilestoreApp,
  syncFromCode,
  syncFromCodeWithImage,
  syncFromPdf,
  syncFromPdfWithImage,
  compileDirectory,
  wordcount,
  wordcountWithImage,
}
