// TODO: This file was created by bulk-decaffeinate.
/*
 * decaffeinate suggestions:
 * DS102: Remove unnecessary code created because of implicit returns
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
import fs from 'node:fs'

import OError from '@overleaf/o-error'
import logger from '@overleaf/logger'
import crypto from 'node:crypto'
import Settings from '@overleaf/settings'
import { fetchStream } from '@overleaf/fetch-utils'
import { Transform, pipeline } from 'node:stream'
import { FileTooLargeError } from '../Features/Errors/Errors.js'
import { callbackify, promisify } from '@overleaf/promise-utils'

export class SizeLimitedStream extends Transform {
  constructor(options) {
    options.autoDestroy = true
    super(options)

    this.bytes = 0
    this.maxSizeBytes = options.maxSizeBytes || Settings.maxUploadSize
    this.drain = false
    this.on('error', () => {
      this.drain = true
      this.resume()
    })
  }

  _transform(chunk, encoding, done) {
    if (this.drain) {
      // mechanism to drain the source stream on error, to avoid leaks
      // we consume the rest of the incoming stream and don't push it anywhere
      return done()
    }

    this.bytes += chunk.length
    if (this.maxSizeBytes && this.bytes > this.maxSizeBytes) {
      return done(
        new FileTooLargeError({
          message: 'stream size limit reached',
          info: { size: this.bytes },
        })
      )
    }
    this.push(chunk)
    done()
  }
}

/**
 * @type {{
 *   ensureDumpFolderExists: () => void,
 *   writeLinesToDisk: (identifier: any, lines: any, callback?: any) => void,
 *   writeContentToDisk: (identifier: any, content: any, callback?: any) => void,
 *   writeStreamToDisk: (identifier: any, stream: any, options?: any, callback?: any) => void,
 *   writeUrlToDisk: (identifier: any, url: any, options?: any, callback?: any) => void,
 *   promises: {
 *     writeLinesToDisk: (identifier: any, lines: any) => Promise<string>,
 *     writeContentToDisk: (identifier: any, content: any) => Promise<string>,
 *     writeStreamToDisk: (identifier: any, stream: any, options?: any) => Promise<string>,
 *     writeUrlToDisk: (identifier: any, url: any, options?: any) => Promise<string>,
 *   },
 * }}
 */
const FileWriter = {
  ensureDumpFolderExists() {
    fs.mkdirSync(Settings.path.dumpFolder, { recursive: true })
  },

  writeLinesToDisk(identifier, lines, callback) {
    if (callback == null) {
      callback = function () {}
    }
    return FileWriter.writeContentToDisk(identifier, lines.join('\n'), callback)
  },

  writeContentToDisk(identifier, content, callback) {
    if (callback == null) {
      callback = function () {}
    }
    const fsPath = `${
      Settings.path.dumpFolder
    }/${identifier}_${crypto.randomUUID()}`
    return fs.writeFile(fsPath, content, function (error) {
      if (error != null) {
        return callback(error)
      }
      return callback(null, fsPath)
    })
  },

  writeStreamToDisk(identifier, stream, options, callback) {
    if (typeof options === 'function') {
      callback = options
      options = {}
    }
    if (callback == null) {
      callback = function () {}
    }
    options = options || {}

    const fsPath = `${
      Settings.path.dumpFolder
    }/${identifier}_${crypto.randomUUID()}`

    const writeStream = fs.createWriteStream(fsPath)
    const passThrough = new SizeLimitedStream({
      maxSizeBytes: options.maxSizeBytes,
    })

    // if writing fails, we want to consume the bytes from the source, to avoid leaks
    for (const evt of ['error', 'close']) {
      writeStream.on(evt, function () {
        passThrough.unpipe(writeStream)
        passThrough.resume()
      })
    }

    pipeline(stream, passThrough, writeStream, function (err) {
      if (
        options.maxSizeBytes &&
        passThrough.bytes >= options.maxSizeBytes &&
        !(err instanceof FileTooLargeError)
      ) {
        err = new FileTooLargeError({
          message: 'stream size limit reached',
          info: { size: passThrough.bytes },
        }).withCause(err || {})
      }
      if (err) {
        stream.destroy()
        writeStream.destroy()
        fs.unlink(fsPath, error => {
          if (error && error.code !== 'ENOENT') {
            logger.warn(
              { error, fsPath },
              'Failed to delete partial file after error'
            )
          }
        })
        OError.tag(
          err,
          '[writeStreamToDisk] something went wrong writing the stream to disk',
          {
            identifier,
            fsPath,
          }
        )
        return callback(err)
      }

      logger.debug(
        { identifier, fsPath },
        '[writeStreamToDisk] write stream finished'
      )
      callback(null, fsPath)
    })
  },
}

async function writeUrlToDisk(identifier, url, options = {}) {
  let stream
  try {
    stream = await fetchStream(url)
  } catch (error) {
    const err = new OError('bad response from url', {
      statusCode: error.response?.status,
    })
    logger.warn({ err, identifier, url }, `[writeUrlToDisk] ${err.message}`)
    throw err
  }
  return await FileWriter.promises.writeStreamToDisk(
    identifier,
    stream,
    options
  )
}

FileWriter.writeUrlToDisk = callbackify(writeUrlToDisk)

FileWriter.promises = {
  writeLinesToDisk: promisify(FileWriter.writeLinesToDisk),
  writeContentToDisk: promisify(FileWriter.writeContentToDisk),
  writeStreamToDisk: promisify(FileWriter.writeStreamToDisk),
  writeUrlToDisk,
}

export default FileWriter
