// 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
 */
import ProjectEntityHandler from './ProjectEntityHandler.mjs'
import ProjectEntityUpdateHandler from './ProjectEntityUpdateHandler.mjs'
import ProjectGetter from './ProjectGetter.mjs'
import DocumentHelper from '../Documents/DocumentHelper.mjs'
import Path from 'node:path'
import fs from 'node:fs'
import pLimit from 'p-limit'
import globby from 'globby'
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
import logger from '@overleaf/logger'
import { BackgroundTaskTracker } from '../../infrastructure/GracefulShutdown.mjs'

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

function 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)
}

async function setRootDocAutomatically(projectId) {
  const docs = await ProjectEntityHandler.promises.getAllDocs(projectId)

  for (const [path, doc] of Object.entries(docs)) {
    if (
      ProjectEntityUpdateHandler.isPathValidForRootDoc(path) &&
      DocumentHelper.contentHasDocumentclass(doc.lines) &&
      doc._id
    ) {
      return await ProjectEntityUpdateHandler.promises.setRootDoc(
        projectId,
        doc._id
      )
    }
  }
}

async function findRootDocFileFromDirectory(directoryPath) {
  const unsortedFiles = await 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

  const files = await _sortFileList(unsortedFiles, directoryPath)
  let firstFileInRootFolder
  let doc = null

  while (files.length > 0 && doc == null) {
    const file = files.shift()
    const content = await fs.promises.readFile(
      Path.join(directoryPath, file),
      'utf8'
    )
    const normalizedContent = (content || '').replace(/\r/g, '')
    if (DocumentHelper.contentHasDocumentclass(normalizedContent)) {
      doc = { path: file, content: normalizedContent }
    }

    if (!firstFileInRootFolder && !file.includes('/')) {
      firstFileInRootFolder = { path: file, content: normalizedContent }
    }
  }

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

  return { path: doc?.path, content: doc?.content }
}

async function setRootDocFromName(projectId, rootDocName) {
  const docPaths =
    await ProjectEntityHandler.promises.getAllDocPathsFromProjectById(projectId)

  let docId, path
  // 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 await ProjectEntityUpdateHandler.promises.setRootDoc(
      projectId,
      rootDocId
    )
  }
}

/**
 * @param {ObjectId | string} projectId
 */
async function ensureRootDocumentIsValid(projectId) {
  const project = await ProjectGetter.promises.getProject(projectId, {
    rootFolder: 1,
    rootDoc_id: 1,
  })
  if (!project) {
    throw new Error('project not found')
  }
  if (project.rootDoc_id != null) {
    const rootResourcePath =
      await ProjectEntityHandler.promises.getDocPathFromProjectByDocId(
        project,
        project.rootDoc_id
      )
    if (rootResourcePath) {
      return { rootDocId: project.rootDoc_id, rootResourcePath }
    }
    await ProjectEntityUpdateHandler.promises.unsetRootDoc(projectId)
  }
  return await ProjectRootDocManager.promises.setRootDocAutomatically(projectId)
}

async function _sortFileList(listToSort, rootDirectory) {
  const limit = pLimit(5)
  const files = await Promise.all(
    listToSort.map(filePath =>
      limit(async () => {
        const fullPath = Path.join(rootDirectory, filePath)
        const stat = await fs.promises.stat(fullPath)
        return {
          size: stat.size,
          path: filePath,
          elements: filePath.split(Path.sep).length,
          name: Path.basename(filePath),
        }
      })
    )
  )
  return files.sort(_rootDocSort).map(file => file.path)
}

function _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)
}

const ProjectRootDocManager = {
  setRootDocAutomaticallyInBackground,
  setRootDocAutomatically: callbackify(setRootDocAutomatically),
  findRootDocFileFromDirectory: callbackifyMultiResult(
    findRootDocFileFromDirectory,
    ['path', 'content']
  ),
  setRootDocFromName: callbackify(setRootDocFromName),
  ensureRootDocumentIsValid: callbackify(ensureRootDocumentIsValid),
  promises: {
    setRootDocAutomatically,
    findRootDocFileFromDirectory,
    setRootDocFromName,
    ensureRootDocumentIsValid,
  },
}

export default ProjectRootDocManager
