const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
const ProjectGetter = require('../Project/ProjectGetter')
const { User } = require('../../models/User')
const PrivilegeLevels = require('./PrivilegeLevels')
const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
const PublicAccessLevels = require('./PublicAccessLevels')
const Errors = require('../Errors/Errors')
const { ObjectId } = require('mongodb')
const { promisifyAll } = require('../../util/promises')

const AuthorizationManager = {
  isRestrictedUser(userId, privilegeLevel, isTokenMember) {
    if (privilegeLevel === PrivilegeLevels.NONE) {
      return true
    }
    return (
      privilegeLevel === PrivilegeLevels.READ_ONLY && (isTokenMember || !userId)
    )
  },

  isRestrictedUserForProject(userId, projectId, token, callback) {
    AuthorizationManager.getPrivilegeLevelForProject(
      userId,
      projectId,
      token,
      (err, privilegeLevel) => {
        if (err) {
          return callback(err)
        }
        CollaboratorsHandler.userIsTokenMember(
          userId,
          projectId,
          (err, isTokenMember) => {
            if (err) {
              return callback(err)
            }
            callback(
              null,
              AuthorizationManager.isRestrictedUser(
                userId,
                privilegeLevel,
                isTokenMember
              )
            )
          }
        )
      }
    )
  },

  getPublicAccessLevel(projectId, callback) {
    if (!ObjectId.isValid(projectId)) {
      return callback(new Error('invalid project id'))
    }
    // Note, the Project property in the DB is `publicAccesLevel`, without the second `s`
    ProjectGetter.getProject(
      projectId,
      { publicAccesLevel: 1 },
      function (error, project) {
        if (error) {
          return callback(error)
        }
        if (!project) {
          return callback(
            new Errors.NotFoundError(`no project found with id ${projectId}`)
          )
        }
        callback(null, project.publicAccesLevel)
      }
    )
  },

  // Get the privilege level that the user has for the project
  // Returns:
  //	* privilegeLevel: "owner", "readAndWrite", of "readOnly" if the user has
  //	  access. false if the user does not have access
  //   * becausePublic: true if the access level is only because the project is public.
  //   * becauseSiteAdmin: true if access level is only because user is admin
  getPrivilegeLevelForProject(userId, projectId, token, callback) {
    if (userId) {
      AuthorizationManager.getPrivilegeLevelForProjectWithUser(
        userId,
        projectId,
        token,
        callback
      )
    } else {
      AuthorizationManager.getPrivilegeLevelForProjectWithoutUser(
        projectId,
        token,
        callback
      )
    }
  },

  // User is present, get their privilege level from database
  getPrivilegeLevelForProjectWithUser(userId, projectId, token, callback) {
    CollaboratorsGetter.getMemberIdPrivilegeLevel(
      userId,
      projectId,
      function (error, privilegeLevel) {
        if (error) {
          return callback(error)
        }
        if (privilegeLevel && privilegeLevel !== PrivilegeLevels.NONE) {
          // The user has direct access
          return callback(null, privilegeLevel, false, false)
        }
        AuthorizationManager.isUserSiteAdmin(userId, function (error, isAdmin) {
          if (error) {
            return callback(error)
          }
          if (isAdmin) {
            return callback(null, PrivilegeLevels.OWNER, false, true)
          }
          // Legacy public-access system
          // User is present (not anonymous), but does not have direct access
          AuthorizationManager.getPublicAccessLevel(
            projectId,
            function (err, publicAccessLevel) {
              if (err) {
                return callback(err)
              }
              if (publicAccessLevel === PublicAccessLevels.READ_ONLY) {
                return callback(null, PrivilegeLevels.READ_ONLY, true, false)
              }
              if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) {
                return callback(
                  null,
                  PrivilegeLevels.READ_AND_WRITE,
                  true,
                  false
                )
              }
              callback(null, PrivilegeLevels.NONE, false, false)
            }
          )
        })
      }
    )
  },

  // User is Anonymous, Try Token-based access
  getPrivilegeLevelForProjectWithoutUser(projectId, token, callback) {
    AuthorizationManager.getPublicAccessLevel(
      projectId,
      function (err, publicAccessLevel) {
        if (err) {
          return callback(err)
        }
        if (publicAccessLevel === PublicAccessLevels.READ_ONLY) {
          // Legacy public read-only access for anonymous user
          return callback(null, PrivilegeLevels.READ_ONLY, true, false)
        }
        if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) {
          // Legacy public read-write access for anonymous user
          return callback(null, PrivilegeLevels.READ_AND_WRITE, true, false)
        }
        if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) {
          return AuthorizationManager.getPrivilegeLevelForProjectWithToken(
            projectId,
            token,
            callback
          )
        }
        // Deny anonymous user access
        callback(null, PrivilegeLevels.NONE, false, false)
      }
    )
  },

  getPrivilegeLevelForProjectWithToken(projectId, token, callback) {
    // Anonymous users can have read-only access to token-based projects,
    // while read-write access must be logged in,
    // unless the `enableAnonymousReadAndWriteSharing` setting is enabled
    TokenAccessHandler.validateTokenForAnonymousAccess(
      projectId,
      token,
      function (err, isValidReadAndWrite, isValidReadOnly) {
        if (err) {
          return callback(err)
        }
        if (isValidReadOnly) {
          // Grant anonymous user read-only access
          return callback(null, PrivilegeLevels.READ_ONLY, false, false)
        }
        if (isValidReadAndWrite) {
          // Grant anonymous user read-and-write access
          return callback(null, PrivilegeLevels.READ_AND_WRITE, false, false)
        }
        // Deny anonymous access
        callback(null, PrivilegeLevels.NONE, false, false)
      }
    )
  },

  canUserReadProject(userId, projectId, token, callback) {
    AuthorizationManager.getPrivilegeLevelForProject(
      userId,
      projectId,
      token,
      function (error, privilegeLevel) {
        if (error) {
          return callback(error)
        }
        callback(
          null,
          [
            PrivilegeLevels.OWNER,
            PrivilegeLevels.READ_AND_WRITE,
            PrivilegeLevels.READ_ONLY
          ].includes(privilegeLevel)
        )
      }
    )
  },

  canUserWriteProjectContent(userId, projectId, token, callback) {
    AuthorizationManager.getPrivilegeLevelForProject(
      userId,
      projectId,
      token,
      function (error, privilegeLevel) {
        if (error) {
          return callback(error)
        }
        callback(
          null,
          [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
            privilegeLevel
          )
        )
      }
    )
  },

  canUserWriteProjectSettings(userId, projectId, token, callback) {
    AuthorizationManager.getPrivilegeLevelForProject(
      userId,
      projectId,
      token,
      function (error, privilegeLevel, becausePublic) {
        if (error) {
          return callback(error)
        }
        if (privilegeLevel === PrivilegeLevels.OWNER) {
          return callback(null, true)
        }
        if (
          privilegeLevel === PrivilegeLevels.READ_AND_WRITE &&
          !becausePublic
        ) {
          return callback(null, true)
        }
        callback(null, false)
      }
    )
  },

  canUserAdminProject(userId, projectId, token, callback) {
    AuthorizationManager.getPrivilegeLevelForProject(
      userId,
      projectId,
      token,
      function (error, privilegeLevel, becausePublic, becauseSiteAdmin) {
        if (error) {
          return callback(error)
        }
        callback(
          null,
          privilegeLevel === PrivilegeLevels.OWNER,
          becauseSiteAdmin
        )
      }
    )
  },

  isUserSiteAdmin(userId, callback) {
    if (!userId) {
      return callback(null, false)
    }
    User.findOne({ _id: userId }, { isAdmin: 1 }, function (error, user) {
      if (error) {
        return callback(error)
      }
      callback(null, (user && user.isAdmin) === true)
    })
  }
}

module.exports = AuthorizationManager
module.exports.promises = promisifyAll(AuthorizationManager, {
  without: 'isRestrictedUser'
})
