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

import (
	"context"
	"encoding/json"
	"time"

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

	"github.com/apache/answer/internal/base/constant"
	"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"
	"github.com/apache/answer/internal/service/activity_common"
	"github.com/apache/answer/internal/service/activityqueue"
	answercommon "github.com/apache/answer/internal/service/answer_common"
	collectioncommon "github.com/apache/answer/internal/service/collection_common"
	"github.com/apache/answer/internal/service/export"
	"github.com/apache/answer/internal/service/noticequeue"
	"github.com/apache/answer/internal/service/permission"
	questioncommon "github.com/apache/answer/internal/service/question_common"
	"github.com/apache/answer/internal/service/review"
	"github.com/apache/answer/internal/service/revision_common"
	"github.com/apache/answer/internal/service/role"
	usercommon "github.com/apache/answer/internal/service/user_common"
	"github.com/apache/answer/internal/service/vector_sync"
	"github.com/apache/answer/pkg/converter"
	"github.com/apache/answer/pkg/htmltext"
	"github.com/apache/answer/pkg/token"
	"github.com/apache/answer/pkg/uid"
	"github.com/segmentfault/pacman/errors"
	"github.com/segmentfault/pacman/log"
)

// AnswerService user service
type AnswerService struct {
	answerRepo                       answercommon.AnswerRepo
	questionRepo                     questioncommon.QuestionRepo
	questionCommon                   *questioncommon.QuestionCommon
	answerActivityService            *activity.AnswerActivityService
	userCommon                       *usercommon.UserCommon
	collectionCommon                 *collectioncommon.CollectionCommon
	userRepo                         usercommon.UserRepo
	revisionService                  *revision_common.RevisionService
	AnswerCommon                     *answercommon.AnswerCommon
	voteRepo                         activity_common.VoteRepo
	emailService                     *export.EmailService
	roleService                      *role.UserRoleRelService
	notificationQueueService         noticequeue.Service
	externalNotificationQueueService noticequeue.ExternalService
	activityQueueService             activityqueue.Service
	reviewService                    *review.ReviewService
	eventQueueService                eventqueue.Service
	vectorSyncService                vector_sync.Service
}

func NewAnswerService(
	answerRepo answercommon.AnswerRepo,
	questionRepo questioncommon.QuestionRepo,
	questionCommon *questioncommon.QuestionCommon,
	userCommon *usercommon.UserCommon,
	collectionCommon *collectioncommon.CollectionCommon,
	userRepo usercommon.UserRepo,
	revisionService *revision_common.RevisionService,
	answerAcceptActivityRepo *activity.AnswerActivityService,
	answerCommon *answercommon.AnswerCommon,
	voteRepo activity_common.VoteRepo,
	emailService *export.EmailService,
	roleService *role.UserRoleRelService,
	notificationQueueService noticequeue.Service,
	externalNotificationQueueService noticequeue.ExternalService,
	activityQueueService activityqueue.Service,
	reviewService *review.ReviewService,
	eventQueueService eventqueue.Service,
	vectorSyncService vector_sync.Service,
) *AnswerService {
	return &AnswerService{
		answerRepo:                       answerRepo,
		questionRepo:                     questionRepo,
		userCommon:                       userCommon,
		collectionCommon:                 collectionCommon,
		questionCommon:                   questionCommon,
		userRepo:                         userRepo,
		revisionService:                  revisionService,
		answerActivityService:            answerAcceptActivityRepo,
		AnswerCommon:                     answerCommon,
		voteRepo:                         voteRepo,
		emailService:                     emailService,
		roleService:                      roleService,
		notificationQueueService:         notificationQueueService,
		externalNotificationQueueService: externalNotificationQueueService,
		activityQueueService:             activityQueueService,
		reviewService:                    reviewService,
		eventQueueService:                eventQueueService,
		vectorSyncService:                vectorSyncService,
	}
}

// RemoveAnswer delete answer
func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAnswerReq) (err error) {
	answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID)
	if err != nil {
		return err
	}
	if !exist {
		return nil
	}
	// if the status is deleted, return directly
	if answerInfo.Status == entity.AnswerStatusDeleted {
		return nil
	}
	roleID, err := as.roleService.GetUserRole(ctx, req.UserID)
	if err != nil {
		return err
	}
	if roleID != role.RoleAdminID && roleID != role.RoleModeratorID {
		if answerInfo.UserID != req.UserID {
			return errors.BadRequest(reason.AnswerCannotDeleted)
		}
		if answerInfo.VoteCount > 0 {
			return errors.BadRequest(reason.AnswerCannotDeleted)
		}
		if answerInfo.Accepted == schema.AnswerAcceptedEnable {
			return errors.BadRequest(reason.AnswerCannotDeleted)
		}
		_, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
		if err != nil {
			return errors.BadRequest(reason.AnswerCannotDeleted)
		}
		if !exist {
			return errors.BadRequest(reason.AnswerCannotDeleted)
		}
	}

	err = as.answerRepo.RemoveAnswer(ctx, req.ID)
	if err != nil {
		return err
	}

	// user add question count
	err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID)
	if err != nil {
		log.Error("IncreaseAnswerCount error", err.Error())
	}
	userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID)
	if err != nil {
		log.Error("GetCountByUserID error", err.Error())
	}
	err = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount))
	if err != nil {
		log.Error("user IncreaseAnswerCount error", err.Error())
	}
	err = as.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{
		FromQuestionID: answerInfo.QuestionID,
		FromAnswerID:   answerInfo.ID,
	}, &entity.QuestionLink{
		ToQuestionID: answerInfo.QuestionID,
		ToAnswerID:   answerInfo.ID,
	})
	if err != nil {
		log.Error("RemoveQuestionLink error", err.Error())
	}

	// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,
	// facing the problem of recovery.
	// err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
	// if err != nil {
	// 	log.Errorf("delete answer activity change failed: %s", err.Error())
	// }
	as.activityQueueService.Send(ctx, &schema.ActivityMsg{
		UserID:           req.UserID,
		TriggerUserID:    converter.StringToInt64(req.UserID),
		ObjectID:         answerInfo.ID,
		OriginalObjectID: answerInfo.ID,
		ActivityTypeKey:  constant.ActAnswerDeleted,
	})
	as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerDelete, req.UserID).TID(answerInfo.ID).
		AID(answerInfo.ID, answerInfo.UserID))
	as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionDelete, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
	as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: answerInfo.QuestionID})
	return
}

// RecoverAnswer recover deleted answer
func (as *AnswerService) RecoverAnswer(ctx context.Context, req *schema.RecoverAnswerReq) (err error) {
	answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.AnswerID)
	if err != nil {
		return err
	}
	if !exist {
		return errors.BadRequest(reason.AnswerNotFound)
	}
	if answerInfo.Status != entity.AnswerStatusDeleted {
		return nil
	}
	if err = as.answerRepo.RecoverAnswer(ctx, req.AnswerID); err != nil {
		return err
	}
	if err = as.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{
		FromQuestionID: answerInfo.QuestionID,
		FromAnswerID:   answerInfo.ID,
	}, &entity.QuestionLink{
		ToQuestionID: answerInfo.QuestionID,
		ToAnswerID:   answerInfo.ID,
	}); err != nil {
		return err
	}

	if err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID); err != nil {
		log.Errorf("update answer count failed: %s", err.Error())
	}
	userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID)
	if err != nil {
		log.Errorf("get user answer count failed: %s", err.Error())
	} else {
		err = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount))
		if err != nil {
			log.Errorf("update user answer count failed: %s", err.Error())
		}
	}
	as.activityQueueService.Send(ctx, &schema.ActivityMsg{
		UserID:           req.UserID,
		TriggerUserID:    converter.StringToInt64(req.UserID),
		ObjectID:         answerInfo.ID,
		OriginalObjectID: answerInfo.ID,
		ActivityTypeKey:  constant.ActAnswerUndeleted,
	})
	as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
	as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: answerInfo.QuestionID})
	return nil
}

func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (string, error) {
	questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)
	if err != nil {
		return "", err
	}
	if !exist {
		return "", errors.BadRequest(reason.QuestionNotFound)
	}
	if questionInfo.Status == entity.QuestionStatusClosed || questionInfo.Status == entity.QuestionStatusDeleted {
		err = errors.BadRequest(reason.AnswerCannotAddByClosedQuestion)
		return "", err
	}
	insertData := &entity.Answer{}
	insertData.UserID = req.UserID
	insertData.OriginalText = req.Content
	insertData.ParsedText = req.HTML
	insertData.Accepted = schema.AnswerAcceptedFailed
	insertData.QuestionID = req.QuestionID
	insertData.RevisionID = "0"
	insertData.LastEditUserID = "0"
	insertData.Status = entity.AnswerStatusPending
	// insertData.UpdatedAt = now
	if err = as.answerRepo.AddAnswer(ctx, insertData); err != nil {
		return "", err
	}
	insertData.Status = as.reviewService.AddAnswerReview(ctx, insertData, req.IP, req.UserAgent)
	if err := as.answerRepo.UpdateAnswerStatus(ctx, insertData.ID, insertData.Status); err != nil {
		return "", err
	}
	if insertData.Status == entity.AnswerStatusAvailable {
		insertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText)
		if err != nil {
			return "", err
		}
		if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"parsed_text"}); err != nil {
			return "", err
		}
	}
	err = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionID)
	if err != nil {
		log.Error("IncreaseAnswerCount error", err.Error())
	}
	err = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionID, uid.DeShortID(insertData.ID))
	if err != nil {
		log.Error("UpdateLastAnswer error", err.Error())
	}
	err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID)
	if err != nil {
		return insertData.ID, err
	}
	userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, req.UserID)
	if err != nil {
		log.Error("GetCountByUserID error", err.Error())
	}
	err = as.userCommon.UpdateAnswerCount(ctx, req.UserID, int(userAnswerCount))
	if err != nil {
		log.Error("user IncreaseAnswerCount error", err.Error())
	}

	revisionDTO := &schema.AddRevisionDTO{
		UserID:   insertData.UserID,
		ObjectID: insertData.ID,
		Title:    "",
	}
	infoJSON, _ := json.Marshal(insertData)
	revisionDTO.Content = string(infoJSON)
	revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)
	if err != nil {
		return insertData.ID, err
	}
	if insertData.Status == entity.AnswerStatusAvailable {
		as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title,
			htmltext.FetchExcerpt(insertData.ParsedText, "...", 240))
	}

	as.activityQueueService.Send(ctx, &schema.ActivityMsg{
		UserID:           insertData.UserID,
		ObjectID:         insertData.ID,
		OriginalObjectID: insertData.ID,
		ActivityTypeKey:  constant.ActAnswerAnswered,
		RevisionID:       revisionID,
	})
	as.activityQueueService.Send(ctx, &schema.ActivityMsg{
		UserID:           insertData.UserID,
		ObjectID:         insertData.ID,
		OriginalObjectID: questionInfo.ID,
		ActivityTypeKey:  constant.ActQuestionAnswered,
	})
	as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerCreate, req.UserID).TID(insertData.ID).
		AID(insertData.ID, insertData.UserID))
	if insertData.Status == entity.AnswerStatusAvailable {
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: insertData.ID})
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: insertData.QuestionID})
	}
	return insertData.ID, nil
}

func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq) (string, error) {
	var canUpdate bool
	_, existUnreviewed, err := as.revisionService.ExistUnreviewedByObjectID(ctx, req.ID)
	if err != nil {
		return "", err
	}
	if existUnreviewed {
		return "", errors.BadRequest(reason.AnswerCannotUpdate)
	}

	answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID)
	if err != nil {
		return "", err
	}
	if !exist {
		return "", errors.BadRequest(reason.AnswerNotFound)
	}
	if answerInfo.Status == entity.AnswerStatusDeleted {
		return "", errors.BadRequest(reason.AnswerCannotUpdate)
	}

	questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
	if err != nil {
		return "", err
	}
	if !exist {
		return "", errors.BadRequest(reason.QuestionNotFound)
	}

	// If the content is the same, ignore it
	if answerInfo.OriginalText == req.Content {
		return "", nil
	}

	insertData := &entity.Answer{}
	insertData.ID = req.ID
	insertData.UserID = answerInfo.UserID
	insertData.QuestionID = questionInfo.ID
	insertData.OriginalText = req.Content
	insertData.ParsedText = req.HTML
	insertData.UpdatedAt = time.Now()
	insertData.LastEditUserID = "0"
	if answerInfo.UserID != req.UserID {
		insertData.LastEditUserID = req.UserID
	}

	revisionDTO := &schema.AddRevisionDTO{
		UserID:   req.UserID,
		ObjectID: req.ID,
		Log:      req.EditSummary,
	}

	if req.NoNeedReview || answerInfo.UserID == req.UserID {
		canUpdate = true
	}

	if !canUpdate {
		revisionDTO.Status = entity.RevisionUnreviewedStatus
	} else {
		insertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText)
		if err != nil {
			return "", err
		}
		if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}); err != nil {
			return "", err
		}
		err = as.questionCommon.UpdatePostTime(ctx, questionInfo.ID)
		if err != nil {
			return insertData.ID, err
		}
		as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID)
		revisionDTO.Status = entity.RevisionReviewPassStatus
	}

	infoJSON, _ := json.Marshal(insertData)
	revisionDTO.Content = string(infoJSON)
	revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)
	if err != nil {
		return insertData.ID, err
	}
	if canUpdate {
		as.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           req.UserID,
			ObjectID:         insertData.ID,
			OriginalObjectID: insertData.ID,
			ActivityTypeKey:  constant.ActAnswerEdited,
			RevisionID:       revisionID,
		})
		as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerUpdate, req.UserID).TID(insertData.ID).
			AID(insertData.ID, insertData.UserID))
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: insertData.ID})
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: insertData.QuestionID})
	}

	return insertData.ID, nil
}

// AcceptAnswer accept answer
func (as *AnswerService) AcceptAnswer(ctx context.Context, req *schema.AcceptAnswerReq) (err error) {
	// find question
	questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)
	if err != nil {
		return err
	}
	if !exist {
		return errors.BadRequest(reason.QuestionNotFound)
	}
	questionInfo.ID = uid.DeShortID(questionInfo.ID)
	if questionInfo.AcceptedAnswerID == req.AnswerID {
		return nil
	}

	// find answer
	var acceptedAnswerInfo *entity.Answer
	if len(req.AnswerID) > 1 {
		acceptedAnswerInfo, exist, err = as.answerRepo.GetByID(ctx, req.AnswerID)
		if err != nil {
			return err
		}
		if !exist {
			return errors.BadRequest(reason.AnswerNotFound)
		}

		// check answer belong to question
		if !sameObjectID(acceptedAnswerInfo.QuestionID, req.QuestionID) {
			return errors.BadRequest(reason.AnswerNotFound)
		}
		acceptedAnswerInfo.ID = uid.DeShortID(acceptedAnswerInfo.ID)
	}

	// update answers status
	if err = as.answerRepo.UpdateAcceptedStatus(ctx, req.AnswerID, req.QuestionID); err != nil {
		return err
	}

	// update question status
	err = as.questionCommon.UpdateAccepted(ctx, req.QuestionID, req.AnswerID)
	if err != nil {
		log.Error("UpdateLastAnswer error", err.Error())
	}

	var oldAnswerInfo *entity.Answer
	if len(questionInfo.AcceptedAnswerID) > 1 {
		oldAnswerInfo, _, err = as.answerRepo.GetByID(ctx, questionInfo.AcceptedAnswerID)
		if err != nil {
			return err
		}
		oldAnswerInfo.ID = uid.DeShortID(oldAnswerInfo.ID)
	}

	if acceptedAnswerInfo != nil {
		as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID).TID(acceptedAnswerInfo.ID).
			QID(questionInfo.ID, questionInfo.UserID).AID(acceptedAnswerInfo.ID, acceptedAnswerInfo.UserID))
	}

	as.updateAnswerRank(ctx, req.UserID, questionInfo, acceptedAnswerInfo, oldAnswerInfo)
	if acceptedAnswerInfo != nil {
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: acceptedAnswerInfo.ID})
	}
	if oldAnswerInfo != nil {
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: oldAnswerInfo.ID})
	}
	as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: questionInfo.ID})
	return nil
}

// sameObjectID reports whether two object ids refer to the same row,
// regardless of whether each is in short-id or long-id form.
func sameObjectID(a, b string) bool {
	return uid.DeShortID(a) == uid.DeShortID(b)
}

func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string,
	questionInfo *entity.Question, newAnswerInfo *entity.Answer, oldAnswerInfo *entity.Answer,
) {
	// if this question is already been answered, should cancel old answer rank
	if oldAnswerInfo != nil {
		err := as.answerActivityService.CancelAcceptAnswer(ctx, userID,
			questionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID)
		if err != nil {
			log.Error(err)
		}
	}
	if newAnswerInfo != nil {
		err := as.answerActivityService.AcceptAnswer(ctx, userID, newAnswerInfo.ID,
			questionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == questionInfo.UserID)
		if err != nil {
			log.Error(err)
		}
	}
}

func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string, isAdminModerator bool) (*schema.AnswerInfo, *schema.QuestionInfoResp, bool, error) {
	answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID)
	if err != nil {
		return nil, nil, has, err
	}
	if !has {
		return nil, nil, false, nil
	}
	question, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
	if err != nil {
		return nil, nil, has, err
	}
	if !exist {
		return nil, nil, false, errors.NotFound(reason.AnswerNotFound)
	}

	if (question.Status == entity.QuestionStatusDeleted ||
		question.Status == entity.QuestionStatusPending ||
		question.Show == entity.QuestionHide) &&
		!isAdminModerator && question.UserID != loginUserID {
		return nil, nil, false, errors.NotFound(reason.AnswerNotFound)
	}
	if (answerInfo.Status == entity.AnswerStatusDeleted ||
		answerInfo.Status == entity.AnswerStatusPending) &&
		!isAdminModerator && answerInfo.UserID != loginUserID {
		return nil, nil, false, errors.NotFound(reason.AnswerNotFound)
	}
	info := as.ShowFormat(ctx, answerInfo)
	// todo questionFunc
	questionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserID)
	if err != nil {
		return nil, nil, has, err
	}
	// todo UserFunc

	userIds := make([]string, 0)
	userIds = append(userIds, answerInfo.UserID)
	userIds = append(userIds, answerInfo.LastEditUserID)
	userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds)
	if err != nil {
		return nil, nil, has, err
	}

	_, ok := userInfoMap[answerInfo.UserID]
	if ok {
		info.UserInfo = userInfoMap[answerInfo.UserID]
	}
	_, ok = userInfoMap[answerInfo.LastEditUserID]
	if ok {
		info.UpdateUserInfo = userInfoMap[answerInfo.LastEditUserID]
	}

	if loginUserID == "" {
		return info, questionInfo, has, nil
	}

	info.VoteStatus = as.voteRepo.GetVoteStatus(ctx, answerID, loginUserID)

	collectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{answerInfo.ID})
	if err != nil {
		return nil, nil, has, err
	}
	if len(collectedMap) > 0 {
		info.Collected = true
	}

	return info, questionInfo, has, nil
}

func (as *AnswerService) GetDetail(ctx context.Context, answerID string) (*schema.AnswerInfo, error) {
	answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID)
	if err != nil {
		return nil, err
	}
	if !has {
		return nil, errors.BadRequest(reason.AnswerNotFound)
	}
	info := as.ShowFormat(ctx, answerInfo)
	return info, nil
}

func (as *AnswerService) GetCountByUserIDQuestionID(ctx context.Context, userId string, questionId string) (ids []string, err error) {
	return as.answerRepo.GetIDsByUserIDAndQuestionID(ctx, userId, questionId)
}

func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.AdminUpdateAnswerStatusReq) error {
	setStatus, ok := entity.AdminAnswerSearchStatus[req.Status]
	if !ok {
		return errors.BadRequest(reason.RequestFormatError)
	}
	answerInfo, exist, err := as.answerRepo.GetAnswer(ctx, req.AnswerID)
	if err != nil {
		return err
	}
	if !exist {
		return errors.BadRequest(reason.AnswerNotFound)
	}

	if setStatus == entity.AnswerStatusDeleted {
		if err := as.RemoveAnswer(ctx, &schema.RemoveAnswerReq{
			ID:        req.AnswerID,
			UserID:    req.UserID,
			CanDelete: true,
		}); err != nil {
			return err
		}

		msg := &schema.NotificationMsg{}
		msg.ObjectID = answerInfo.ID
		msg.Type = schema.NotificationTypeInbox
		msg.ReceiverUserID = answerInfo.UserID
		msg.TriggerUserID = answerInfo.UserID
		msg.ObjectType = constant.AnswerObjectType
		msg.NotificationAction = constant.NotificationYourAnswerWasDeleted
		as.notificationQueueService.Send(ctx, msg)
	}

	// recover
	if setStatus == entity.QuestionStatusAvailable && answerInfo.Status == entity.QuestionStatusDeleted {
		if err := as.RecoverAnswer(ctx, &schema.RecoverAnswerReq{
			AnswerID: req.AnswerID,
			UserID:   req.UserID,
		}); err != nil {
			return err
		}
	}
	switch setStatus {
	case entity.AnswerStatusDeleted:
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionDelete, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: answerInfo.QuestionID})
	case entity.AnswerStatusAvailable:
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
		as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: answerInfo.QuestionID})
	}
	return nil
}

func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {
	list := make([]*schema.AnswerInfo, 0)
	questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)
	if err != nil {
		return list, 0, err
	}
	if !exist {
		return list, 0, errors.NotFound(reason.QuestionNotFound)
	}
	if (questionInfo.Status == entity.QuestionStatusDeleted ||
		questionInfo.Status == entity.QuestionStatusPending ||
		questionInfo.Show == entity.QuestionHide) &&
		!req.IsAdminModerator && questionInfo.UserID != req.UserID {
		return list, 0, errors.NotFound(reason.QuestionNotFound)
	}
	dbSearch := entity.AnswerSearch{}
	dbSearch.QuestionID = req.QuestionID
	dbSearch.Page = req.Page
	dbSearch.PageSize = req.PageSize
	dbSearch.Order = req.Order
	dbSearch.IncludeDeleted = req.CanDelete
	dbSearch.LoginUserID = req.UserID
	answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch)
	if err != nil {
		return list, count, err
	}
	answerList, err := as.SearchFormatInfo(ctx, answerOriginalList, req)
	if err != nil {
		return answerList, count, err
	}
	return answerList, count, nil
}

func (as *AnswerService) SearchFormatInfo(ctx context.Context, answers []*entity.Answer, req *schema.AnswerListReq) (
	[]*schema.AnswerInfo, error) {
	list := make([]*schema.AnswerInfo, 0)
	objectIDs := make([]string, 0)
	userIDs := make([]string, 0)
	for _, info := range answers {
		item := as.ShowFormat(ctx, info)
		list = append(list, item)
		objectIDs = append(objectIDs, info.ID)
		userIDs = append(userIDs, info.UserID, info.LastEditUserID)
	}

	userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
	if err != nil {
		return list, err
	}
	for _, item := range list {
		item.UserInfo = userInfoMap[item.UserID]
		item.UpdateUserInfo = userInfoMap[item.UpdateUserID]
	}
	if len(req.UserID) == 0 {
		return list, nil
	}

	collectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, req.UserID, objectIDs)
	if err != nil {
		return nil, err
	}
	for _, item := range list {
		item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, req.UserID)
		item.Collected = collectedMap[item.ID]
		item.MemberActions = permission.GetAnswerPermission(ctx,
			req.UserID,
			item.UserID,
			item.Status,
			req.CanEdit,
			req.CanDelete,
			req.CanRecover)
	}
	return list, nil
}

func (as *AnswerService) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo {
	return as.AnswerCommon.ShowFormat(ctx, data)
}

func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionUserID, answerID, answerUserID string) {
	// If the answer is updated by me, there is no notification for myself.
	// equivalent behaviour as AnswerService.notificationAnswerTheQuestion
	if questionUserID == answerUserID {
		return
	}
	msg := &schema.NotificationMsg{
		TriggerUserID:  answerUserID,
		ReceiverUserID: questionUserID,
		Type:           schema.NotificationTypeInbox,
		ObjectID:       answerID,
	}
	msg.ObjectType = constant.AnswerObjectType
	msg.NotificationAction = constant.NotificationUpdateAnswer
	as.notificationQueueService.Send(ctx, msg)
}

func (as *AnswerService) 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
	as.notificationQueueService.Send(ctx, msg)

	receiverUserInfo, exist, err := as.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, _, _ := as.userCommon.GetUserBasicInfoByID(ctx, answerUserID)
	if answerUser != nil {
		rawData.AnswerUserDisplayName = answerUser.DisplayName
	}
	externalNotificationMsg.NewAnswerTemplateRawData = rawData
	as.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
}
