/* eslint-disable
    n/handle-callback-err,
    max-len,
    no-unused-vars,
    no-useless-escape,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * 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
 */
let ProjectRootDocManager
const ProjectEntityHandler = require('./ProjectEntityHandler')
const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler')
const ProjectGetter = require('./ProjectGetter')
const DocumentHelper = require('../Documents/DocumentHelper')
const Path = require('path')
const fs = require('fs')
const async = require('async')
const globby = require('globby')
const _ = require('lodash')
const { promisifyAll } = require('@overleaf/promise-utils')
const logger = require('@overleaf/logger')
const {
  BackgroundTaskTracker,
} = require('../../infrastructure/GracefulShutdown')

const rootDocResets = new BackgroundTaskTracker('root doc resets')

module.exports = ProjectRootDocManager = {
  setRootDocAutomaticallyInBackground(projectId) {
    rootDocResets.add()
    setTimeout(async () => {
      try {
        await ProjectRootDocManager.promises.setRootDocAutomatically(projectId)
      } catch (err) {
        logger.warn(
          { err },
          'failed to set root doc automatically in background'
        )
      } finally {
        rootDocResets.done()
      }
    }, 30 * 1000)
  },

  setRootDocAutomatically(projectId, callback) {
    if (callback == null) {
      callback = function () {}
    }
    return ProjectEntityHandler.getAllDocs(projectId, function (error, docs) {
      if (error != null) {
        return callback(error)
      }

      const jobs = _.map(
        docs,
        (doc, path) =>
          function (cb) {
            if (
              ProjectEntityUpdateHandler.isPathValidForRootDoc(path) &&
              DocumentHelper.contentHasDocumentclass(doc.lines)
            ) {
              async.setImmediate(function () {
                cb(doc._id)
              })
            } else {
              async.setImmediate(function () {
                cb(null)
              })
            }
          }
      )

      return async.series(jobs, function (rootDocId) {
        if (rootDocId != null) {
          return ProjectEntityUpdateHandler.setRootDoc(
            projectId,
            rootDocId,
            callback
          )
        } else {
          return callback()
        }
      })
    })
  },

  findRootDocFileFromDirectory(directoryPath, callback) {
    if (callback == null) {
      callback = function () {}
    }
    const filePathsPromise = globby(['**/*.{tex,Rtex,Rnw}'], {
      cwd: directoryPath,
      followSymlinkedDirectories: false,
      onlyFiles: true,
      case: false,
    })

    // the search order is such that we prefer files closer to the project root, then
    // we go by file size in ascending order, because people often have a main
    // file that just includes a bunch of other files; then we go by name, in
    // order to be deterministic
    filePathsPromise.then(
      unsortedFiles =>
        ProjectRootDocManager._sortFileList(
          unsortedFiles,
          directoryPath,
          function (err, files) {
            if (err != null) {
              return callback(err)
            }
            let firstFileInRootFolder
            let doc = null

            return async.until(
              cb => cb(null, doc != null || files.length === 0),
              function (cb) {
                const file = files.shift()
                return fs.readFile(
                  Path.join(directoryPath, file),
                  'utf8',
                  function (error, content) {
                    if (error != null) {
                      return cb(error)
                    }
                    content = (content || '').replace(/\r/g, '')
                    if (DocumentHelper.contentHasDocumentclass(content)) {
                      doc = { path: file, content }
                    }

                    if (!firstFileInRootFolder && !file.includes('/')) {
                      firstFileInRootFolder = { path: file, content }
                    }
                    cb(null)
                  }
                )
              },
              err => {
                if (err) {
                  return callback(err)
                }

                // if no doc was found, use the first file in the root folder as the main doc
                if (!doc && firstFileInRootFolder) {
                  doc = firstFileInRootFolder
                }

                callback(null, doc?.path, doc?.content)
              }
            )
          }
        ),
      err => callback(err)
    )

    // coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha
    return null
  },

  setRootDocFromName(projectId, rootDocName, callback) {
    if (callback == null) {
      callback = function () {}
    }
    return ProjectEntityHandler.getAllDocPathsFromProjectById(
      projectId,
      function (error, docPaths) {
        let docId, path
        if (error != null) {
          return callback(error)
        }
        // strip off leading and trailing quotes from rootDocName
        rootDocName = rootDocName.replace(/^\'|\'$/g, '')
        // prepend a slash for the root folder if not present
        if (rootDocName[0] !== '/') {
          rootDocName = `/${rootDocName}`
        }
        // find the root doc from the filename
        let rootDocId = null
        for (docId in docPaths) {
          // docpaths have a leading / so allow matching "folder/filename" and "/folder/filename"
          path = docPaths[docId]
          if (path === rootDocName) {
            rootDocId = docId
          }
        }
        // try a basename match if there was no match
        if (!rootDocId) {
          for (docId in docPaths) {
            path = docPaths[docId]
            if (Path.basename(path) === Path.basename(rootDocName)) {
              rootDocId = docId
            }
          }
        }
        // set the root doc id if we found a match
        if (rootDocId != null) {
          return ProjectEntityUpdateHandler.setRootDoc(
            projectId,
            rootDocId,
            callback
          )
        } else {
          return callback()
        }
      }
    )
  },

  ensureRootDocumentIsSet(projectId, callback) {
    if (callback == null) {
      callback = function () {}
    }
    return ProjectGetter.getProject(
      projectId,
      { rootDoc_id: 1 },
      function (error, project) {
        if (error != null) {
          return callback(error)
        }
        if (project == null) {
          return callback(new Error('project not found'))
        }

        if (project.rootDoc_id != null) {
          return callback()
        } else {
          return ProjectRootDocManager.setRootDocAutomatically(
            projectId,
            callback
          )
        }
      }
    )
  },

  /**
   * @param {ObjectId | string} project_id
   * @param {Function} callback
   */
  ensureRootDocumentIsValid(projectId, callback) {
    ProjectGetter.getProjectWithoutDocLines(
      projectId,
      function (error, project) {
        if (error != null) {
          return callback(error)
        }
        if (project == null) {
          return callback(new Error('project not found'))
        }

        if (project.rootDoc_id != null) {
          ProjectEntityHandler.getDocPathFromProjectByDocId(
            project,
            project.rootDoc_id,
            (err, docPath) => {
              if (docPath) return callback()
              ProjectEntityUpdateHandler.unsetRootDoc(projectId, () =>
                ProjectRootDocManager.setRootDocAutomatically(
                  projectId,
                  callback
                )
              )
            }
          )
        } else {
          return ProjectRootDocManager.setRootDocAutomatically(
            projectId,
            callback
          )
        }
      }
    )
  },

  _sortFileList(listToSort, rootDirectory, callback) {
    if (callback == null) {
      callback = function () {}
    }
    return async.mapLimit(
      listToSort,
      5,
      (filePath, cb) =>
        fs.stat(Path.join(rootDirectory, filePath), function (err, stat) {
          if (err != null) {
            return cb(err)
          }
          return cb(null, {
            size: stat.size,
            path: filePath,
            elements: filePath.split(Path.sep).length,
            name: Path.basename(filePath),
          })
        }),
      function (err, files) {
        if (err != null) {
          return callback(err)
        }

        return callback(
          null,
          _.map(
            files.sort(ProjectRootDocManager._rootDocSort),
            file => file.path
          )
        )
      }
    )
  },

  _rootDocSort(a, b) {
    // sort first by folder depth
    if (a.elements !== b.elements) {
      return a.elements - b.elements
    }
    // ensure main.tex is at the start of each folder
    if (a.name === 'main.tex' && b.name !== 'main.tex') {
      return -1
    }
    if (a.name !== 'main.tex' && b.name === 'main.tex') {
      return 1
    }
    // prefer smaller files
    if (a.size !== b.size) {
      return a.size - b.size
    }
    // otherwise, use the full path name
    return a.path.localeCompare(b.path)
  },
}

module.exports = ProjectRootDocManager
module.exports.promises = promisifyAll(module.exports, {
  without: ['_rootDocSort', 'setRootDocAutomaticallyInBackground'],
  multiResult: {
    findRootDocFileFromDirectory: ['path', 'content'],
  },
})
