/*
 * 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 comment

import (
	"context"

	"github.com/apache/answer/internal/service/eventqueue"
	"github.com/apache/answer/internal/service/review"

	"time"

	"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"
	"github.com/apache/answer/internal/service/activity_common"
	"github.com/apache/answer/internal/service/activityqueue"
	"github.com/apache/answer/internal/service/comment_common"
	"github.com/apache/answer/internal/service/export"
	"github.com/apache/answer/internal/service/noticequeue"
	"github.com/apache/answer/internal/service/object_info"
	"github.com/apache/answer/internal/service/permission"
	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/jinzhu/copier"
	"github.com/segmentfault/pacman/errors"
	"github.com/segmentfault/pacman/log"
)

// CommentRepo comment repository
type CommentRepo interface {
	AddComment(ctx context.Context, comment *entity.Comment) (err error)
	RemoveComment(ctx context.Context, commentID string) (err error)
	UpdateCommentContent(ctx context.Context, commentID string, original string, parsedText string) (err error)
	UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error)
	GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)
	GetCommentPage(ctx context.Context, commentQuery *CommentQuery) (
		comments []*entity.Comment, total int64, err error)
}

type CommentQuery struct {
	pager.PageCond
	// object id
	ObjectID string
	// query condition
	QueryCond string
	// user id
	UserID string
}

func (c *CommentQuery) GetOrderBy() string {
	if c.QueryCond == "vote" {
		return "vote_count DESC,created_at ASC"
	}
	if c.QueryCond == "created_at" {
		return "created_at DESC"
	}
	return "created_at ASC"
}

// CommentService user service
type CommentService struct {
	commentRepo                      CommentRepo
	commentCommonRepo                comment_common.CommentCommonRepo
	userCommon                       *usercommon.UserCommon
	voteCommon                       activity_common.VoteRepo
	objectInfoService                *object_info.ObjService
	emailService                     *export.EmailService
	userRepo                         usercommon.UserRepo
	notificationQueueService         noticequeue.Service
	externalNotificationQueueService noticequeue.ExternalService
	activityQueueService             activityqueue.Service
	eventQueueService                eventqueue.Service
	reviewService                    *review.ReviewService
	vectorSyncService                vector_sync.Service
}

// NewCommentService new comment service
func NewCommentService(
	commentRepo CommentRepo,
	commentCommonRepo comment_common.CommentCommonRepo,
	userCommon *usercommon.UserCommon,
	objectInfoService *object_info.ObjService,
	voteCommon activity_common.VoteRepo,
	emailService *export.EmailService,
	userRepo usercommon.UserRepo,
	notificationQueueService noticequeue.Service,
	externalNotificationQueueService noticequeue.ExternalService,
	activityQueueService activityqueue.Service,
	eventQueueService eventqueue.Service,
	reviewService *review.ReviewService,
	vectorSyncService vector_sync.Service,
) *CommentService {
	return &CommentService{
		commentRepo:                      commentRepo,
		commentCommonRepo:                commentCommonRepo,
		userCommon:                       userCommon,
		voteCommon:                       voteCommon,
		objectInfoService:                objectInfoService,
		emailService:                     emailService,
		userRepo:                         userRepo,
		notificationQueueService:         notificationQueueService,
		externalNotificationQueueService: externalNotificationQueueService,
		activityQueueService:             activityQueueService,
		eventQueueService:                eventQueueService,
		reviewService:                    reviewService,
		vectorSyncService:                vectorSyncService,
	}
}

// AddComment add comment
func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddCommentReq) (
	resp *schema.GetCommentResp, err error) {
	comment := &entity.Comment{}
	_ = copier.Copy(comment, req)
	comment.Status = entity.CommentStatusAvailable

	objInfo, err := cs.objectInfoService.GetInfo(ctx, req.ObjectID)
	if err != nil {
		return nil, err
	}
	if objInfo.IsDeleted() {
		return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted)
	}
	objInfo.ObjectID = uid.DeShortID(objInfo.ObjectID)
	objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)
	objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)
	if objInfo.ObjectType == constant.QuestionObjectType || objInfo.ObjectType == constant.AnswerObjectType {
		comment.QuestionID = objInfo.QuestionID
	}

	if len(req.ReplyCommentID) > 0 {
		replyComment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.ReplyCommentID)
		if err != nil {
			return nil, err
		}
		if !exist {
			return nil, errors.BadRequest(reason.CommentNotFound)
		}
		comment.SetReplyUserID(replyComment.UserID)
		comment.SetReplyCommentID(replyComment.ID)
	} else {
		comment.SetReplyUserID("")
		comment.SetReplyCommentID("")
	}

	err = cs.commentRepo.AddComment(ctx, comment)
	if err != nil {
		return nil, err
	}

	comment.Status = cs.reviewService.AddCommentReview(ctx, comment, req.IP, req.UserAgent)
	if err := cs.commentRepo.UpdateCommentStatus(ctx, comment.ID, comment.Status); err != nil {
		return nil, err
	}

	resp = &schema.GetCommentResp{}
	resp.SetFromComment(comment)
	resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
		time.Now(), req.CanEdit, req.CanDelete)

	if comment.Status == entity.CommentStatusAvailable {
		if err := cs.addCommentNotification(ctx, req, resp, comment, objInfo); err != nil {
			return nil, err
		}
	}

	// get user info
	userInfo, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID)
	if err != nil {
		return nil, err
	}
	if exist {
		resp.Username = userInfo.Username
		resp.UserDisplayName = userInfo.DisplayName
		resp.UserAvatar = userInfo.Avatar
		resp.UserStatus = userInfo.Status
	}

	activityMsg := &schema.ActivityMsg{
		UserID:           comment.UserID,
		ObjectID:         comment.ID,
		OriginalObjectID: req.ObjectID,
		ActivityTypeKey:  constant.ActQuestionCommented,
	}
	var event *schema.EventMsg
	switch objInfo.ObjectType {
	case constant.QuestionObjectType:
		activityMsg.ActivityTypeKey = constant.ActQuestionCommented
		event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID).
			CID(comment.ID, comment.UserID).QID(objInfo.QuestionID, objInfo.ObjectCreatorUserID)
	case constant.AnswerObjectType:
		activityMsg.ActivityTypeKey = constant.ActAnswerCommented
		event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID).
			CID(comment.ID, comment.UserID).AID(objInfo.AnswerID, objInfo.ObjectCreatorUserID)
	}
	cs.activityQueueService.Send(ctx, activityMsg)
	cs.eventQueueService.Send(ctx, event)
	if comment.Status == entity.CommentStatusAvailable {
		switch objInfo.ObjectType {
		case constant.QuestionObjectType:
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: objInfo.QuestionID})
		case constant.AnswerObjectType:
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: objInfo.AnswerID})
			cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: objInfo.QuestionID})
		}
	}
	return resp, nil
}

func (cs *CommentService) addCommentNotification(
	ctx context.Context, req *schema.AddCommentReq, resp *schema.GetCommentResp,
	comment *entity.Comment, objInfo *schema.SimpleObjectInfo) error {
	// 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
	if len(resp.ReplyUserID) > 0 && resp.ReplyUserID != req.UserID {
		replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
		if err != nil {
			return err
		}
		if exist {
			resp.ReplyUsername = replyUser.Username
			resp.ReplyUserDisplayName = replyUser.DisplayName
			resp.ReplyUserStatus = replyUser.Status
		}
		cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID,
			objInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
		alreadyNotifiedUserID[replyUser.ID] = true
		return nil
	}

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

	if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
		cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
			objInfo.QuestionID, objInfo.Title, comment.ID, req.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, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
	}
	return nil
}

// RemoveComment delete comment
func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) {
	commentInfo, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
	if err != nil {
		return err
	}
	if !exist {
		return nil
	}
	err = cs.commentRepo.RemoveComment(ctx, req.CommentID)
	if err != nil {
		return err
	}
	cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentDelete, req.UserID).
		TID(req.CommentID).CID(req.CommentID, req.UserID))
	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 nil
}

// UpdateComment update comment
func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (
	resp *schema.UpdateCommentResp, err error) {
	old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
	if err != nil {
		return nil, err
	}
	if !exist {
		return nil, errors.BadRequest(reason.CommentNotFound)
	}
	// user can't edit the comment that was posted by others except admin
	if !req.IsAdmin && req.UserID != old.UserID {
		return nil, errors.BadRequest(reason.CommentNotFound)
	}

	// user can edit the comment that was posted by himself before deadline.
	// admin can edit it at any time
	if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) {
		return nil, errors.BadRequest(reason.CommentCannotEditAfterDeadline)
	}

	if err = cs.commentRepo.UpdateCommentContent(ctx, old.ID, req.OriginalText, req.ParsedText); err != nil {
		return nil, err
	}
	resp = &schema.UpdateCommentResp{
		CommentID:    old.ID,
		OriginalText: req.OriginalText,
		ParsedText:   req.ParsedText,
	}
	cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentUpdate, req.UserID).TID(old.ID).
		CID(old.ID, old.UserID))
	if old.ObjectID == old.QuestionID {
		cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: old.QuestionID})
	} else {
		cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: old.ObjectID})
		cs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: old.QuestionID})
	}
	return resp, nil
}

// GetComment get comment one
func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetCommentReq) (resp *schema.GetCommentResp, err error) {
	comment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.ID)
	if err != nil {
		return
	}
	if !exist {
		return nil, errors.BadRequest(reason.CommentNotFound)
	}
	objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)
	if err != nil {
		return nil, err
	}
	if err := objInfo.CheckVisibility(req.UserID, req.IsAdminModerator); err != nil {
		return nil, err
	}

	resp = &schema.GetCommentResp{
		CommentID:      comment.ID,
		CreatedAt:      comment.CreatedAt.Unix(),
		UserID:         comment.UserID,
		ReplyUserID:    comment.GetReplyUserID(),
		ReplyCommentID: comment.GetReplyCommentID(),
		ObjectID:       comment.ObjectID,
		VoteCount:      comment.VoteCount,
		OriginalText:   comment.OriginalText,
		ParsedText:     comment.ParsedText,
	}

	// get comment user info
	if len(resp.UserID) > 0 {
		commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID)
		if err != nil {
			return nil, err
		}
		if exist {
			resp.Username = commentUser.Username
			resp.UserDisplayName = commentUser.DisplayName
			resp.UserAvatar = commentUser.Avatar
			resp.UserStatus = commentUser.Status
		}
	}

	// get reply user info
	if len(resp.ReplyUserID) > 0 {
		replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
		if err != nil {
			return nil, err
		}
		if exist {
			resp.ReplyUsername = replyUser.Username
			resp.ReplyUserDisplayName = replyUser.DisplayName
			resp.ReplyUserStatus = replyUser.Status
		}
	}

	// check if current user vote this comment
	resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID)

	resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
		comment.CreatedAt, req.CanEdit, req.CanDelete)
	return resp, nil
}

// GetCommentWithPage get comment list page
func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.GetCommentWithPageReq) (
	pageModel *pager.PageModel, err error) {
	objInfo, err := cs.objectInfoService.GetInfo(ctx, req.ObjectID)
	if err != nil {
		return nil, err
	}
	if err := objInfo.CheckVisibility(req.UserID, req.IsAdminModerator); err != nil {
		return nil, err
	}
	dto := &CommentQuery{
		PageCond:  pager.PageCond{Page: req.Page, PageSize: req.PageSize},
		ObjectID:  req.ObjectID,
		QueryCond: req.QueryCond,
	}
	commentList, total, err := cs.commentRepo.GetCommentPage(ctx, dto)
	if err != nil {
		return nil, err
	}
	resp := make([]*schema.GetCommentResp, 0)
	for _, comment := range commentList {
		commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment)
		if err != nil {
			return nil, err
		}
		resp = append(resp, commentResp)
	}

	// if user request the specific comment, add it if not exist.
	if len(req.CommentID) > 0 {
		commentExist := false
		for _, t := range resp {
			if t.CommentID == req.CommentID {
				commentExist = true
				break
			}
		}
		if !commentExist {
			comment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
			if err != nil {
				return nil, err
			}
			if exist && comment.ObjectID == req.ObjectID {
				commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment)
				if err != nil {
					return nil, err
				}
				resp = append(resp, commentResp)
			}
		}
	}
	return pager.NewPageModel(total, resp), nil
}

func (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *schema.GetCommentWithPageReq,
	comment *entity.Comment) (commentResp *schema.GetCommentResp, err error) {
	commentResp = &schema.GetCommentResp{
		CommentID:      comment.ID,
		CreatedAt:      comment.CreatedAt.Unix(),
		UserID:         comment.UserID,
		ReplyUserID:    comment.GetReplyUserID(),
		ReplyCommentID: comment.GetReplyCommentID(),
		ObjectID:       comment.ObjectID,
		VoteCount:      comment.VoteCount,
		OriginalText:   comment.OriginalText,
		ParsedText:     comment.ParsedText,
	}

	// get comment user info
	if len(commentResp.UserID) > 0 {
		commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.UserID)
		if err != nil {
			return nil, err
		}
		if exist {
			commentResp.Username = commentUser.Username
			commentResp.UserDisplayName = commentUser.DisplayName
			commentResp.UserAvatar = commentUser.Avatar
			commentResp.UserStatus = commentUser.Status
		}
	}

	// get reply user info
	if len(commentResp.ReplyUserID) > 0 {
		replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.ReplyUserID)
		if err != nil {
			return nil, err
		}
		if exist {
			commentResp.ReplyUsername = replyUser.Username
			commentResp.ReplyUserDisplayName = replyUser.DisplayName
			commentResp.ReplyUserStatus = replyUser.Status
		}
	}

	// check if current user vote this comment
	commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID)

	commentResp.MemberActions = permission.GetCommentPermission(ctx,
		req.UserID, commentResp.UserID, comment.CreatedAt, req.CanEdit, req.CanDelete)
	return commentResp, nil
}

func (cs *CommentService) checkIsVote(ctx context.Context, userID, commentID string) (isVote bool) {
	status := cs.voteCommon.GetVoteStatus(ctx, commentID, userID)
	return len(status) > 0
}

// GetCommentPersonalWithPage get personal comment list page
func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *schema.GetCommentPersonalWithPageReq) (
	pageModel *pager.PageModel, err error) {
	if len(req.Username) > 0 {
		userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
		if err != nil {
			return nil, err
		}
		if !exist {
			return nil, errors.BadRequest(reason.UserNotFound)
		}
		req.UserID = userInfo.ID
	}
	if len(req.UserID) == 0 {
		return nil, errors.BadRequest(reason.UserNotFound)
	}

	dto := &CommentQuery{
		PageCond:  pager.PageCond{Page: req.Page, PageSize: req.PageSize},
		UserID:    req.UserID,
		QueryCond: "created_at",
	}
	commentList, total, err := cs.commentRepo.GetCommentPage(ctx, dto)
	if err != nil {
		return nil, err
	}
	resp := make([]*schema.GetCommentPersonalWithPageResp, 0)
	for _, comment := range commentList {
		commentResp := &schema.GetCommentPersonalWithPageResp{
			CommentID: comment.ID,
			CreatedAt: comment.CreatedAt.Unix(),
			ObjectID:  comment.ObjectID,
			Content:   comment.ParsedText, // todo trim
		}
		if len(comment.ObjectID) > 0 {
			objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)
			if err != nil {
				log.Error(err)
			} else {
				commentResp.ObjectType = objInfo.ObjectType
				commentResp.Title = objInfo.Title
				commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)
				commentResp.QuestionID = objInfo.QuestionID
				commentResp.AnswerID = objInfo.AnswerID
				if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
					commentResp.Title = "Deleted question"
				}
			}
		}
		resp = append(resp, commentResp)
	}
	return pager.NewPageModel(total, resp), nil
}

func (cs *CommentService) 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 *CommentService) 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)
}

func (cs *CommentService) 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 *CommentService) 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
}
