const loggerFactory = require('@alias/logger');

const logger = loggerFactory('growi:routes:apiv3:user-group'); // eslint-disable-line no-unused-vars

const express = require('express');

const router = express.Router();

const { body, param, query } = require('express-validator');
const { sanitizeQuery } = require('express-validator');

const mongoose = require('mongoose');

const ErrorV3 = require('../../models/vo/error-apiv3');

const { toPagingLimit, toPagingOffset } = require('../../util/express-validator/sanitizer');

const validator = {};

const { ObjectId } = mongoose.Types;


/**
 * @swagger
 *  tags:
 *    name: UserGroup
 */

module.exports = (crowi) => {
  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  const adminRequired = require('../../middlewares/admin-required')(crowi);
  const csrf = require('../../middlewares/csrf')(crowi);
  const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);

  const {
    UserGroup,
    UserGroupRelation,
    User,
    Page,
  } = crowi.models;

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups:
   *      get:
   *        tags: [UserGroup]
   *        operationId: getUserGroup
   *        summary: /user-groups
   *        description: Get usergroups
   *        responses:
   *          200:
   *            description: usergroups are fetched
   *            content:
   *              application/json:
   *                schema:
   *                  properties:
   *                    userGroups:
   *                      type: object
   *                      description: a result of `UserGroup.find`
   */
  router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
    // TODO: filter with querystring
    try {
      const page = parseInt(req.query.page) || 1;
      const result = await UserGroup.findUserGroupsWithPagination({ page });
      const { docs: userGroups, totalDocs: totalUserGroups, limit: pagingLimit } = result;
      return res.apiv3({ userGroups, totalUserGroups, pagingLimit });
    }
    catch (err) {
      const msg = 'Error occurred in fetching user group list';
      logger.error('Error', err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-list-fetch-failed'));
    }
  });

  validator.create = [
    body('name', 'Group name is required').trim().exists({ checkFalsy: true }),
  ];

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups:
   *      post:
   *        tags: [UserGroup]
   *        operationId: createUserGroup
   *        summary: /user-groups
   *        description: Adds userGroup
   *        requestBody:
   *          required: true
   *          content:
   *            application/json:
   *              schema:
   *                properties:
   *                  name:
   *                    type: string
   *                    description: name of the userGroup trying to be added
   *        responses:
   *          200:
   *            description: userGroup is added
   *            content:
   *              application/json:
   *                schema:
   *                  properties:
   *                    userGroup:
   *                      type: object
   *                      description: A result of `UserGroup.createGroupByName`
   */
  router.post('/', loginRequiredStrictly, adminRequired, csrf, validator.create, apiV3FormValidator, async(req, res) => {
    const { name } = req.body;

    try {
      const userGroupName = crowi.xss.process(name);
      const userGroup = await UserGroup.createGroupByName(userGroupName);

      return res.apiv3({ userGroup });
    }
    catch (err) {
      const msg = 'Error occurred in creating a user group';
      logger.error(msg, err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-create-failed'));
    }
  });

  validator.delete = [
    param('id').trim().exists({ checkFalsy: true }),
    query('actionName').trim().exists({ checkFalsy: true }),
    query('transferToUserGroupId').trim(),
  ];

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups/{id}:
   *      delete:
   *        tags: [UserGroup]
   *        operationId: deleteUserGroup
   *        summary: /user-groups/{id}
   *        description: Deletes userGroup
   *        parameters:
   *          - name: id
   *            in: path
   *            required: true
   *            description: id of userGroup
   *            schema:
   *              type: string
   *          - name: actionName
   *            in: query
   *            description: name of action
   *            schema:
   *              type: string
   *          - name: transferToUserGroupId
   *            in: query
   *            description: userGroup id that will be transferred to
   *            schema:
   *              type: string
   *        responses:
   *          200:
   *            description: userGroup is removed
   *            content:
   *              application/json:
   *                schema:
   *                  properties:
   *                    userGroups:
   *                      type: object
   *                      description: A result of `UserGroup.removeCompletelyById`
   */
  router.delete('/:id', loginRequiredStrictly, adminRequired, csrf, validator.delete, apiV3FormValidator, async(req, res) => {
    const { id: deleteGroupId } = req.params;
    const { actionName, transferToUserGroupId } = req.query;

    try {
      const userGroup = await UserGroup.removeCompletelyById(deleteGroupId, actionName, transferToUserGroupId);

      return res.apiv3({ userGroup });
    }
    catch (err) {
      const msg = 'Error occurred in deleting a user group';
      logger.error(msg, err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-delete-failed'));
    }
  });

  // return one group with the id
  // router.get('/:id', async(req, res) => {
  // });

  validator.update = [
    body('name', 'Group name is required').trim().exists({ checkFalsy: true }),
  ];

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups/{id}:
   *      put:
   *        tags: [UserGroup]
   *        operationId: updateUserGroups
   *        summary: /user-groups/{id}
   *        description: Update userGroup
   *        parameters:
   *          - name: id
   *            in: path
   *            required: true
   *            description: id of userGroup
   *            schema:
   *              type: string
   *        responses:
   *          200:
   *            description: userGroup is updated
   *            content:
   *              application/json:
   *                schema:
   *                  properties:
   *                    userGroup:
   *                      type: object
   *                      description: A result of `UserGroup.updateName`
   */
  router.put('/:id', loginRequiredStrictly, adminRequired, csrf, validator.update, apiV3FormValidator, async(req, res) => {
    const { id } = req.params;
    const { name } = req.body;

    try {
      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 isRegisterableName = await UserGroup.isRegisterableName(name);
      if (!isRegisterableName) {
        throw new Error('The group name is already taken');
      }

      await userGroup.updateName(name);

      res.apiv3({ userGroup });
    }
    catch (err) {
      const msg = 'Error occurred in updating a user group name';
      logger.error(msg, err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-update-failed'));
    }
  });

  validator.users = {};

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups/{id}/users:
   *      get:
   *        tags: [UserGroup]
   *        operationId: getUsersUserGroups
   *        summary: /user-groups/{id}/users
   *        description: Get users related to the userGroup
   *        parameters:
   *          - name: id
   *            in: path
   *            required: true
   *            description: id of userGroup
   *            schema:
   *              type: string
   *        responses:
   *          200:
   *            description: users are fetched
   *            content:
   *              application/json:
   *                schema:
   *                  properties:
   *                    users:
   *                      type: array
   *                      items:
   *                        type: object
   *                      description: user objects
   */
  router.get('/:id/users', loginRequiredStrictly, adminRequired, async(req, res) => {
    const { id } = req.params;

    try {
      const userGroup = await UserGroup.findById(id);
      const userGroupRelations = await UserGroupRelation.findAllRelationForUserGroup(userGroup);

      const users = userGroupRelations.map((userGroupRelation) => {
        return userGroupRelation.relatedUser;
      });

      return res.apiv3({ users });
    }
    catch (err) {
      const msg = `Error occurred in fetching users for group: ${id}`;
      logger.error(msg, err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-user-list-fetch-failed'));
    }
  });

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups/{id}/unrelated-users:
   *      get:
   *        tags: [UserGroup]
   *        operationId: getUnrelatedUsersUserGroups
   *        summary: /user-groups/{id}/unrelated-users
   *        description: Get users unrelated to the userGroup
   *        parameters:
   *          - name: id
   *            in: path
   *            required: true
   *            description: id of userGroup
   *            schema:
   *              type: string
   *        responses:
   *          200:
   *            description: users are fetched
   *            content:
   *              application/json:
   *                schema:
   *                  properties:
   *                    users:
   *                      type: array
   *                      items:
   *                        type: object
   *                      description: user objects
   */
  router.get('/:id/unrelated-users', loginRequiredStrictly, adminRequired, async(req, res) => {
    const { id } = req.params;
    const {
      searchWord, searchType, isAlsoNameSearched, isAlsoMailSearched,
    } = req.query;

    const queryOptions = {
      searchWord, searchType, isAlsoNameSearched, isAlsoMailSearched,
    };

    try {
      const userGroup = await UserGroup.findById(id);
      const users = await UserGroupRelation.findUserByNotRelatedGroup(userGroup, queryOptions);

      return res.apiv3({ users });
    }
    catch (err) {
      const msg = `Error occurred in fetching unrelated users for group: ${id}`;
      logger.error(msg, err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-unrelated-user-list-fetch-failed'));
    }
  });

  validator.users.post = [
    param('id').trim().exists({ checkFalsy: true }),
    param('username').trim().exists({ checkFalsy: true }),
  ];

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups/{id}/users:
   *      post:
   *        tags: [UserGroup]
   *        operationId: addUserUserGroups
   *        summary: /user-groups/{id}/users
   *        description: Add a user to the userGroup
   *        parameters:
   *          - name: id
   *            in: path
   *            required: true
   *            description: id of userGroup
   *            schema:
   *              type: string
   *        responses:
   *          200:
   *            description: a user is added
   *            content:
   *              application/json:
   *                schema:
   *                  type: object
   *                  properties:
   *                    user:
   *                      type: object
   *                      description: the user added to the group
   *                    userGroup:
   *                      type: object
   *                      description: the group to which a user was added
   *                    userGroupRelation:
   *                      type: object
   *                      description: the associative entity between user and userGroup
   */
  router.post('/:id/users/:username', loginRequiredStrictly, adminRequired, validator.users.post, apiV3FormValidator, async(req, res) => {
    const { id, username } = req.params;

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

      // check for duplicate users in groups
      const isRelatedUserForGroup = await UserGroupRelation.isRelatedUserForGroup(userGroup, user);

      if (isRelatedUserForGroup) {
        logger.warn('The user is already joined');
        return res.apiv3();
      }

      const userGroupRelation = await UserGroupRelation.createRelation(userGroup, user);
      await userGroupRelation.populate('relatedUser', User.USER_PUBLIC_FIELDS).execPopulate();

      return res.apiv3({ user, userGroup, userGroupRelation });
    }
    catch (err) {
      const msg = `Error occurred in adding the user "${username}" to group "${id}"`;
      logger.error(msg, err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-add-user-failed'));
    }
  });

  validator.users.delete = [
    param('id').trim().exists({ checkFalsy: true }),
    param('username').trim().exists({ checkFalsy: true }),
  ];

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups/{id}/users:
   *      delete:
   *        tags: [UserGroup]
   *        operationId: deleteUsersUserGroups
   *        summary: /user-groups/{id}/users
   *        description: remove a user from the userGroup
   *        parameters:
   *          - name: id
   *            in: path
   *            required: true
   *            description: id of userGroup
   *            schema:
   *              type: string
   *        responses:
   *          200:
   *            description: a user was removed
   *            content:
   *              application/json:
   *                schema:
   *                  type: object
   *                  properties:
   *                    user:
   *                      type: object
   *                      description: the user removed from the group
   *                    userGroup:
   *                      type: object
   *                      description: the group from which a user was removed
   *                    userGroupRelation:
   *                      type: object
   *                      description: the associative entity between user and userGroup
   */
  router.delete('/:id/users/:username', loginRequiredStrictly, adminRequired, validator.users.delete, apiV3FormValidator, async(req, res) => {
    const { id, username } = req.params;

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

      const userGroupRelation = await UserGroupRelation.findOne({ relatedUser: new ObjectId(user._id), relatedGroup: new ObjectId(userGroup._id) });
      if (userGroupRelation == null) {
        throw new Error(`Group "${id}" does not exist or user "${username}" does not belong to group "${id}"`);
      }

      await userGroupRelation.remove();

      return res.apiv3({ user, userGroup, userGroupRelation });
    }
    catch (err) {
      const msg = `Error occurred in removing the user "${username}" from group "${id}"`;
      logger.error(msg, err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-remove-user-failed'));
    }
  });

  validator.userGroupRelations = {};

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups/{id}/user-group-relations:
   *      get:
   *        tags: [UserGroup]
   *        operationId: getUserGroupRelationsUserGroups
   *        summary: /user-groups/{id}/user-group-relations
   *        description: Get the user group relations for the userGroup
   *        parameters:
   *          - name: id
   *            in: path
   *            required: true
   *            description: id of userGroup
   *            schema:
   *              type: string
   *        responses:
   *          200:
   *            description: user group relations are fetched
   *            content:
   *              application/json:
   *                schema:
   *                  properties:
   *                    userGroupRelations:
   *                      type: array
   *                      items:
   *                        type: object
   *                      description: userGroupRelation objects
   */
  router.get('/:id/user-group-relations', loginRequiredStrictly, adminRequired, async(req, res) => {
    const { id } = req.params;

    try {
      const userGroup = await UserGroup.findById(id);
      const userGroupRelations = await UserGroupRelation.findAllRelationForUserGroup(userGroup);

      return res.apiv3({ userGroupRelations });
    }
    catch (err) {
      const msg = `Error occurred in fetching user group relations for group: ${id}`;
      logger.error(msg, err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-user-group-relation-list-fetch-failed'));
    }
  });

  validator.pages = {};

  validator.pages.get = [
    param('id').trim().exists({ checkFalsy: true }),
    sanitizeQuery('limit').customSanitizer(toPagingLimit),
    sanitizeQuery('offset').customSanitizer(toPagingOffset),
  ];

  /**
   * @swagger
   *
   *  paths:
   *    /user-groups/{id}/pages:
   *      get:
   *        tags: [UserGroup]
   *        operationId: getPagesUserGroups
   *        summary: /user-groups/{id}/pages
   *        description: Get closed pages for the userGroup
   *        parameters:
   *          - name: id
   *            in: path
   *            required: true
   *            description: id of userGroup
   *            schema:
   *              type: string
   *        responses:
   *          200:
   *            description: pages are fetched
   *            content:
   *              application/json:
   *                schema:
   *                  properties:
   *                    pages:
   *                      type: array
   *                      items:
   *                        type: object
   *                      description: page objects
   */
  router.get('/:id/pages', loginRequiredStrictly, adminRequired, validator.pages.get, apiV3FormValidator, async(req, res) => {
    const { id } = req.params;
    const { limit, offset } = req.query;

    try {
      const { docs, total } = await Page.paginate({
        grant: Page.GRANT_USER_GROUP,
        grantedGroup: { $in: [id] },
      }, {
        offset,
        limit,
        populate: {
          path: 'lastUpdateUser',
          select: User.USER_PUBLIC_FIELDS,
        },
      });

      const current = offset / limit + 1;

      // TODO: create a common moudule for paginated response
      return res.apiv3({ total, current, pages: docs });
    }
    catch (err) {
      const msg = `Error occurred in fetching pages for group: ${id}`;
      logger.error(msg, err);
      return res.apiv3Err(new ErrorV3(msg, 'user-group-page-list-fetch-failed'));
    }
  });

  return router;
};
