import mongodb from 'mongodb-legacy'
import { callbackify } from '@overleaf/promise-utils'
import { Institution } from '../../models/Institution.mjs'
import { Subscription } from '../../models/Subscription.mjs'
import { Publisher } from '../../models/Publisher.mjs'
import UserMembershipViewModel from './UserMembershipViewModel.mjs'
import UserGetter from '../User/UserGetter.mjs'
import UserMembershipErrors from './UserMembershipErrors.mjs'
import Modules from '../../infrastructure/Modules.mjs'
import mongoose from '../../infrastructure/Mongoose.mjs'
import SubscriptionUpdater from '../Subscription/SubscriptionUpdater.mjs'

const { ObjectId } = mongodb

const EntityModels = { Institution, Subscription, Publisher }

const UserMembershipHandler = {
  async getEntityWithoutAuthorizationCheck(entityId, entityConfig) {
    const query = buildEntityQuery(entityId, entityConfig)
    return await EntityModels[entityConfig.modelName].findOne(query).exec()
  },

  async createEntity(entityId, entityConfig) {
    const data = buildEntityQuery(entityId, entityConfig)
    return await EntityModels[entityConfig.modelName].create(data)
  },

  async getUsers(entity, entityConfig) {
    const attributes = entityConfig.fields.read
    return await getPopulatedListOfMembers(entity, attributes)
  },

  async addUser(entity, entityConfig, email, auditInfo) {
    const attribute = entityConfig.fields.write
    const user = await UserGetter.promises.getUserByAnyEmail(email)

    if (!user) {
      throw new UserMembershipErrors.UserNotFoundError()
    }

    if (entity[attribute].some(managerId => managerId.equals(user._id))) {
      throw new UserMembershipErrors.UserAlreadyAddedError()
    }

    // if the entity is a Subscription with managed users enabled, then audit log the event
    if (
      entityConfig.modelName === 'Subscription' &&
      entity.managedUsersEnabled
    ) {
      const session = await mongoose.startSession()
      try {
        await session.withTransaction(async () => {
          const auditLog = {
            groupId: entity._id,
            operation: 'group-role-changed',
            initiatorId: auditInfo.initiatorId,
            ipAddress: auditInfo.ipAddress,
            info: {
              userId: user._id,
              role: 'manager',
            },
          }
          await Modules.promises.hooks.fire(
            'addGroupAuditLogEntry',
            auditLog,
            session
          )
        })
      } finally {
        await session.endSession()
      }
    }

    await addUserToEntity(entity, attribute, user)

    if (
      entityConfig.modelName === 'Subscription' &&
      attribute === 'manager_ids'
    ) {
      await SubscriptionUpdater.promises.sendGroupRoleUserProperty(user._id)
    }

    return UserMembershipViewModel.build(user)
  },

  async removeUser(entity, entityConfig, userId, auditInfo) {
    const attribute = entityConfig.fields.write
    if (entity.admin_id ? entity.admin_id.equals(userId) : undefined) {
      throw new UserMembershipErrors.UserIsManagerError()
    }

    // if the entity is a Subscription with managed users enabled, then audit log the event
    if (
      entityConfig.modelName === 'Subscription' &&
      entity.managedUsersEnabled
    ) {
      const auditLog = {
        groupId: entity._id,
        operation: 'group-role-changed',
        initiatorId: auditInfo.initiatorId,
        ipAddress: auditInfo.ipAddress,
        info: {
          userId,
          role: 'member',
        },
      }
      await Modules.promises.hooks.fire('addGroupAuditLogEntry', auditLog)
    }

    const result = await removeUserFromEntity(entity, attribute, userId)

    if (
      entityConfig.modelName === 'Subscription' &&
      attribute === 'manager_ids'
    ) {
      await SubscriptionUpdater.promises.sendGroupRoleUserProperty(userId)
    }

    return result
  },
}

async function getPopulatedListOfMembers(entity, attributes) {
  const userObjects = []

  for (const attribute of attributes) {
    for (const userObject of entity[attribute] || []) {
      // userObject can be an email as String, a user id as ObjectId or an
      // invite as Object with an email attribute as String. We want to pass to
      // UserMembershipViewModel either an email as (String) or a user id (ObjectId)
      const userIdOrEmail = userObject.email || userObject
      userObjects.push(userIdOrEmail)
    }
  }

  const users = await UserMembershipViewModel.promises.buildAsync(userObjects)

  for (const user of users) {
    if (
      user?._id &&
      entity?.admin_id &&
      user._id.toString() === entity.admin_id.toString()
    ) {
      user.isEntityAdmin = true
    }
    if (
      user?._id &&
      entity?.manager_ids &&
      entity.manager_ids.includes(user._id)
    ) {
      user.isEntityManager = true
    }
    if (
      user?._id &&
      entity?.member_ids &&
      entity.member_ids.includes(user._id)
    ) {
      user.isEntityMember = true
    }
  }

  return users
}

async function addUserToEntity(entity, attribute, user) {
  const fieldUpdate = {}
  fieldUpdate[attribute] = user._id
  return await entity.updateOne({ $addToSet: fieldUpdate }).exec()
}

async function removeUserFromEntity(entity, attribute, userId) {
  const fieldUpdate = {}
  fieldUpdate[attribute] = userId
  return await entity.updateOne({ $pull: fieldUpdate }).exec()
}

function buildEntityQuery(entityId, entityConfig) {
  if (ObjectId.isValid(entityId.toString())) {
    entityId = new ObjectId(entityId)
  }
  const query = Object.assign({}, entityConfig.baseQuery)
  query[entityConfig.fields.primaryKey] = entityId
  return query
}

export default {
  getEntityWithoutAuthorizationCheck: callbackify(
    UserMembershipHandler.getEntityWithoutAuthorizationCheck
  ),
  createEntity: callbackify(UserMembershipHandler.createEntity),
  getUsers: callbackify(UserMembershipHandler.getUsers),
  addUser: callbackify(UserMembershipHandler.addUser),
  removeUser: callbackify(UserMembershipHandler.removeUser),
  promises: UserMembershipHandler,
}
