/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package review

import (
	"context"

	"github.com/apache/answer/internal/base/constant"
	"github.com/apache/answer/internal/base/pager"
	"github.com/apache/answer/internal/base/reason"
	"github.com/apache/answer/internal/entity"
	"github.com/apache/answer/internal/schema"
	answercommon "github.com/apache/answer/internal/service/answer_common"
	commentcommon "github.com/apache/answer/internal/service/comment_common"
	"github.com/apache/answer/internal/service/noticequeue"
	"github.com/apache/answer/internal/service/object_info"
	questioncommon "github.com/apache/answer/internal/service/question_common"
	"github.com/apache/answer/internal/service/role"
	"github.com/apache/answer/internal/service/siteinfo_common"
	tagcommon "github.com/apache/answer/internal/service/tag_common"
	usercommon "github.com/apache/answer/internal/service/user_common"
	"github.com/apache/answer/internal/service/vector_sync"
	"github.com/apache/answer/pkg/htmltext"
	"github.com/apache/answer/pkg/token"
	"github.com/apache/answer/pkg/uid"
	"github.com/apache/answer/plugin"
	"github.com/jinzhu/copier"
	"github.com/segmentfault/pacman/errors"
	"github.com/segmentfault/pacman/log"
)

// ReviewRepo review repository
type ReviewRepo interface {
	AddReview(ctx context.Context, review *entity.Review) (err error)
	UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error)
	GetReview(ctx context.Context, reviewID int) (review *entity.Review, exist bool, err error)
	GetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error)
	GetReviewCount(ctx context.Context, status int) (count int64, err error)
	GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) (reviewList []*entity.Review, total int64, err error)
}

// ReviewService user service
type ReviewService struct {
	reviewRepo                       ReviewRepo
	objectInfoService                *object_info.ObjService
	userCommon                       *usercommon.UserCommon
	userRepo                         usercommon.UserRepo
	questionRepo                     questioncommon.QuestionRepo
	answerRepo                       answercommon.AnswerRepo
	userRoleService                  *role.UserRoleRelService
	tagCommon                        *tagcommon.TagCommonService
	questionCommon                   *questioncommon.QuestionCommon
	externalNotificationQueueService noticequeue.ExternalService
	notificationQueueService         noticequeue.Service
	siteInfoService                  siteinfo_common.SiteInfoCommonService
	commentCommonRepo                commentcommon.CommentCommonRepo
	vectorSyncService                vector_sync.Service
}

// NewReviewService new review service
func NewReviewService(
	reviewRepo ReviewRepo,
	objectInfoService *object_info.ObjService,
	userCommon *usercommon.UserCommon,
	userRepo usercommon.UserRepo,
	questionRepo questioncommon.QuestionRepo,
	answerRepo answercommon.AnswerRepo,
	userRoleService *role.UserRoleRelService,
	externalNotificationQueueService noticequeue.ExternalService,
	tagCommon *tagcommon.TagCommonService,
	questionCommon *questioncommon.QuestionCommon,
	notificationQueueService noticequeue.Service,
	siteInfoService siteinfo_common.SiteInfoCommonService,
	commentCommonRepo commentcommon.CommentCommonRepo,
	vectorSyncService vector_sync.Service,
) *ReviewService {
	return &ReviewService{
		reviewRepo:                       reviewRepo,
		objectInfoService:                objectInfoService,
		userCommon:                       userCommon,
		userRepo:                         userRepo,
		questionRepo:                     questionRepo,
		answerRepo:                       answerRepo,
		userRoleService:                  userRoleService,
		externalNotificationQueueService: externalNotificationQueueService,
		tagCommon:                        tagCommon,
		questionCommon:                   questionCommon,
		notificationQueueService:         notificationQueueService,
		siteInfoService:                  siteInfoService,
		commentCommonRepo:                commentCommonRepo,
		vectorSyncService:                vectorSyncService,
	}
}

// AddQuestionReview add review for question if needed
func (cs *ReviewService) AddQuestionReview(ctx context.Context,
	question *entity.Question, tags []*schema.TagItem, ip, ua string) (questionStatus int) {
	reviewContent := &plugin.ReviewContent{
		ObjectType: constant.QuestionObjectType,
		Title:      question.Title,
		Content:    question.ParsedText,
		IP:         ip,
		UserAgent:  ua,
	}
	for _, tag := range tags {
		reviewContent.Tags = append(reviewContent.Tags, tag.SlugName)
	}
	reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, question.UserID)
	reviewStatus := cs.callPluginToReview(ctx, question.UserID, question.ID, reviewContent)
	switch reviewStatus {
	case plugin.ReviewStatusApproved:
		questionStatus = entity.QuestionStatusAvailable
	case plugin.ReviewStatusNeedReview:
		questionStatus = entity.QuestionStatusPending
	case plugin.ReviewStatusDeleteDirectly:
		questionStatus = entity.QuestionStatusDeleted
	default:
		questionStatus = entity.QuestionStatusAvailable
	}
	return questionStatus
}

// AddAnswerReview add review for answer if needed
func (cs *ReviewService) AddAnswerReview(ctx context.Context,
	answer *entity.Answer, ip, ua string) (answerStatus int) {
	reviewContent := &plugin.ReviewContent{
		ObjectType: constant.AnswerObjectType,
		Content:    answer.ParsedText,
		IP:         ip,
		UserAgent:  ua,
	}
	reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, answer.UserID)
	reviewStatus := cs.callPluginToReview(ctx, answer.UserID, answer.ID, reviewContent)
	switch reviewStatus {
	case plugin.ReviewStatusApproved:
		answerStatus = entity.AnswerStatusAvailable
	case plugin.ReviewStatusNeedReview:
		answerStatus = entity.AnswerStatusPending
	case plugin.ReviewStatusDeleteDirectly:
		answerStatus = entity.AnswerStatusDeleted
	default:
		answerStatus = entity.AnswerStatusAvailable
	}
	return answerStatus
}

// AddCommentReview add review for comment if needed
func (cs *ReviewService) AddCommentReview(ctx context.Context,
	comment *entity.Comment, ip, ua string) (commentStatus int) {
	reviewContent := &plugin.ReviewContent{
		ObjectType: constant.CommentObjectType,
		Content:    comment.ParsedText,
		IP:         ip,
		UserAgent:  ua,
	}
	reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, comment.UserID)
	reviewStatus := cs.callPluginToReview(ctx, comment.UserID, comment.ID, reviewContent)
	switch reviewStatus {
	case plugin.ReviewStatusApproved:
		commentStatus = entity.CommentStatusAvailable
	case plugin.ReviewStatusNeedReview:
		commentStatus = entity.CommentStatusPending
	case plugin.ReviewStatusDeleteDirectly:
		commentStatus = entity.CommentStatusDeleted
	default:
		commentStatus = entity.CommentStatusAvailable
	}
	return commentStatus
}

// get review content author info
func (cs *ReviewService) getReviewContentAuthorInfo(ctx context.Context, userID string) (author plugin.ReviewContentAuthor) {
	user, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, userID)
	if err != nil {
		log.Errorf("get user info failed, err: %v", err)
		return
	}
	if !exist {
		log.Errorf("user not found by id: %s", userID)
		return
	}
	author.Rank = user.Rank
	author.ApprovedQuestionAmount, _ = cs.questionRepo.GetUserQuestionCount(ctx, userID, 0)
	author.ApprovedAnswerAmount, _ = cs.answerRepo.GetCountByUserID(ctx, userID)
	author.Role, _ = cs.userRoleService.GetUserRole(ctx, userID)
	return
}

// call plugin to review
func (cs *ReviewService) callPluginToReview(ctx context.Context, userID, objectID string,
	reviewContent *plugin.ReviewContent) (reviewStatus plugin.ReviewStatus) {
	// As default, no need review
	reviewStatus = plugin.ReviewStatusApproved
	objectID = uid.DeShortID(objectID)

	r := &entity.Review{
		UserID:         userID,
		ObjectID:       objectID,
		ObjectType:     constant.ObjectTypeStrMapping[reviewContent.ObjectType],
		ReviewerUserID: "0",
		Status:         entity.ReviewStatusPending,
	}
	if siteInterface, _ := cs.siteInfoService.GetSiteInterface(ctx); siteInterface != nil {
		reviewContent.Language = siteInterface.Language
	}

	_ = plugin.CallReviewer(func(reviewer plugin.Reviewer) error {
		// If one of the reviewer plugin return false, then the review is not approved
		if reviewStatus != plugin.ReviewStatusApproved {
			return nil
		}
		if result := reviewer.Review(reviewContent); !result.Approved {
			reviewStatus = result.ReviewStatus
			r.Reason = result.Reason
			r.Submitter = reviewer.Info().SlugName
		}
		return nil
	})

	if reviewStatus == plugin.ReviewStatusNeedReview {
		if err := cs.reviewRepo.AddReview(ctx, r); err != nil {
			log.Errorf("add review failed, err: %v", err)
		}
	}
	return reviewStatus
}

// UpdateReview update review
func (cs *ReviewService) UpdateReview(ctx context.Context, req *schema.UpdateReviewReq) (err error) {
	review, exist, err := cs.reviewRepo.GetReview(ctx, req.ReviewID)
	if err != nil {
		return err
	}
	if !exist {
		return errors.BadRequest(reason.ObjectNotFound)
	}
	if review.Status != entity.ReviewStatusPending {
		return nil
	}

	if err = cs.updateObjectStatus(ctx, review, req.IsApprove()); err != nil {
		return err
	}

	if req.IsApprove() {
		err = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusApproved)
	} else {
		err = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusRejected)
	}
	return
}

// update object status
func (cs *ReviewService) updateObjectStatus(ctx context.Context, review *entity.Review, isApprove bool) (err error) {
	objectType := constant.ObjectTypeNumberMapping[review.ObjectType]
	switch objectType {
	case constant.QuestionObjectType:
		questionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, review.ObjectID)
		if err != nil {
			return err
		}
		if !exist {
			return errors.BadRequest(reason.ObjectNotFound)
		}
		if isApprove {
			questionInfo.Status = entity.QuestionStatusAvailable
		} else {
			questionInfo.Status = entity.QuestionStatusDeleted
		}
		if err := cs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status); err != nil {
			return err
		}
		if isApprove {
			tags, err := cs.tagCommon.GetObjectEntityTag(ctx, questionInfo.ID)
			if err != nil {
				log.Errorf("get question tags failed, err: %v", err)
			}
			cs.externalNotificationQueueService.Send(ctx,
				schema.CreateNewQuestionNotificationMsg(questionInfo.ID, questionInfo.Title, questionInfo.UserID, tags))
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: questionInfo.ID})
		} else {
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionDelete, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: questionInfo.ID})
		}
		userQuestionCount, err := cs.questionRepo.GetUserQuestionCount(ctx, questionInfo.UserID, 0)
		if err != nil {
			log.Errorf("get user question count failed, err: %v", err)
		} else {
			err = cs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)
			if err != nil {
				log.Errorf("update user question count failed, err: %v", err)
			}
		}
	case constant.AnswerObjectType:
		answerInfo, exist, err := cs.answerRepo.GetAnswer(ctx, review.ObjectID)
		if err != nil {
			return err
		}
		if !exist {
			return errors.BadRequest(reason.ObjectNotFound)
		}
		if isApprove {
			answerInfo.Status = entity.AnswerStatusAvailable
		} else {
			answerInfo.Status = entity.AnswerStatusDeleted
		}
		if err := cs.answerRepo.UpdateAnswerStatus(ctx, answerInfo.ID, answerInfo.Status); err != nil {
			return err
		}
		questionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
		if err != nil {
			return err
		}
		if !exist {
			return errors.BadRequest(reason.ObjectNotFound)
		}
		if isApprove {
			cs.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, answerInfo.ID,
				answerInfo.UserID, questionInfo.Title, answerInfo.OriginalText)
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
		} else {
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionDelete, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
		}
		cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: answerInfo.QuestionID})
		if err := cs.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID); err != nil {
			log.Errorf("update question answer count failed, err: %v", err)
		}
		if err := cs.questionCommon.UpdateLastAnswer(ctx, answerInfo.QuestionID, uid.DeShortID(answerInfo.ID)); err != nil {
			log.Errorf("update question last answer failed, err: %v", err)
		}
		userAnswerCount, err := cs.answerRepo.GetCountByUserID(ctx, answerInfo.UserID)
		if err != nil {
			log.Errorf("get user answer count failed, err: %v", err)
		} else {
			err = cs.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount))
			if err != nil {
				log.Errorf("update user answer count failed, err: %v", err)
			}
		}
	case constant.CommentObjectType:
		commentInfo, exist, err := cs.commentCommonRepo.GetCommentWithoutStatus(ctx, review.ObjectID)
		if err != nil {
			return err
		}
		if !exist {
			return errors.BadRequest(reason.ObjectNotFound)
		}
		if isApprove {
			commentInfo.Status = entity.CommentStatusAvailable
		} else {
			commentInfo.Status = entity.CommentStatusDeleted
		}
		if err := cs.commentCommonRepo.UpdateCommentStatus(ctx, commentInfo.ID, commentInfo.Status); err != nil {
			return err
		}
		_, exist, err = cs.questionRepo.GetQuestion(ctx, commentInfo.QuestionID)
		if err != nil {
			return err
		}
		if !exist {
			return errors.BadRequest(reason.ObjectNotFound)
		}
		if isApprove {
			cs.notificationCommentOnTheQuestion(ctx, commentInfo)
		}
		if commentInfo.ObjectID == commentInfo.QuestionID {
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: commentInfo.QuestionID})
		} else {
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: commentInfo.ObjectID})
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: commentInfo.QuestionID})
		}
	}
	return
}

func (cs *ReviewService) notificationAnswerTheQuestion(ctx context.Context,
	questionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) {
	// If the question is answered by me, there is no notification for myself.
	if questionUserID == answerUserID {
		return
	}
	msg := &schema.NotificationMsg{
		TriggerUserID:  answerUserID,
		ReceiverUserID: questionUserID,
		Type:           schema.NotificationTypeInbox,
		ObjectID:       answerID,
	}
	msg.ObjectType = constant.AnswerObjectType
	msg.NotificationAction = constant.NotificationAnswerTheQuestion
	cs.notificationQueueService.Send(ctx, msg)

	receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
	if err != nil {
		log.Error(err)
		return
	}
	if !exist {
		log.Warnf("user %s not found", questionUserID)
		return
	}

	externalNotificationMsg := &schema.ExternalNotificationMsg{
		ReceiverUserID: receiverUserInfo.ID,
		ReceiverEmail:  receiverUserInfo.EMail,
		ReceiverLang:   receiverUserInfo.Language,
	}
	rawData := &schema.NewAnswerTemplateRawData{
		QuestionTitle:   questionTitle,
		QuestionID:      questionID,
		AnswerID:        answerID,
		AnswerSummary:   answerSummary,
		UnsubscribeCode: token.GenerateToken(),
	}
	answerUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, answerUserID)
	if answerUser != nil {
		rawData.AnswerUserDisplayName = answerUser.DisplayName
	}
	externalNotificationMsg.NewAnswerTemplateRawData = rawData
	cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}

func (cs *ReviewService) notificationCommentOnTheQuestion(ctx context.Context, comment *entity.Comment) {
	objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)
	if err != nil {
		log.Error(err)
		return
	}
	if objInfo.IsDeleted() {
		log.Error("object already deleted")
		return
	}
	objInfo.ObjectID = uid.DeShortID(objInfo.ObjectID)
	objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)
	objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)

	// The priority of the notification
	// 1. reply to user
	// 2. comment mention to user
	// 3. answer or question was commented
	alreadyNotifiedUserID := make(map[string]bool)

	// get reply user info
	replyUserID := comment.GetReplyUserID()
	if len(replyUserID) > 0 && replyUserID != comment.UserID {
		replyUser, _, err := cs.userCommon.GetUserBasicInfoByID(ctx, replyUserID)
		if err != nil {
			log.Error(err)
			return
		}
		cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, comment.UserID,
			objInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
		alreadyNotifiedUserID[replyUser.ID] = true
		return
	}

	mentionUsernameList := comment.GetMentionUsernameList()
	if len(mentionUsernameList) > 0 {
		alreadyNotifiedUserIDs := cs.notificationMention(
			ctx, mentionUsernameList, comment.ID, comment.UserID, alreadyNotifiedUserID)
		for _, userID := range alreadyNotifiedUserIDs {
			alreadyNotifiedUserID[userID] = true
		}
		return
	}

	if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
		cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
			objInfo.QuestionID, objInfo.Title, comment.ID, comment.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
	} else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
		cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
			objInfo.ObjectCreatorUserID, comment.ID, comment.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
	}
}

func (cs *ReviewService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID,
	questionID, questionTitle, commentSummary string) {
	msg := &schema.NotificationMsg{
		ReceiverUserID: replyUserID,
		TriggerUserID:  commentUserID,
		Type:           schema.NotificationTypeInbox,
		ObjectID:       commentID,
	}
	msg.ObjectType = constant.CommentObjectType
	msg.NotificationAction = constant.NotificationReplyToYou
	cs.notificationQueueService.Send(ctx, msg)

	// Send external notification.
	receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, replyUserID)
	if err != nil {
		log.Error(err)
		return
	}
	if !exist {
		log.Warnf("user %s not found", replyUserID)
		return
	}
	externalNotificationMsg := &schema.ExternalNotificationMsg{
		ReceiverUserID: receiverUserInfo.ID,
		ReceiverEmail:  receiverUserInfo.EMail,
		ReceiverLang:   receiverUserInfo.Language,
	}
	rawData := &schema.NewCommentTemplateRawData{
		QuestionTitle:   questionTitle,
		QuestionID:      questionID,
		CommentID:       commentID,
		CommentSummary:  commentSummary,
		UnsubscribeCode: token.GenerateToken(),
	}
	commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
	if commentUser != nil {
		rawData.CommentUserDisplayName = commentUser.DisplayName
	}
	externalNotificationMsg.NewCommentTemplateRawData = rawData
	cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}

func (cs *ReviewService) notificationMention(
	ctx context.Context, mentionUsernameList []string, commentID, commentUserID string,
	alreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) {
	for _, username := range mentionUsernameList {
		userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username)
		if err != nil {
			log.Error(err)
			continue
		}
		if exist && !alreadyNotifiedUserID[userInfo.ID] {
			msg := &schema.NotificationMsg{
				ReceiverUserID: userInfo.ID,
				TriggerUserID:  commentUserID,
				Type:           schema.NotificationTypeInbox,
				ObjectID:       commentID,
			}
			msg.ObjectType = constant.CommentObjectType
			msg.NotificationAction = constant.NotificationMentionYou
			cs.notificationQueueService.Send(ctx, msg)
			alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
		}
	}
	return alreadyNotifiedUserIDs
}

func (cs *ReviewService) notificationQuestionComment(ctx context.Context, questionUserID,
	questionID, questionTitle, commentID, commentUserID, commentSummary string) {
	if questionUserID == commentUserID {
		return
	}
	// send internal notification
	msg := &schema.NotificationMsg{
		ReceiverUserID: questionUserID,
		TriggerUserID:  commentUserID,
		Type:           schema.NotificationTypeInbox,
		ObjectID:       commentID,
	}
	msg.ObjectType = constant.CommentObjectType
	msg.NotificationAction = constant.NotificationCommentQuestion
	cs.notificationQueueService.Send(ctx, msg)

	// send external notification
	receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
	if err != nil {
		log.Error(err)
		return
	}
	if !exist {
		log.Warnf("user %s not found", questionUserID)
		return
	}

	externalNotificationMsg := &schema.ExternalNotificationMsg{
		ReceiverUserID: receiverUserInfo.ID,
		ReceiverEmail:  receiverUserInfo.EMail,
		ReceiverLang:   receiverUserInfo.Language,
	}
	rawData := &schema.NewCommentTemplateRawData{
		QuestionTitle:   questionTitle,
		QuestionID:      questionID,
		CommentID:       commentID,
		CommentSummary:  commentSummary,
		UnsubscribeCode: token.GenerateToken(),
	}
	commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
	if commentUser != nil {
		rawData.CommentUserDisplayName = commentUser.DisplayName
	}
	externalNotificationMsg.NewCommentTemplateRawData = rawData
	cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}

func (cs *ReviewService) notificationAnswerComment(ctx context.Context,
	questionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) {
	if answerUserID == commentUserID {
		return
	}

	// Send internal notification.
	msg := &schema.NotificationMsg{
		ReceiverUserID: answerUserID,
		TriggerUserID:  commentUserID,
		Type:           schema.NotificationTypeInbox,
		ObjectID:       commentID,
	}
	msg.ObjectType = constant.CommentObjectType
	msg.NotificationAction = constant.NotificationCommentAnswer
	cs.notificationQueueService.Send(ctx, msg)

	// Send external notification.
	receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
	if err != nil {
		log.Error(err)
		return
	}
	if !exist {
		log.Warnf("user %s not found", answerUserID)
		return
	}
	externalNotificationMsg := &schema.ExternalNotificationMsg{
		ReceiverUserID: receiverUserInfo.ID,
		ReceiverEmail:  receiverUserInfo.EMail,
		ReceiverLang:   receiverUserInfo.Language,
	}
	rawData := &schema.NewCommentTemplateRawData{
		QuestionTitle:   questionTitle,
		QuestionID:      questionID,
		AnswerID:        answerID,
		CommentID:       commentID,
		CommentSummary:  commentSummary,
		UnsubscribeCode: token.GenerateToken(),
	}
	commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
	if commentUser != nil {
		rawData.CommentUserDisplayName = commentUser.DisplayName
	}
	externalNotificationMsg.NewCommentTemplateRawData = rawData
	cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}

// GetReviewPendingCount get review pending count
func (cs *ReviewService) GetReviewPendingCount(ctx context.Context) (count int64, err error) {
	return cs.reviewRepo.GetReviewCount(ctx, entity.ReviewStatusPending)
}

// GetUnreviewedPostPage get review page
func (cs *ReviewService) GetUnreviewedPostPage(ctx context.Context, req *schema.GetUnreviewedPostPageReq) (
	pageModel *pager.PageModel, err error) {
	if !req.IsAdmin {
		return pager.NewPageModel(0, make([]*schema.GetUnreviewedPostPageResp, 0)), nil
	}
	cond := &entity.Review{
		ObjectID: req.ObjectID,
		Status:   entity.ReviewStatusPending,
	}
	reviewList, total, err := cs.reviewRepo.GetReviewPage(ctx, req.Page, 1, cond)
	if err != nil {
		return
	}

	resp := make([]*schema.GetUnreviewedPostPageResp, 0)
	for _, review := range reviewList {
		info, err := cs.objectInfoService.GetUnreviewedRevisionInfo(ctx, review.ObjectID)
		if err != nil {
			log.Errorf("GetUnreviewedRevisionInfo failed, err: %v", err)
			continue
		}

		r := &schema.GetUnreviewedPostPageResp{
			ReviewID:             review.ID,
			CreatedAt:            info.CreatedAt,
			ObjectID:             info.ObjectID,
			QuestionID:           info.QuestionID,
			AnswerID:             info.AnswerID,
			CommentID:            info.CommentID,
			ObjectType:           info.ObjectType,
			Title:                info.Title,
			UrlTitle:             htmltext.UrlTitle(info.Title),
			OriginalText:         info.Content,
			ParsedText:           info.Html,
			Tags:                 info.Tags,
			ObjectStatus:         info.Status,
			ObjectShowStatus:     info.ShowStatus,
			SubmitAt:             review.CreatedAt.Unix(),
			SubmitterDisplayName: req.ReviewerMapping[review.Submitter],
			Reason:               review.Reason,
		}

		// get user info
		userInfo, exists, e := cs.userCommon.GetUserBasicInfoByID(ctx, info.ObjectCreatorUserID)
		if e != nil {
			log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e)
		}
		if exists {
			_ = copier.Copy(&r.AuthorUserInfo, userInfo)
		}
		resp = append(resp, r)
	}
	return pager.NewPageModel(total, resp), nil
}
