const request = require('./request')
const settings = require('settings-sharelatex')
const { db, ObjectId } = require('../../../../app/src/infrastructure/mongodb')
const UserModel = require('../../../../app/src/models/User').User
const UserUpdater = require('../../../../app/src/Features/User/UserUpdater')
const AuthenticationManager = require('../../../../app/src/Features/Authentication/AuthenticationManager')
const { promisify } = require('util')
const fs = require('fs')
const Path = require('path')

let count = 0

class User {
  constructor(options) {
    if (options == null) {
      options = {}
    }
    this.emails = [
      {
        email: options.email || `acceptance-test-${count}@example.com`,
        createdAt: new Date()
      }
    ]
    this.email = this.emails[0].email
    this.password = `a-terrible-secret-${count}`
    count++
    this.jar = request.jar()
    this.request = request.defaults({
      jar: this.jar
    })
  }

  setExtraAttributes(user) {
    if ((user != null ? user._id : undefined) == null) {
      throw new Error('User does not exist')
    }
    this.id = user._id.toString()
    this._id = user._id.toString()
    this.first_name = user.first_name
    this.referal_id = user.referal_id
  }

  get(callback) {
    db.users.findOne({ _id: ObjectId(this._id) }, callback)
  }

  mongoUpdate(updateOp, callback) {
    db.users.updateOne({ _id: ObjectId(this._id) }, updateOp, callback)
  }

  register(callback) {
    this.registerWithQuery('', callback)
  }

  registerWithQuery(query, callback) {
    if (this._id != null) {
      return callback(new Error('User already registered'))
    }
    this.getCsrfToken(error => {
      if (error != null) {
        return callback(error)
      }
      this.request.post(
        {
          url: `/register${query}`,
          json: { email: this.email, password: this.password }
        },
        (error, response, body) => {
          if (error != null) {
            return callback(error)
          }
          db.users.findOne({ email: this.email }, (error, user) => {
            if (error != null) {
              return callback(error)
            }
            this.setExtraAttributes(user)
            callback(null, user)
          })
        }
      )
    })
  }

  login(callback) {
    this.loginWith(this.email, callback)
  }

  loginWith(email, callback) {
    this.ensureUserExists(error => {
      if (error != null) {
        return callback(error)
      }
      this.getCsrfToken(error => {
        if (error != null) {
          return callback(error)
        }
        this.request.post(
          {
            url: settings.enableLegacyLogin ? '/login/legacy' : '/login',
            json: { email, password: this.password }
          },
          callback
        )
      })
    })
  }

  ensureUserExists(callback) {
    const filter = { email: this.email }
    const options = { upsert: true, new: true, setDefaultsOnInsert: true }
    UserModel.findOneAndUpdate(filter, {}, options, (error, user) => {
      if (error != null) {
        return callback(error)
      }
      this.setExtraAttributes(user)
      AuthenticationManager.setUserPasswordInV2(user, this.password, error => {
        if (error != null) {
          return callback(error)
        }
        this.mongoUpdate({ $set: { emails: this.emails } }, error => {
          if (error != null) {
            return callback(error)
          }
          callback(null, this.password)
        })
      })
    })
  }

  setFeatures(features, callback) {
    const update = {}
    for (let key in features) {
      const value = features[key]
      update[`features.${key}`] = value
    }
    UserModel.updateOne({ _id: this.id }, update, callback)
  }

  setFeaturesOverride(featuresOverride, callback) {
    const update = { $push: { featuresOverrides: featuresOverride } }
    UserModel.updateOne({ _id: this.id }, update, callback)
  }

  setOverleafId(overleafId, callback) {
    UserModel.updateOne(
      { _id: this.id },
      { 'overleaf.id': overleafId },
      callback
    )
  }

  logout(callback) {
    this.getCsrfToken(error => {
      if (error != null) {
        return callback(error)
      }
      this.request.post(
        {
          url: '/logout',
          json: {
            email: this.email,
            password: this.password
          }
        },
        (error, response, body) => {
          if (error != null) {
            return callback(error)
          }
          db.users.findOne({ email: this.email }, (error, user) => {
            if (error != null) {
              return callback(error)
            }
            if (user == null) {
              return callback()
            }
            this.id = user._id.toString()
            this._id = user._id.toString()
            callback()
          })
        }
      )
    })
  }

  addEmail(email, callback) {
    this.emails.push({ email, createdAt: new Date() })
    UserUpdater.addEmailAddress(
      this.id,
      email,
      {},
      { initiatorId: this._id, ipAddress: '127:0:0:0' },
      callback
    )
  }

  confirmEmail(email, callback) {
    for (let idx = 0; idx < this.emails.length; idx++) {
      const emailData = this.emails[idx]
      if (emailData.email === email) {
        this.emails[idx].confirmedAt = new Date()
      }
    }
    UserUpdater.confirmEmail(this.id, email, callback)
  }

  ensureAdmin(callback) {
    this.mongoUpdate({ $set: { isAdmin: true } }, callback)
  }

  ensureStaffAccess(flag, callback) {
    const update = { $set: {} }
    update.$set[`staffAccess.${flag}`] = true
    this.mongoUpdate(update, callback)
  }

  upgradeFeatures(callback) {
    const features = {
      collaborators: -1, // Infinite
      versioning: true,
      dropbox: true,
      compileTimeout: 60,
      compileGroup: 'priority',
      templates: true,
      references: true,
      trackChanges: true,
      trackChangesVisible: true
    }
    this.mongoUpdate({ $set: { features } }, callback)
  }

  downgradeFeatures(callback) {
    const features = {
      collaborators: 1,
      versioning: false,
      dropbox: false,
      compileTimeout: 60,
      compileGroup: 'standard',
      templates: false,
      references: false,
      trackChanges: false,
      trackChangesVisible: false
    }
    this.mongoUpdate({ $set: { features } }, callback)
  }

  defaultFeatures(callback) {
    const features = settings.defaultFeatures
    this.mongoUpdate({ $set: { features } }, callback)
  }

  getFeatures(callback) {
    db.users.findOne(
      { _id: ObjectId(this.id) },
      { projection: { features: 1 } },
      (error, user) => callback(error, user && user.features)
    )
  }

  fullDeleteUser(email, callback) {
    db.users.findOne({ email }, (error, user) => {
      if (error != null) {
        return callback(error)
      }
      if (user == null) {
        return callback()
      }
      const userId = user._id
      db.projects.deleteMany({ owner_ref: ObjectId(userId) }, err => {
        if (err != null) {
          callback(err)
        }
        db.users.deleteOne({ _id: ObjectId(userId) }, callback)
      })
    })
  }

  deleteUser(callback) {
    this.getCsrfToken(error => {
      if (error) {
        return callback(error)
      }
      this.request.post(
        {
          url: '/user/delete',
          json: { password: this.password }
        },
        (err, res) => {
          if (err) {
            return callback(err)
          }
          if (res.statusCode < 200 || res.statusCode >= 300) {
            return callback(
              new Error('Error received from API: ' + res.statusCode)
            )
          }

          callback()
        }
      )
    })
  }

  getProject(projectId, callback) {
    db.projects.findOne({ _id: ObjectId(projectId.toString()) }, callback)
  }

  saveProject(project, callback) {
    db.projects.updateOne({ _id: project._id }, { $set: project }, callback)
  }

  createProject(name, options, callback) {
    if (typeof options === 'function') {
      callback = options
      options = {}
    }

    this.request.post(
      {
        url: '/project/new',
        json: Object.assign({ projectName: name }, options)
      },
      (error, response, body) => {
        if (error != null) {
          return callback(error)
        }
        if ((body != null ? body.project_id : undefined) == null) {
          error = new Error(
            JSON.stringify([
              'SOMETHING WENT WRONG CREATING PROJECT',
              name,
              options,
              response.statusCode,
              response.headers.location,
              body
            ])
          )
          callback(error)
        } else {
          callback(null, body.project_id)
        }
      }
    )
  }

  deleteProject(projectId, callback) {
    this.request.delete(
      {
        url: `/project/${projectId}`
      },
      (error, response, body) => {
        if (error != null) {
          return callback(error)
        }
        callback(null)
      }
    )
  }

  deleteProjects(callback) {
    db.projects.deleteMany({ owner_ref: ObjectId(this.id) }, callback)
  }

  openProject(projectId, callback) {
    this.request.get(
      {
        url: `/project/${projectId}`
      },
      (error, response, body) => {
        if (error != null) {
          return callback(error)
        }
        if (response.statusCode !== 200) {
          const err = new Error(
            `Non-success response when opening project: ${response.statusCode}`
          )
          return callback(err)
        }
        callback(null)
      }
    )
  }

  createDocInProject(projectId, parentFolderId, name, callback) {
    this.getCsrfToken(error => {
      if (error != null) {
        return callback(error)
      }
      this.request.post(
        {
          url: `/project/${projectId}/doc`,
          json: {
            name,
            parentFolderId
          }
        },
        (error, response, body) => {
          if (error != null) {
            return callback(error)
          }
          callback(null, body._id)
        }
      )
    })
  }

  uploadFileInProject(projectId, folderId, file, name, contentType, callback) {
    const imageFile = fs.createReadStream(
      Path.resolve(Path.join(__dirname, '..', '..', 'files', file))
    )

    this.request.post(
      {
        uri: `project/${projectId}/upload`,
        qs: {
          folder_id: String(folderId)
        },
        formData: {
          qqfile: {
            value: imageFile,
            options: {
              filename: name,
              contentType: contentType
            }
          }
        }
      },
      (error, res, body) => {
        if (error) {
          return callback(error)
        }
        if (res.statusCode < 200 || res.statusCode >= 300) {
          return callback(new Error(`failed to upload file ${res.statusCode}`))
        }

        callback(null, JSON.parse(body).entity_id)
      }
    )
  }

  uploadExampleFileInProject(projectId, folderId, name, callback) {
    this.uploadFileInProject(
      projectId,
      folderId,
      '1pixel.png',
      name,
      'image/png',
      callback
    )
  }

  moveItemInProject(projectId, type, itemId, folderId, callback) {
    this.request.post(
      {
        uri: `project/${projectId}/${type}/${itemId}/move`,
        json: {
          folder_id: folderId
        }
      },
      (error, res) => {
        if (error) {
          return callback(error)
        }
        if (res.statusCode < 200 || res.statusCode >= 300) {
          return callback(new Error(`failed to move ${type} ${res.statusCode}`))
        }

        callback()
      }
    )
  }

  renameItemInProject(projectId, type, itemId, name, callback) {
    this.request.post(
      {
        uri: `project/${projectId}/${type}/${itemId}/rename`,
        json: {
          name: name
        }
      },
      (error, res) => {
        if (error) {
          return callback(error)
        }
        if (res.statusCode < 200 || res.statusCode >= 300) {
          return callback(
            new Error(`failed to rename ${type} ${res.statusCode}`)
          )
        }

        callback()
      }
    )
  }

  deleteItemInProject(projectId, type, itemId, callback) {
    this.request.delete(
      `project/${projectId}/${type}/${itemId}`,
      (error, res) => {
        if (error) {
          return callback(error)
        }
        if (res.statusCode < 200 || res.statusCode >= 300) {
          return callback(
            new Error(`failed to delete ${type} ${res.statusCode}`)
          )
        }
        callback()
      }
    )
  }

  addUserToProject(projectId, user, privileges, callback) {
    let updateOp
    if (privileges === 'readAndWrite') {
      updateOp = { $addToSet: { collaberator_refs: user._id } }
    } else if (privileges === 'readOnly') {
      updateOp = { $addToSet: { readOnly_refs: user._id } }
    }
    db.projects.updateOne({ _id: ObjectId(projectId) }, updateOp, callback)
  }

  makePublic(projectId, level, callback) {
    this.request.post(
      {
        url: `/project/${projectId}/settings/admin`,
        json: {
          publicAccessLevel: level
        }
      },
      (error, response, body) => {
        if (error != null) {
          return callback(error)
        }
        callback(null)
      }
    )
  }

  makePrivate(projectId, callback) {
    this.request.post(
      {
        url: `/project/${projectId}/settings/admin`,
        json: {
          publicAccessLevel: 'private'
        }
      },
      (error, response, body) => {
        if (error != null) {
          return callback(error)
        }
        callback(null)
      }
    )
  }

  makeTokenBased(projectId, callback) {
    this.request.post(
      {
        url: `/project/${projectId}/settings/admin`,
        json: {
          publicAccessLevel: 'tokenBased'
        }
      },
      (error, response, body) => {
        if (error != null) {
          return callback(error)
        }
        callback(null)
      }
    )
  }

  getCsrfToken(callback) {
    this.request.get(
      {
        url: '/dev/csrf'
      },
      (err, response, body) => {
        if (err != null) {
          return callback(err)
        }
        this.csrfToken = body
        this.request = this.request.defaults({
          headers: {
            'x-csrf-token': this.csrfToken
          }
        })
        callback()
      }
    )
  }

  changePassword(callback) {
    this.getCsrfToken(error => {
      if (error != null) {
        return callback(error)
      }
      this.request.post(
        {
          url: '/user/password/update',
          json: {
            currentPassword: this.password,
            newPassword1: this.password,
            newPassword2: this.password
          }
        },
        callback
      )
    })
  }

  reconfirmAccountRequest(userEmail, callback) {
    this.getCsrfToken(error => {
      if (error != null) {
        return callback(error)
      }
      this.request.post(
        {
          url: '/user/reconfirm',
          json: {
            email: userEmail
          }
        },
        (error, response, body) => {
          callback(error, response)
        }
      )
    })
  }

  getUserSettingsPage(callback) {
    this.getCsrfToken(error => {
      if (error != null) {
        return callback(error)
      }
      this.request.get(
        {
          url: '/user/settings'
        },
        (error, response, body) => {
          if (error != null) {
            return callback(error)
          }
          callback(null, response.statusCode)
        }
      )
    })
  }

  updateSettings(newSettings, callback) {
    this.getCsrfToken(error => {
      if (error != null) {
        return callback(error)
      }
      this.request.post(
        {
          url: '/user/settings',
          json: newSettings
        },
        callback
      )
    })
  }

  getProjectListPage(callback) {
    this.getCsrfToken(error => {
      if (error != null) {
        return callback(error)
      }
      this.request.get(
        {
          url: '/project'
        },
        (error, response, body) => {
          if (error != null) {
            return callback(error)
          }
          callback(null, response.statusCode)
        }
      )
    })
  }

  isLoggedIn(callback) {
    this.request.get('/user/personal_info', (error, response, body) => {
      if (error != null) {
        return callback(error)
      }
      if (response.statusCode === 200) {
        callback(null, true)
      } else if (response.statusCode === 302) {
        callback(null, false)
      } else {
        callback(
          new Error(
            `unexpected status code from /user/personal_info: ${response.statusCode}`
          )
        )
      }
    })
  }

  transferProjectOwnership(projectId, userId, callback) {
    this.getCsrfToken(err => {
      if (err != null) {
        return callback(err)
      }
      this.request.post(
        {
          url: `/project/${projectId.toString()}/transfer-ownership`,
          json: {
            user_id: userId.toString()
          }
        },
        (err, response) => {
          if (err != null) {
            return callback(err)
          }
          if (response.statusCode !== 204) {
            return callback(
              new Error(`Unexpected status code: ${response.statusCode}`)
            )
          }
          callback()
        }
      )
    })
  }

  setV1Id(v1Id, callback) {
    UserModel.updateOne(
      {
        _id: this._id
      },
      {
        overleaf: {
          id: v1Id
        }
      },
      callback
    )
  }

  setCollaboratorInfo(projectId, userId, info, callback) {
    this.getCsrfToken(err => {
      if (err != null) {
        return callback(err)
      }
      this.request.put(
        {
          url: `/project/${projectId.toString()}/users/${userId.toString()}`,
          json: info
        },
        (err, response) => {
          if (err != null) {
            return callback(err)
          }
          if (response.statusCode !== 204) {
            return callback(
              new Error(`Unexpected status code: ${response.statusCode}`)
            )
          }
          callback()
        }
      )
    })
  }
}

User.promises = class extends User {
  doRequest(method, params) {
    return new Promise((resolve, reject) => {
      this.request[method.toLowerCase()](params, (err, response, body) => {
        if (err) {
          reject(err)
        } else {
          resolve({ response, body })
        }
      })
    })
  }
}

// promisify User class methods - works for methods with 0-1 output parameters,
// otherwise we will need to implement the method manually instead
const nonPromiseMethods = ['constructor', 'setExtraAttributes']
Object.getOwnPropertyNames(User.prototype).forEach(methodName => {
  const method = User.prototype[methodName]
  if (typeof method === 'function' && !nonPromiseMethods.includes(methodName)) {
    User.promises.prototype[methodName] = promisify(method)
  }
})

module.exports = User
