/* eslint-disable
    camelcase,
    handle-callback-err,
    no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS101: Remove unnecessary use of Array.from
 * 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 DocumentManager
const RedisManager = require('./RedisManager')
const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
const PersistenceManager = require('./PersistenceManager')
const DiffCodec = require('./DiffCodec')
const logger = require('logger-sharelatex')
const Metrics = require('./Metrics')
const HistoryManager = require('./HistoryManager')
const RealTimeRedisManager = require('./RealTimeRedisManager')
const Errors = require('./Errors')
const RangesManager = require('./RangesManager')
const async = require('async')

const MAX_UNFLUSHED_AGE = 300 * 1000 // 5 mins, document should be flushed to mongo this time after a change

module.exports = DocumentManager = {
  getDoc(project_id, doc_id, _callback) {
    if (_callback == null) {
      _callback = function (
        error,
        lines,
        version,
        ranges,
        pathname,
        projectHistoryId,
        unflushedTime,
        alreadyLoaded
      ) {}
    }
    const timer = new Metrics.Timer('docManager.getDoc')
    const callback = function (...args) {
      timer.done()
      return _callback(...Array.from(args || []))
    }

    return RedisManager.getDoc(project_id, doc_id, function (
      error,
      lines,
      version,
      ranges,
      pathname,
      projectHistoryId,
      unflushedTime
    ) {
      if (error != null) {
        return callback(error)
      }
      if (lines == null || version == null) {
        logger.log(
          { project_id, doc_id },
          'doc not in redis so getting from persistence API'
        )
        return PersistenceManager.getDoc(project_id, doc_id, function (
          error,
          lines,
          version,
          ranges,
          pathname,
          projectHistoryId,
          projectHistoryType
        ) {
          if (error != null) {
            return callback(error)
          }
          logger.log(
            {
              project_id,
              doc_id,
              lines,
              version,
              pathname,
              projectHistoryId,
              projectHistoryType
            },
            'got doc from persistence API'
          )
          return RedisManager.putDocInMemory(
            project_id,
            doc_id,
            lines,
            version,
            ranges,
            pathname,
            projectHistoryId,
            function (error) {
              if (error != null) {
                return callback(error)
              }
              return RedisManager.setHistoryType(
                doc_id,
                projectHistoryType,
                function (error) {
                  if (error != null) {
                    return callback(error)
                  }
                  return callback(
                    null,
                    lines,
                    version,
                    ranges || {},
                    pathname,
                    projectHistoryId,
                    null,
                    false
                  )
                }
              )
            }
          )
        })
      } else {
        return callback(
          null,
          lines,
          version,
          ranges,
          pathname,
          projectHistoryId,
          unflushedTime,
          true
        )
      }
    })
  },

  getDocAndRecentOps(project_id, doc_id, fromVersion, _callback) {
    if (_callback == null) {
      _callback = function (
        error,
        lines,
        version,
        ops,
        ranges,
        pathname,
        projectHistoryId
      ) {}
    }
    const timer = new Metrics.Timer('docManager.getDocAndRecentOps')
    const callback = function (...args) {
      timer.done()
      return _callback(...Array.from(args || []))
    }

    return DocumentManager.getDoc(project_id, doc_id, function (
      error,
      lines,
      version,
      ranges,
      pathname,
      projectHistoryId
    ) {
      if (error != null) {
        return callback(error)
      }
      if (fromVersion === -1) {
        return callback(
          null,
          lines,
          version,
          [],
          ranges,
          pathname,
          projectHistoryId
        )
      } else {
        return RedisManager.getPreviousDocOps(
          doc_id,
          fromVersion,
          version,
          function (error, ops) {
            if (error != null) {
              return callback(error)
            }
            return callback(
              null,
              lines,
              version,
              ops,
              ranges,
              pathname,
              projectHistoryId
            )
          }
        )
      }
    })
  },

  setDoc(project_id, doc_id, newLines, source, user_id, undoing, _callback) {
    if (_callback == null) {
      _callback = function (error) {}
    }
    const timer = new Metrics.Timer('docManager.setDoc')
    const callback = function (...args) {
      timer.done()
      return _callback(...Array.from(args || []))
    }

    if (newLines == null) {
      return callback(new Error('No lines were provided to setDoc'))
    }

    const UpdateManager = require('./UpdateManager')
    return DocumentManager.getDoc(project_id, doc_id, function (
      error,
      oldLines,
      version,
      ranges,
      pathname,
      projectHistoryId,
      unflushedTime,
      alreadyLoaded
    ) {
      if (error != null) {
        return callback(error)
      }

      if (oldLines != null && oldLines.length > 0 && oldLines[0].text != null) {
        logger.log(
          { doc_id, project_id, oldLines, newLines },
          'document is JSON so not updating'
        )
        return callback(null)
      }

      logger.log(
        { doc_id, project_id, oldLines, newLines },
        'setting a document via http'
      )
      return DiffCodec.diffAsShareJsOp(oldLines, newLines, function (
        error,
        op
      ) {
        if (error != null) {
          return callback(error)
        }
        if (undoing) {
          for (const o of Array.from(op || [])) {
            o.u = true
          } // Turn on undo flag for each op for track changes
        }
        const update = {
          doc: doc_id,
          op,
          v: version,
          meta: {
            type: 'external',
            source,
            user_id
          }
        }
        return UpdateManager.applyUpdate(project_id, doc_id, update, function (
          error
        ) {
          if (error != null) {
            return callback(error)
          }
          // If the document was loaded already, then someone has it open
          // in a project, and the usual flushing mechanism will happen.
          // Otherwise we should remove it immediately since nothing else
          // is using it.
          if (alreadyLoaded) {
            return DocumentManager.flushDocIfLoaded(
              project_id,
              doc_id,
              function (error) {
                if (error != null) {
                  return callback(error)
                }
                return callback(null)
              }
            )
          } else {
            return DocumentManager.flushAndDeleteDoc(
              project_id,
              doc_id,
              {},
              function (error) {
                // There is no harm in flushing project history if the previous
                // call failed and sometimes it is required
                HistoryManager.flushProjectChangesAsync(project_id)

                if (error != null) {
                  return callback(error)
                }
                return callback(null)
              }
            )
          }
        })
      })
    })
  },

  flushDocIfLoaded(project_id, doc_id, _callback) {
    if (_callback == null) {
      _callback = function (error) {}
    }
    const timer = new Metrics.Timer('docManager.flushDocIfLoaded')
    const callback = function (...args) {
      timer.done()
      return _callback(...Array.from(args || []))
    }
    return RedisManager.getDoc(project_id, doc_id, function (
      error,
      lines,
      version,
      ranges,
      pathname,
      projectHistoryId,
      unflushedTime,
      lastUpdatedAt,
      lastUpdatedBy
    ) {
      if (error != null) {
        return callback(error)
      }
      if (lines == null || version == null) {
        logger.log({ project_id, doc_id }, 'doc is not loaded so not flushing')
        return callback(null) // TODO: return a flag to bail out, as we go on to remove doc from memory?
      } else {
        logger.log({ project_id, doc_id, version }, 'flushing doc')
        return PersistenceManager.setDoc(
          project_id,
          doc_id,
          lines,
          version,
          ranges,
          lastUpdatedAt,
          lastUpdatedBy,
          function (error) {
            if (error != null) {
              return callback(error)
            }
            return RedisManager.clearUnflushedTime(doc_id, callback)
          }
        )
      }
    })
  },

  flushAndDeleteDoc(project_id, doc_id, options, _callback) {
    const timer = new Metrics.Timer('docManager.flushAndDeleteDoc')
    const callback = function (...args) {
      timer.done()
      return _callback(...Array.from(args || []))
    }

    return DocumentManager.flushDocIfLoaded(project_id, doc_id, function (
      error
    ) {
      if (error != null) {
        if (options.ignoreFlushErrors) {
          logger.warn(
            { project_id, doc_id, err: error },
            'ignoring flush error while deleting document'
          )
        } else {
          return callback(error)
        }
      }

      // Flush in the background since it requires a http request
      HistoryManager.flushDocChangesAsync(project_id, doc_id)

      return RedisManager.removeDocFromMemory(project_id, doc_id, function (
        error
      ) {
        if (error != null) {
          return callback(error)
        }
        return callback(null)
      })
    })
  },

  acceptChanges(project_id, doc_id, change_ids, _callback) {
    if (change_ids == null) {
      change_ids = []
    }
    if (_callback == null) {
      _callback = function (error) {}
    }
    const timer = new Metrics.Timer('docManager.acceptChanges')
    const callback = function (...args) {
      timer.done()
      return _callback(...Array.from(args || []))
    }

    return DocumentManager.getDoc(project_id, doc_id, function (
      error,
      lines,
      version,
      ranges
    ) {
      if (error != null) {
        return callback(error)
      }
      if (lines == null || version == null) {
        return callback(
          new Errors.NotFoundError(`document not found: ${doc_id}`)
        )
      }
      return RangesManager.acceptChanges(change_ids, ranges, function (
        error,
        new_ranges
      ) {
        if (error != null) {
          return callback(error)
        }
        return RedisManager.updateDocument(
          project_id,
          doc_id,
          lines,
          version,
          [],
          new_ranges,
          {},
          function (error) {
            if (error != null) {
              return callback(error)
            }
            return callback()
          }
        )
      })
    })
  },

  deleteComment(project_id, doc_id, comment_id, _callback) {
    if (_callback == null) {
      _callback = function (error) {}
    }
    const timer = new Metrics.Timer('docManager.deleteComment')
    const callback = function (...args) {
      timer.done()
      return _callback(...Array.from(args || []))
    }

    return DocumentManager.getDoc(project_id, doc_id, function (
      error,
      lines,
      version,
      ranges
    ) {
      if (error != null) {
        return callback(error)
      }
      if (lines == null || version == null) {
        return callback(
          new Errors.NotFoundError(`document not found: ${doc_id}`)
        )
      }
      return RangesManager.deleteComment(comment_id, ranges, function (
        error,
        new_ranges
      ) {
        if (error != null) {
          return callback(error)
        }
        return RedisManager.updateDocument(
          project_id,
          doc_id,
          lines,
          version,
          [],
          new_ranges,
          {},
          function (error) {
            if (error != null) {
              return callback(error)
            }
            return callback()
          }
        )
      })
    })
  },

  renameDoc(project_id, doc_id, user_id, update, projectHistoryId, _callback) {
    if (_callback == null) {
      _callback = function (error) {}
    }
    const timer = new Metrics.Timer('docManager.updateProject')
    const callback = function (...args) {
      timer.done()
      return _callback(...Array.from(args || []))
    }

    return RedisManager.renameDoc(
      project_id,
      doc_id,
      user_id,
      update,
      projectHistoryId,
      callback
    )
  },

  getDocAndFlushIfOld(project_id, doc_id, callback) {
    if (callback == null) {
      callback = function (error, doc) {}
    }
    return DocumentManager.getDoc(project_id, doc_id, function (
      error,
      lines,
      version,
      ranges,
      pathname,
      projectHistoryId,
      unflushedTime,
      alreadyLoaded
    ) {
      if (error != null) {
        return callback(error)
      }
      // if doc was already loaded see if it needs to be flushed
      if (
        alreadyLoaded &&
        unflushedTime != null &&
        Date.now() - unflushedTime > MAX_UNFLUSHED_AGE
      ) {
        return DocumentManager.flushDocIfLoaded(project_id, doc_id, function (
          error
        ) {
          if (error != null) {
            return callback(error)
          }
          return callback(null, lines, version)
        })
      } else {
        return callback(null, lines, version)
      }
    })
  },

  resyncDocContents(project_id, doc_id, callback) {
    logger.log({ project_id, doc_id }, 'start resyncing doc contents')
    return RedisManager.getDoc(project_id, doc_id, function (
      error,
      lines,
      version,
      ranges,
      pathname,
      projectHistoryId
    ) {
      if (error != null) {
        return callback(error)
      }

      if (lines == null || version == null) {
        logger.log(
          { project_id, doc_id },
          'resyncing doc contents - not found in redis - retrieving from web'
        )
        return PersistenceManager.getDoc(project_id, doc_id, function (
          error,
          lines,
          version,
          ranges,
          pathname,
          projectHistoryId
        ) {
          if (error != null) {
            logger.error(
              { project_id, doc_id, getDocError: error },
              'resyncing doc contents - error retrieving from web'
            )
            return callback(error)
          }
          return ProjectHistoryRedisManager.queueResyncDocContent(
            project_id,
            projectHistoryId,
            doc_id,
            lines,
            version,
            pathname,
            callback
          )
        })
      } else {
        logger.log(
          { project_id, doc_id },
          'resyncing doc contents - doc in redis - will queue in redis'
        )
        return ProjectHistoryRedisManager.queueResyncDocContent(
          project_id,
          projectHistoryId,
          doc_id,
          lines,
          version,
          pathname,
          callback
        )
      }
    })
  },

  getDocWithLock(project_id, doc_id, callback) {
    if (callback == null) {
      callback = function (error, lines, version) {}
    }
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.getDoc,
      project_id,
      doc_id,
      callback
    )
  },

  getDocAndRecentOpsWithLock(project_id, doc_id, fromVersion, callback) {
    if (callback == null) {
      callback = function (
        error,
        lines,
        version,
        ops,
        ranges,
        pathname,
        projectHistoryId
      ) {}
    }
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.getDocAndRecentOps,
      project_id,
      doc_id,
      fromVersion,
      callback
    )
  },

  getDocAndFlushIfOldWithLock(project_id, doc_id, callback) {
    if (callback == null) {
      callback = function (error, doc) {}
    }
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.getDocAndFlushIfOld,
      project_id,
      doc_id,
      callback
    )
  },

  setDocWithLock(
    project_id,
    doc_id,
    lines,
    source,
    user_id,
    undoing,
    callback
  ) {
    if (callback == null) {
      callback = function (error) {}
    }
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.setDoc,
      project_id,
      doc_id,
      lines,
      source,
      user_id,
      undoing,
      callback
    )
  },

  flushDocIfLoadedWithLock(project_id, doc_id, callback) {
    if (callback == null) {
      callback = function (error) {}
    }
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.flushDocIfLoaded,
      project_id,
      doc_id,
      callback
    )
  },

  flushAndDeleteDocWithLock(project_id, doc_id, options, callback) {
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.flushAndDeleteDoc,
      project_id,
      doc_id,
      options,
      callback
    )
  },

  acceptChangesWithLock(project_id, doc_id, change_ids, callback) {
    if (callback == null) {
      callback = function (error) {}
    }
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.acceptChanges,
      project_id,
      doc_id,
      change_ids,
      callback
    )
  },

  deleteCommentWithLock(project_id, doc_id, thread_id, callback) {
    if (callback == null) {
      callback = function (error) {}
    }
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.deleteComment,
      project_id,
      doc_id,
      thread_id,
      callback
    )
  },

  renameDocWithLock(
    project_id,
    doc_id,
    user_id,
    update,
    projectHistoryId,
    callback
  ) {
    if (callback == null) {
      callback = function (error) {}
    }
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.renameDoc,
      project_id,
      doc_id,
      user_id,
      update,
      projectHistoryId,
      callback
    )
  },

  resyncDocContentsWithLock(project_id, doc_id, callback) {
    if (callback == null) {
      callback = function (error) {}
    }
    const UpdateManager = require('./UpdateManager')
    return UpdateManager.lockUpdatesAndDo(
      DocumentManager.resyncDocContents,
      project_id,
      doc_id,
      callback
    )
  }
}
