import mongoose from 'mongoose';


import { IUser } from '~/interfaces/user';
import { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
import UserGroup from '~/server/models/user-group';
import { excludeTestIdsFromTargetIds, isIncludesObjectId } from '~/server/util/compare-objectId';
import loggerFactory from '~/utils/logger';

const logger = loggerFactory('growi:service:UserGroupService'); // eslint-disable-line no-unused-vars


const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model

/**
 * the service class of UserGroupService
 */
class UserGroupService {

  crowi: any;

  constructor(crowi) {
    this.crowi = crowi;
  }

  async init() {
    logger.debug('removing all invalid relations');
    return UserGroupRelation.removeAllInvalidRelations();
  }

  // ref: https://dev.growi.org/61b2cdabaa330ce7d8152844
  async updateGroup(id, name?: string, description?: string, parentId?: string | null, forceUpdateParents = false) {
    const userGroup = await UserGroup.findById(id);
    if (userGroup == null) {
      throw new Error('The group does not exist');
    }

    // check if the new group name is available
    const isExist = (await UserGroup.countDocuments({ name })) > 0;
    if (userGroup.name !== name && isExist) {
      throw new Error('The group name is already taken');
    }

    if (name != null) {
      userGroup.name = name;
    }
    if (description != null) {
      userGroup.description = description;
    }

    // return when not update parent
    if (userGroup.parent === parentId) {
      return userGroup.save();
    }

    /*
     * Update parent
     */
    if (parentId === undefined) { // undefined will be ignored
      return userGroup.save();
    }

    // set parent to null and return when parentId is null
    if (parentId == null) {
      userGroup.parent = null;
      return userGroup.save();
    }


    const parent = await UserGroup.findById(parentId);

    if (parent == null) { // it should not be null
      throw Error('Parent group does not exist.');
    }

    /*
     * check if able to update parent or not
     */

    // throw if parent was in self and its descendants
    const descendantsWithTarget = await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
    if (isIncludesObjectId(descendantsWithTarget.map(d => d._id), parent._id)) {
      throw Error('It is not allowed to choose parent from descendant groups.');
    }

    // find users for comparison
    const [targetGroupUsers, parentGroupUsers] = await Promise.all(
      [UserGroupRelation.findUserIdsByGroupId(userGroup._id), UserGroupRelation.findUserIdsByGroupId(parent._id)],
    );
    const usersBelongsToTargetButNotParent = excludeTestIdsFromTargetIds(targetGroupUsers, parentGroupUsers);

    // save if no users exist in both target and parent groups
    if (targetGroupUsers.length === 0 && parentGroupUsers.length === 0) {
      userGroup.parent = parent._id;
      return userGroup.save();
    }

    // add the target group's users to all ancestors
    if (forceUpdateParents) {
      const ancestorGroups = await UserGroup.findGroupsWithAncestorsRecursively(parent);
      const ancestorGroupIds = ancestorGroups.map(group => group._id);

      await UserGroupRelation.createByGroupIdsAndUserIds(ancestorGroupIds, usersBelongsToTargetButNotParent);
    }
    // throw if any of users in the target group is NOT included in the parent group
    else {
      const isUpdatable = usersBelongsToTargetButNotParent.length === 0;
      if (!isUpdatable) {
        throw Error('The parent group does not contain the users in this group.');
      }
    }

    userGroup.parent = parent._id;
    return userGroup.save();
  }

  async removeCompletelyByRootGroupId(deleteRootGroupId, action, transferToUserGroupId, user) {
    const rootGroup = await UserGroup.findById(deleteRootGroupId);
    if (rootGroup == null) {
      throw new Error(`UserGroup data does not exist. id: ${deleteRootGroupId}`);
    }

    const groupsToDelete = await UserGroup.findGroupsWithDescendantsRecursively([rootGroup]);

    // 1. update page & remove all groups
    await this.crowi.pageService.handlePrivatePagesForGroupsToDelete(groupsToDelete, action, transferToUserGroupId, user);
    // 2. remove all groups
    const deletedGroups = await UserGroup.deleteMany({ _id: { $in: groupsToDelete.map(g => g._id) } });
    // 3. remove all relations
    await UserGroupRelation.removeAllByUserGroups(groupsToDelete);

    return deletedGroups;
  }

  async removeUserByUsername(userGroupId: ObjectIdLike, username: string): Promise<{user: IUser, deletedGroupsCount: number}> {
    const User = this.crowi.model('User');

    const [userGroup, user] = await Promise.all([
      UserGroup.findById(userGroupId),
      User.findUserByUsername(username),
    ]);

    const groupsOfRelationsToDelete = await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
    const relatedGroupIdsToDelete = groupsOfRelationsToDelete.map(g => g._id);

    const deleteManyRes = await UserGroupRelation.deleteMany({ relatedUser: user._id, relatedGroup: { $in: relatedGroupIdsToDelete } });

    return { user, deletedGroupsCount: deleteManyRes.deletedCount };
  }

}

module.exports = UserGroupService;
