import path from 'node:path'
import DocstoreManager from '../Docstore/DocstoreManager.mjs'
import Errors from '../Errors/Errors.js'
import ProjectGetter from './ProjectGetter.mjs'
import { callbackifyAll } from '@overleaf/promise-utils'
import OError from '@overleaf/o-error'
import { iterablePaths } from './IterablePath.mjs'

/** @import {ProjectDoc, ProjectFile} from './types' */

/**
 * @param {string} projectId
 * @returns {Promise<Record<string, ProjectDoc>>}
 */
async function getAllDocs(projectId) {
  // We get the path and name info from the project, and the lines and
  // version info from the doc store.
  const docContentsArray = await DocstoreManager.promises.getAllDocs(projectId)

  // Turn array from docstore into a dictionary based on doc id
  const docContents = {}
  for (const docContent of docContentsArray) {
    docContents[docContent._id] = docContent
  }

  const folders = await _getAllFolders(projectId)
  const docs = {}
  for (const { path: folderPath, folder } of folders) {
    for (const doc of iterablePaths(folder, 'docs')) {
      const content = docContents[doc._id.toString()]
      if (content != null) {
        docs[path.join(folderPath, doc.name)] = {
          _id: doc._id,
          name: doc.name,
          lines: content.lines,
          rev: content.rev,
          folder,
        }
      }
    }
  }

  return docs
}

/**
 * @param {string} projectId
 * @returns {Promise<Record<string, ProjectFile>>}
 */
async function getAllFiles(projectId) {
  const folders = await _getAllFolders(projectId)
  const files = {}
  for (const { path: folderPath, folder } of folders) {
    for (const file of iterablePaths(folder, 'fileRefs')) {
      if (file != null) {
        files[path.join(folderPath, file.name)] = { ...file, folder }
      }
    }
  }
  return files
}

async function getAllEntities(projectId) {
  const project = await ProjectGetter.promises.getProject(projectId)
  if (project == null) {
    throw new Errors.NotFoundError('project not found')
  }
  const entities = getAllEntitiesFromProject(project)
  return entities
}

function getAllEntitiesFromProject(project) {
  const folders = _getAllFoldersFromProject(project)
  const docs = []
  const files = []
  for (const { path: folderPath, folder } of folders) {
    for (const doc of iterablePaths(folder, 'docs')) {
      if (doc != null) {
        docs.push({ path: path.join(folderPath, doc.name), doc })
      }
    }
    for (const file of iterablePaths(folder, 'fileRefs')) {
      if (file != null) {
        files.push({ path: path.join(folderPath, file.name), file })
      }
    }
  }
  return { docs, files, folders }
}

async function getAllDocPathsFromProjectById(projectId) {
  const project = await ProjectGetter.promises.getProject(projectId, {
    rootFolder: 1,
  })
  if (project == null) {
    throw new Errors.NotFoundError('no project')
  }
  const docPaths = getAllDocPathsFromProject(project)
  return docPaths
}

function getAllDocPathsFromProject(project) {
  const folders = _getAllFoldersFromProject(project)
  const docPath = {}
  for (const { path: folderPath, folder } of folders) {
    for (const doc of iterablePaths(folder, 'docs')) {
      docPath[doc._id] = path.join(folderPath, doc.name)
    }
  }
  return docPath
}

/**
 *
 * @param {string} projectId
 * @param {string} docId
 * @param {{peek?: boolean, include_deleted?: boolean}} options
 * @return {Promise<{lines: *, rev: *, version: *, ranges: *}>}
 */
async function getDoc(projectId, docId, options = {}) {
  const { lines, rev, version, ranges } = await DocstoreManager.promises.getDoc(
    projectId,
    docId,
    options
  )
  return { lines, rev, version, ranges }
}

/**
 * @param {ObjectId | string} projectId
 * @param {ObjectId | string} docId
 */
async function getDocPathByProjectIdAndDocId(projectId, docId) {
  const project = await ProjectGetter.promises.getProject(projectId, {
    rootFolder: 1,
  })
  if (project == null) {
    throw new Errors.NotFoundError('no project')
  }
  const docPath = await getDocPathFromProjectByDocId(project, docId)
  if (docPath == null) {
    throw new Errors.NotFoundError('no doc')
  }
  return docPath
}

function _recursivelyFindDocInFolder(basePath, docId, folder) {
  const docInCurrentFolder = (folder.docs || []).find(
    currentDoc => currentDoc._id.toString() === docId.toString()
  )
  if (docInCurrentFolder != null) {
    return path.join(basePath, docInCurrentFolder.name)
  } else {
    let docPath, childFolder
    for (childFolder of iterablePaths(folder, 'folders')) {
      docPath = _recursivelyFindDocInFolder(
        path.join(basePath, childFolder.name),
        docId,
        childFolder
      )
      if (docPath != null) {
        return docPath
      }
    }
    return null
  }
}

/**
 * @param {Project} project
 * @param {ObjectId | string} docId
 * @param {Function} callback
 */
async function getDocPathFromProjectByDocId(project, docId) {
  const docPath = _recursivelyFindDocInFolder('/', docId, project.rootFolder[0])
  return docPath
}

async function _getAllFolders(projectId) {
  const project = await ProjectGetter.promises.getProject(projectId, {
    rootFolder: 1,
  })

  if (project == null) {
    throw new Errors.NotFoundError('no project')
  }
  const folders = _getAllFoldersFromProject(project)
  return folders
}

function _getAllFoldersFromProject(project) {
  const folders = []
  try {
    const processFolder = (basePath, folder) => {
      folders.push({ path: basePath, folder })
      if (folder.folders) {
        for (const childFolder of iterablePaths(folder, 'folders')) {
          if (childFolder.name != null) {
            const childPath = path.join(basePath, childFolder.name)
            processFolder(childPath, childFolder)
          }
        }
      }
    }
    processFolder('/', project.rootFolder[0])
    return folders
  } catch (err) {
    throw OError.tag(err, 'Error getting folders', { projectId: project._id })
  }
}

const ProjectEntityHandler = {
  getAllDocs,
  getAllFiles,
  getAllEntities,
  getAllDocPathsFromProjectById,
  getDoc,
  getDocPathByProjectIdAndDocId,
  getDocPathFromProjectByDocId,
  _getAllFolders,
}

export default {
  ...callbackifyAll(ProjectEntityHandler, {
    multiResult: {
      getDoc: ['lines', 'rev', 'version', 'ranges'],
    },
  }),
  promises: ProjectEntityHandler,
  getAllEntitiesFromProject,
  getAllDocPathsFromProject,
  _getAllFoldersFromProject,
}
