// @ts-check
'use strict'

/**
 * @import { Snapshot } from 'overleaf-editor-core'
 * @import { BlobStore } from '../../storage/lib/blob_store/index'
 */

const Archive = require('archiver')
const BPromise = require('bluebird')
const fs = require('node:fs')
const { pipeline } = require('node:stream')

const core = require('overleaf-editor-core')

const Snapshot = core.Snapshot
const OError = require('@overleaf/o-error')

const assert = require('./assert')

// The maximum safe concurrency appears to be 1.
// https://github.com/overleaf/issues/issues/1909
const FETCH_CONCURRENCY = 1 // number of files to fetch at once

class DownloadError extends OError {
  constructor(hash) {
    super(`ProjectArchive: blob download failed: ${hash}`, { hash })
  }
}

class ArchiveTimeout extends OError {
  constructor() {
    super('ProjectArchive timed out')
  }
}

class MissingfileError extends OError {
  constructor() {
    super('ProjectArchive: attempting to look up a file that does not exist')
  }
}

class ProjectArchive {
  static ArchiveTimeout = ArchiveTimeout
  static MissingfileError = MissingfileError
  static DownloadError = DownloadError

  /**
   * @constructor
   * @param {Snapshot} snapshot
   * @param {number} timeout in ms
   * @classdesc
   * Writes the project snapshot to a zip file.
   */
  constructor(snapshot, timeout) {
    assert.instance(snapshot, Snapshot)
    this.snapshot = snapshot
    this.timeout = timeout
  }

  /**
   * Write zip archive to the given file path.
   *
   * @param {BlobStore} blobStore
   * @param {string} zipFilePath
   */
  writeZip(blobStore, zipFilePath) {
    const snapshot = this.snapshot
    const timeout = this.timeout

    const startTime = process.hrtime()
    const archive = new Archive('zip')

    // Convert elapsed seconds and nanoseconds to milliseconds.
    function findElapsedMilliseconds() {
      const elapsed = process.hrtime(startTime)
      return elapsed[0] * 1e3 + elapsed[1] * 1e-6
    }

    function addFileToArchive(pathname) {
      if (findElapsedMilliseconds() > timeout) {
        throw new ProjectArchive.ArchiveTimeout()
      }

      const file = snapshot.getFile(pathname)
      if (!file) {
        throw new ProjectArchive.MissingfileError()
      }
      return file.load('eager', blobStore).then(function () {
        const content = file.getContent({ filterTrackedDeletes: true })
        if (content === null) {
          return streamFileToArchive(pathname, file).catch(function (err) {
            throw new ProjectArchive.DownloadError(file.getHash()).withCause(
              err
            )
          })
        } else {
          archive.append(content, { name: pathname })
        }
      })
    }

    function streamFileToArchive(pathname, file) {
      return new BPromise(function (resolve, reject) {
        blobStore
          .getStream(file.getHash())
          .then(stream => {
            stream.on('error', reject)
            stream.on('end', resolve)
            archive.append(stream, { name: pathname })
          })
          .catch(reject)
      })
    }

    const addFilesToArchiveAndFinalize = BPromise.map(
      snapshot.getFilePathnames(),
      addFileToArchive,
      { concurrency: FETCH_CONCURRENCY }
    ).then(function () {
      archive.finalize()
    })

    const streamArchiveToFile = new BPromise(function (resolve, reject) {
      const stream = fs.createWriteStream(zipFilePath)
      pipeline(archive, stream, function (err) {
        if (err) {
          reject(err)
        } else {
          resolve()
        }
      })
    })

    return BPromise.join(streamArchiveToFile, addFilesToArchiveAndFinalize)
  }
}

module.exports = ProjectArchive
