/*
 * 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 (
	"encoding/json"
	"fmt"
	"strings"
	"time"

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

	"github.com/apache/answer/internal/base/constant"
	"github.com/apache/answer/internal/base/handler"
	"github.com/apache/answer/internal/base/pager"
	"github.com/apache/answer/internal/base/reason"
	"github.com/apache/answer/internal/base/translator"
	"github.com/apache/answer/internal/base/validator"
	"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/config"
	"github.com/apache/answer/internal/service/export"
	metacommon "github.com/apache/answer/internal/service/meta_common"
	"github.com/apache/answer/internal/service/noticequeue"
	"github.com/apache/answer/internal/service/notification"
	"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"
	"github.com/apache/answer/internal/service/siteinfo_common"
	"github.com/apache/answer/internal/service/tag"
	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/checker"
	"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/jinzhu/copier"
	"github.com/segmentfault/pacman/errors"
	"github.com/segmentfault/pacman/log"
	"golang.org/x/net/context"
)

// QuestionRepo question repository

// QuestionService user service
type QuestionService struct {
	activityRepo                     activity_common.ActivityRepo
	questionRepo                     questioncommon.QuestionRepo
	answerRepo                       answercommon.AnswerRepo
	tagCommon                        *tagcommon.TagCommonService
	tagService                       *tag.TagService
	questioncommon                   *questioncommon.QuestionCommon
	userCommon                       *usercommon.UserCommon
	userRepo                         usercommon.UserRepo
	userRoleRelService               *role.UserRoleRelService
	revisionService                  *revision_common.RevisionService
	metaService                      *metacommon.MetaCommonService
	collectionCommon                 *collectioncommon.CollectionCommon
	answerActivityService            *activity.AnswerActivityService
	emailService                     *export.EmailService
	notificationQueueService         noticequeue.Service
	externalNotificationQueueService noticequeue.ExternalService
	activityQueueService             activityqueue.Service
	siteInfoService                  siteinfo_common.SiteInfoCommonService
	newQuestionNotificationService   *notification.ExternalNotificationService
	reviewService                    *review.ReviewService
	configService                    *config.ConfigService
	eventQueueService                eventqueue.Service
	reviewRepo                       review.ReviewRepo
	vectorSyncService                vector_sync.Service
}

func NewQuestionService(
	activityRepo activity_common.ActivityRepo,
	questionRepo questioncommon.QuestionRepo,
	answerRepo answercommon.AnswerRepo,
	tagCommon *tagcommon.TagCommonService,
	tagService *tag.TagService,
	questioncommon *questioncommon.QuestionCommon,
	userCommon *usercommon.UserCommon,
	userRepo usercommon.UserRepo,
	userRoleRelService *role.UserRoleRelService,
	revisionService *revision_common.RevisionService,
	metaService *metacommon.MetaCommonService,
	collectionCommon *collectioncommon.CollectionCommon,
	answerActivityService *activity.AnswerActivityService,
	emailService *export.EmailService,
	notificationQueueService noticequeue.Service,
	externalNotificationQueueService noticequeue.ExternalService,
	activityQueueService activityqueue.Service,
	siteInfoService siteinfo_common.SiteInfoCommonService,
	newQuestionNotificationService *notification.ExternalNotificationService,
	reviewService *review.ReviewService,
	configService *config.ConfigService,
	eventQueueService eventqueue.Service,
	reviewRepo review.ReviewRepo,
	vectorSyncService vector_sync.Service,
) *QuestionService {
	return &QuestionService{
		activityRepo:                     activityRepo,
		questionRepo:                     questionRepo,
		answerRepo:                       answerRepo,
		tagCommon:                        tagCommon,
		tagService:                       tagService,
		questioncommon:                   questioncommon,
		userCommon:                       userCommon,
		userRepo:                         userRepo,
		userRoleRelService:               userRoleRelService,
		revisionService:                  revisionService,
		metaService:                      metaService,
		collectionCommon:                 collectionCommon,
		answerActivityService:            answerActivityService,
		emailService:                     emailService,
		notificationQueueService:         notificationQueueService,
		externalNotificationQueueService: externalNotificationQueueService,
		activityQueueService:             activityQueueService,
		siteInfoService:                  siteInfoService,
		newQuestionNotificationService:   newQuestionNotificationService,
		reviewService:                    reviewService,
		configService:                    configService,
		eventQueueService:                eventQueueService,
		reviewRepo:                       reviewRepo,
		vectorSyncService:                vectorSyncService,
	}
}

func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQuestionReq) error {
	questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
	if err != nil {
		return err
	}
	if !has {
		return nil
	}

	cf, err := qs.configService.GetConfigByID(ctx, req.CloseType)
	if err != nil || cf == nil {
		return errors.BadRequest(reason.ReportNotFound)
	}
	if cf.Key == constant.ReasonADuplicate && !checker.IsURL(req.CloseMsg) {
		return errors.BadRequest(reason.InvalidURLError)
	}

	questionInfo.Status = entity.QuestionStatusClosed
	err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status)
	if err != nil {
		return err
	}

	closeMeta, _ := json.Marshal(schema.CloseQuestionMeta{
		CloseType: req.CloseType,
		CloseMsg:  req.CloseMsg,
	})
	err = qs.metaService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta))
	if err != nil {
		return err
	}
	if cf.Key == constant.ReasonADuplicate {
		qs.questioncommon.AddQuestionLinkForCloseReason(ctx, questionInfo, req.CloseMsg)
	}

	qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
		UserID:           req.UserID,
		ObjectID:         questionInfo.ID,
		OriginalObjectID: questionInfo.ID,
		ActivityTypeKey:  constant.ActQuestionClosed,
	})
	return nil
}

// ReopenQuestion reopen question
func (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.ReopenQuestionReq) error {
	questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID)
	if err != nil {
		return err
	}
	if !has {
		return nil
	}

	questionInfo.Status = entity.QuestionStatusAvailable
	err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status)
	if err != nil {
		return err
	}
	qs.questioncommon.RemoveQuestionLinkForReopen(ctx, questionInfo)
	qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
		UserID:           req.UserID,
		ObjectID:         questionInfo.ID,
		OriginalObjectID: questionInfo.ID,
		ActivityTypeKey:  constant.ActQuestionReopened,
	})
	return nil
}

func (qs *QuestionService) AddQuestionCheckTags(ctx context.Context, tags []*entity.Tag) ([]string, error) {
	list := make([]string, 0)
	for _, tag := range tags {
		if tag.Reserved {
			list = append(list, tag.DisplayName)
		}
	}
	if len(list) > 0 {
		return list, errors.BadRequest(reason.RequestFormatError)
	}
	return []string{}, nil
}
func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.QuestionAdd) (errorlist any, err error) {
	minimumTags, err := qs.tagCommon.GetMinimumTags(ctx)
	if err != nil {
		return
	}
	if len(req.Tags) < minimumTags {
		errorlist := make([]*validator.FormErrorField, 0)
		errorlist = append(errorlist, &validator.FormErrorField{
			ErrorField: "tags",
			ErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount),
		})
		err = errors.BadRequest(reason.TagMinCount)
		return errorlist, err
	}
	minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx)
	if err != nil {
		return
	}
	if len(req.Content) < minimumContentLength {
		errorlist := make([]*validator.FormErrorField, 0)
		errorlist = append(errorlist, &validator.FormErrorField{
			ErrorField: "content",
			ErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum),
		})
		err = errors.BadRequest(reason.QuestionContentLessThanMinimum)
		return errorlist, err
	}
	recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
	if err != nil {
		return
	}
	if !recommendExist {
		errorlist := make([]*validator.FormErrorField, 0)
		errorlist = append(errorlist, &validator.FormErrorField{
			ErrorField: "tags",
			ErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
		})
		err = errors.BadRequest(reason.RecommendTagEnter)
		return errorlist, err
	}

	tagNameList := make([]string, 0)
	for _, tag := range req.Tags {
		tagNameList = append(tagNameList, tag.SlugName)
	}
	Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
	if tagerr != nil {
		return errorlist, tagerr
	}
	if !req.CanUseReservedTag {
		taglist, err := qs.AddQuestionCheckTags(ctx, Tags)
		errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
			strings.Join(taglist, ","))
		if err != nil {
			errorlist := make([]*validator.FormErrorField, 0)
			errorlist = append(errorlist, &validator.FormErrorField{
				ErrorField: "tags",
				ErrorMsg:   errMsg,
			})
			err = errors.BadRequest(reason.RecommendTagEnter)
			return errorlist, err
		}
	}
	return nil, nil
}

// HasNewTag
func (qs *QuestionService) HasNewTag(ctx context.Context, tags []*schema.TagItem) (bool, error) {
	return qs.tagCommon.HasNewTag(ctx, tags)
}

// AddQuestion add question
func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, err error) {
	minimumTags, err := qs.tagCommon.GetMinimumTags(ctx)
	if err != nil {
		return
	}
	if len(req.Tags) < minimumTags {
		errorlist := make([]*validator.FormErrorField, 0)
		errorlist = append(errorlist, &validator.FormErrorField{
			ErrorField: "tags",
			ErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount),
		})
		err = errors.BadRequest(reason.TagMinCount)
		return errorlist, err
	}
	minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx)
	if err != nil {
		return
	}
	if len(req.Content) < minimumContentLength {
		errorlist := make([]*validator.FormErrorField, 0)
		errorlist = append(errorlist, &validator.FormErrorField{
			ErrorField: "content",
			ErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum),
		})
		err = errors.BadRequest(reason.QuestionContentLessThanMinimum)
		return errorlist, err
	}
	recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
	if err != nil {
		return
	}
	if !recommendExist {
		errorlist := make([]*validator.FormErrorField, 0)
		errorlist = append(errorlist, &validator.FormErrorField{
			ErrorField: "tags",
			ErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
		})
		err = errors.BadRequest(reason.RecommendTagEnter)
		return errorlist, err
	}

	tagNameList := make([]string, 0)
	for _, tag := range req.Tags {
		tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-")
		tagNameList = append(tagNameList, tag.SlugName)
	}
	tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
	if tagerr != nil {
		return questionInfo, tagerr
	}
	if !req.CanUseReservedTag {
		taglist, err := qs.AddQuestionCheckTags(ctx, tags)
		errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
			strings.Join(taglist, ","))
		if err != nil {
			errorlist := make([]*validator.FormErrorField, 0)
			errorlist = append(errorlist, &validator.FormErrorField{
				ErrorField: "tags",
				ErrorMsg:   errMsg,
			})
			err = errors.BadRequest(reason.RecommendTagEnter)
			return errorlist, err
		}
	}

	question := &entity.Question{}
	now := time.Now()
	question.UserID = req.UserID
	question.Title = req.Title
	question.OriginalText = req.Content
	question.ParsedText = req.HTML
	question.AcceptedAnswerID = "0"
	question.LastAnswerID = "0"
	question.LastEditUserID = "0"
	// question.PostUpdateTime = nil
	question.Status = entity.QuestionStatusPending
	question.RevisionID = "0"
	question.CreatedAt = now
	question.PostUpdateTime = now
	question.Pin = entity.QuestionUnPin
	question.Show = entity.QuestionShow
	// question.UpdatedAt = nil
	err = qs.questionRepo.AddQuestion(ctx, question)
	if err != nil {
		return
	}
	question.Status = qs.reviewService.AddQuestionReview(ctx, question, req.Tags, req.IP, req.UserAgent)
	if err := qs.questionRepo.UpdateQuestionStatus(ctx, question.ID, question.Status); err != nil {
		return nil, err
	}
	if question.Status == entity.QuestionStatusAvailable {
		question.ParsedText, err = qs.questioncommon.UpdateQuestionLink(ctx, question.ID, "", question.ParsedText, question.OriginalText)
		if err != nil {
			return nil, err
		}
		err = qs.questionRepo.UpdateQuestion(ctx, question, []string{"parsed_text"})
		if err != nil {
			return nil, err
		}
	}
	objectTagData := schema.TagChange{}
	objectTagData.ObjectID = question.ID
	objectTagData.Tags = req.Tags
	objectTagData.UserID = req.UserID
	errorlist, err := qs.ChangeTag(ctx, &objectTagData)
	if err != nil {
		return errorlist, err
	}
	_ = qs.questionRepo.UpdateSearch(ctx, question.ID)

	revisionDTO := &schema.AddRevisionDTO{
		UserID:   question.UserID,
		ObjectID: question.ID,
		Title:    question.Title,
	}

	questionWithTagsRevision := qs.changeQuestionToRevision(ctx, question, tags)
	infoJSON, _ := json.Marshal(questionWithTagsRevision)
	revisionDTO.Content = string(infoJSON)
	revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true)
	if err != nil {
		return
	}

	// user add question count
	userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, question.UserID)
	if err != nil {
		log.Errorf("get user question count error %v", err)
	} else {
		err = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, userQuestionCount)
		if err != nil {
			log.Errorf("update user question count error %v", err)
		}
	}

	qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
		UserID:           question.UserID,
		ObjectID:         question.ID,
		OriginalObjectID: question.ID,
		ActivityTypeKey:  constant.ActQuestionAsked,
		RevisionID:       revisionID,
	})

	if question.Status == entity.QuestionStatusAvailable {
		newTags, newTagsErr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
		if newTagsErr != nil {
			log.Error("get question newTags error %v", newTagsErr)
			qs.externalNotificationQueueService.Send(ctx,
				schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, tags))
		} else {
			qs.externalNotificationQueueService.Send(ctx,
				schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, newTags))
		}
	}
	qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionCreate, req.UserID).TID(question.ID).
		QID(question.ID, question.UserID))
	if question.Status == entity.QuestionStatusAvailable {
		qs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: question.ID})
	}

	questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission)
	return
}

// OperationQuestion
func (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.OperationQuestionReq) (err error) {
	questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
	if err != nil {
		return err
	}
	if !has {
		return nil
	}
	// Hidden question cannot be placed at the top
	if questionInfo.Show == entity.QuestionHide && req.Operation == schema.QuestionOperationPin {
		return nil
	}
	// Question cannot be hidden when they are at the top
	if questionInfo.Pin == entity.QuestionPin && req.Operation == schema.QuestionOperationHide {
		return nil
	}

	switch req.Operation {
	case schema.QuestionOperationHide:
		questionInfo.Show = entity.QuestionHide
		err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{
			FromQuestionID: questionInfo.ID,
		}, &entity.QuestionLink{
			ToQuestionID: questionInfo.ID,
		})
		if err != nil {
			return
		}
		err = qs.tagCommon.HideTagRelListByObjectID(ctx, req.ID)
		if err != nil {
			return err
		}
		err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID)
		if err != nil {
			return err
		}
	case schema.QuestionOperationShow:
		questionInfo.Show = entity.QuestionShow
		err = qs.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{
			FromQuestionID: questionInfo.ID,
		}, &entity.QuestionLink{
			ToQuestionID: questionInfo.ID,
		})
		if err != nil {
			return
		}
		err = qs.tagCommon.ShowTagRelListByObjectID(ctx, req.ID)
		if err != nil {
			return err
		}
		err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID)
		if err != nil {
			return err
		}
	case schema.QuestionOperationPin:
		questionInfo.Pin = entity.QuestionPin
	case schema.QuestionOperationUnPin:
		questionInfo.Pin = entity.QuestionUnPin
	}

	err = qs.questionRepo.UpdateQuestionOperation(ctx, questionInfo)
	if err != nil {
		return err
	}

	actMap := make(map[string]constant.ActivityTypeKey)
	actMap[schema.QuestionOperationPin] = constant.ActQuestionPin
	actMap[schema.QuestionOperationUnPin] = constant.ActQuestionUnPin
	actMap[schema.QuestionOperationHide] = constant.ActQuestionHide
	actMap[schema.QuestionOperationShow] = constant.ActQuestionShow
	_, ok := actMap[req.Operation]
	if ok {
		qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           req.UserID,
			ObjectID:         questionInfo.ID,
			OriginalObjectID: questionInfo.ID,
			ActivityTypeKey:  actMap[req.Operation],
		})
	}

	return nil
}

// RemoveQuestion delete question
func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) {
	questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
	if err != nil {
		return err
	}
	// if the status is deleted, return directly
	if questionInfo.Status == entity.QuestionStatusDeleted {
		return nil
	}
	if !has {
		return nil
	}
	if !req.IsAdmin {
		if questionInfo.UserID != req.UserID {
			return errors.BadRequest(reason.QuestionCannotDeleted)
		}

		if questionInfo.AcceptedAnswerID != "0" {
			return errors.BadRequest(reason.QuestionCannotDeleted)
		}
		if questionInfo.AnswerCount > 1 {
			return errors.BadRequest(reason.QuestionCannotDeleted)
		}

		if questionInfo.AnswerCount == 1 {
			answersearch := &entity.AnswerSearch{}
			answersearch.QuestionID = req.ID
			answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
			if err != nil {
				return err
			}
			for _, answer := range answerList {
				if answer.VoteCount > 0 {
					return errors.BadRequest(reason.QuestionCannotDeleted)
				}
			}
		}
	}

	questionInfo.Status = entity.QuestionStatusDeleted
	err = qs.questionRepo.UpdateQuestionStatusWithOutUpdateTime(ctx, questionInfo)
	if err != nil {
		return err
	}

	userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID)
	if err != nil {
		log.Error("user GetUserQuestionCount error", err.Error())
	} else {
		err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)
		if err != nil {
			log.Error("user IncreaseQuestionCount error", err.Error())
		}
	}

	// If this question has been reviewed, then delete the review.
	reviewInfo, exist, err := qs.reviewRepo.GetReviewByObject(ctx, questionInfo.ID)
	if exist && err == nil {
		err = qs.reviewRepo.UpdateReviewStatus(ctx, reviewInfo.ID, req.UserID, entity.ReviewStatusRejected)
		if err != nil {
			return errors.InternalServer(reason.DatabaseError)
		}
	}

	// tag count
	tagIDs := make([]string, 0)
	Tags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID)
	if tagerr != nil {
		log.Error("GetObjectEntityTag error", tagerr)
		return nil
	}
	for _, v := range Tags {
		tagIDs = append(tagIDs, v.ID)
	}
	err = qs.tagCommon.RemoveTagRelListByObjectID(ctx, req.ID)
	if err != nil {
		log.Error("RemoveTagRelListByObjectID error", err.Error())
	}
	err = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs)
	if err != nil {
		log.Error("efreshTagQuestionCount 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 = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount)
	// if err != nil {
	// 	 log.Errorf("user DeleteQuestion rank rollback error %s", err.Error())
	// }
	err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{
		FromQuestionID: questionInfo.ID,
	}, &entity.QuestionLink{
		ToQuestionID: questionInfo.ID,
	})
	if err != nil {
		return
	}
	qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
		UserID:           questionInfo.UserID,
		TriggerUserID:    converter.StringToInt64(req.UserID),
		ObjectID:         questionInfo.ID,
		OriginalObjectID: questionInfo.ID,
		ActivityTypeKey:  constant.ActQuestionDeleted,
	})
	qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionDelete, req.UserID).TID(questionInfo.ID).
		QID(questionInfo.ID, questionInfo.UserID))
	qs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionDelete, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: questionInfo.ID})
	return nil
}

func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *schema.QuestionUpdate) (errorlist []*validator.FormErrorField, err error) {
	dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
	if err != nil {
		return
	}
	if !has {
		return
	}

	oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID)
	if tagerr != nil {
		log.Error("GetObjectEntityTag error", tagerr)
		return nil, nil
	}

	tagNameList := make([]string, 0)
	oldtagNameList := make([]string, 0)
	for _, tag := range req.Tags {
		tagNameList = append(tagNameList, tag.SlugName)
	}
	for _, tag := range oldTags {
		oldtagNameList = append(oldtagNameList, tag.SlugName)
	}

	isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList)

	// If the content is the same, ignore it
	if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange {
		return
	}

	Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
	if tagerr != nil {
		log.Error("GetTagListByNames error", tagerr)
		return nil, nil
	}

	// if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added.
	if !req.CanUseReservedTag {
		CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)
		if !CheckOldTag {
			errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`,
				strings.Join(CheckOldTaglist, ","))
			errorlist := make([]*validator.FormErrorField, 0)
			errorlist = append(errorlist, &validator.FormErrorField{
				ErrorField: "tags",
				ErrorMsg:   errMsg,
			})
			err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
			return errorlist, err
		}
		if !CheckNewTag {
			errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
				strings.Join(CheckNewTaglist, ","))
			errorlist := make([]*validator.FormErrorField, 0)
			errorlist = append(errorlist, &validator.FormErrorField{
				ErrorField: "tags",
				ErrorMsg:   errMsg,
			})
			err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
			return errorlist, err
		}
	}
	return nil, nil
}

func (qs *QuestionService) RecoverQuestion(ctx context.Context, req *schema.QuestionRecoverReq) (err error) {
	questionInfo, exist, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID)
	if err != nil {
		return err
	}
	if !exist {
		return errors.BadRequest(reason.QuestionNotFound)
	}
	if questionInfo.Status != entity.QuestionStatusDeleted {
		return nil
	}

	err = qs.questionRepo.RecoverQuestion(ctx, req.QuestionID)
	if err != nil {
		return err
	}

	// update user's question count
	userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID)
	if err != nil {
		log.Error("user GetUserQuestionCount error", err.Error())
	} else {
		err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)
		if err != nil {
			log.Error("user IncreaseQuestionCount error", err.Error())
		}
	}

	// update tag's question count
	if err = qs.tagCommon.RecoverTagRelListByObjectID(ctx, questionInfo.ID); err != nil {
		log.Errorf("remove tag rel list by object id error %v", err)
	}

	tagIDs := make([]string, 0)
	tags, err := qs.tagCommon.GetObjectEntityTag(ctx, questionInfo.ID)
	if err != nil {
		return err
	}
	for _, v := range tags {
		tagIDs = append(tagIDs, v.ID)
	}
	if len(tagIDs) > 0 {
		if err = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs); err != nil {
			log.Errorf("update tag's question count failed, %v", err)
		}
	}
	err = qs.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{
		FromQuestionID: questionInfo.ID,
	}, &entity.QuestionLink{
		ToQuestionID: questionInfo.ID,
	})
	if err != nil {
		return
	}

	qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
		UserID:           req.UserID,
		TriggerUserID:    converter.StringToInt64(req.UserID),
		ObjectID:         questionInfo.ID,
		OriginalObjectID: questionInfo.ID,
		ActivityTypeKey:  constant.ActQuestionUndeleted,
	})
	qs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: questionInfo.ID})
	return nil
}

func (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *schema.QuestionUpdateInviteUser) (err error) {
	originQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, req.ID)
	if err != nil {
		return err
	}
	if !exist {
		return errors.BadRequest(reason.QuestionNotFound)
	}

	// verify invite user
	inviteUserInfoList, err := qs.userCommon.BatchGetUserBasicInfoByUserNames(ctx, req.InviteUser)
	if err != nil {
		log.Error("BatchGetUserBasicInfoByUserNames error", err.Error())
	}
	inviteUserIDs := make([]string, 0)
	for _, item := range req.InviteUser {
		_, ok := inviteUserInfoList[item]
		if ok {
			inviteUserIDs = append(inviteUserIDs, inviteUserInfoList[item].ID)
		}
	}
	inviteUserStr := ""
	inviteUserByte, err := json.Marshal(inviteUserIDs)
	if err != nil {
		log.Error("json.Marshal error", err.Error())
		inviteUserStr = "[]"
	} else {
		inviteUserStr = string(inviteUserByte)
	}
	question := &entity.Question{}
	question.ID = uid.DeShortID(req.ID)
	question.InviteUserID = inviteUserStr

	saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"invite_user_id"})
	if saveerr != nil {
		return saveerr
	}
	// send notification
	oldInviteUserIDsStr := originQuestion.InviteUserID
	oldInviteUserIDs := make([]string, 0)
	needSendNotificationUserIDs := make([]string, 0)
	if oldInviteUserIDsStr != "" {
		err = json.Unmarshal([]byte(oldInviteUserIDsStr), &oldInviteUserIDs)
		if err == nil {
			needSendNotificationUserIDs = converter.ArrayNotInArray(oldInviteUserIDs, inviteUserIDs)
		}
	} else {
		needSendNotificationUserIDs = inviteUserIDs
	}
	go qs.notificationInviteUser(ctx, needSendNotificationUserIDs, originQuestion.ID, originQuestion.Title, req.UserID)

	return nil
}

func (qs *QuestionService) notificationInviteUser(
	ctx context.Context, invitedUserIDs []string, questionID, questionTitle, questionUserID string) {
	inviter, exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, questionUserID)
	if err != nil {
		log.Error(err)
		return
	}
	if !exist {
		log.Warnf("user %s not found", questionUserID)
		return
	}

	users, err := qs.userRepo.BatchGetByID(ctx, invitedUserIDs)
	if err != nil {
		log.Error(err)
		return
	}
	invitee := make(map[string]*entity.User, len(users))
	for _, user := range users {
		invitee[user.ID] = user
	}
	for _, userID := range invitedUserIDs {
		msg := &schema.NotificationMsg{
			ReceiverUserID: userID,
			TriggerUserID:  questionUserID,
			Type:           schema.NotificationTypeInbox,
			ObjectID:       questionID,
		}
		msg.ObjectType = constant.QuestionObjectType
		msg.NotificationAction = constant.NotificationInvitedYouToAnswer
		qs.notificationQueueService.Send(ctx, msg)

		receiverUserInfo, ok := invitee[userID]
		if !ok {
			log.Warnf("user %s not found", userID)
			return
		}
		externalNotificationMsg := &schema.ExternalNotificationMsg{
			ReceiverUserID: receiverUserInfo.ID,
			ReceiverEmail:  receiverUserInfo.EMail,
			ReceiverLang:   receiverUserInfo.Language,
		}
		rawData := &schema.NewInviteAnswerTemplateRawData{
			InviterDisplayName: inviter.DisplayName,
			QuestionTitle:      questionTitle,
			QuestionID:         questionID,
			UnsubscribeCode:    token.GenerateToken(),
		}
		externalNotificationMsg.NewInviteAnswerTemplateRawData = rawData
		qs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
	}
}

// UpdateQuestion update question
func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) {
	var canUpdate bool
	questionInfo = &schema.QuestionInfoResp{}

	_, existUnreviewed, err := qs.revisionService.ExistUnreviewedByObjectID(ctx, req.ID)
	if err != nil {
		return
	}
	if existUnreviewed {
		err = errors.BadRequest(reason.QuestionCannotUpdate)
		return
	}

	dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
	if err != nil {
		return
	}
	if !has {
		return
	}
	if dbinfo.Status == entity.QuestionStatusDeleted {
		err = errors.BadRequest(reason.QuestionCannotUpdate)
		return nil, err
	}

	now := time.Now()
	question := &entity.Question{}
	question.Title = req.Title
	question.OriginalText = req.Content
	question.ParsedText = req.HTML
	question.ID = uid.DeShortID(req.ID)
	question.UpdatedAt = now
	question.PostUpdateTime = now
	question.UserID = dbinfo.UserID
	question.LastEditUserID = req.UserID

	minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx)
	if err != nil {
		return
	}
	if len(req.Content) < minimumContentLength {
		errorlist := make([]*validator.FormErrorField, 0)
		errorlist = append(errorlist, &validator.FormErrorField{
			ErrorField: "content",
			ErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum),
		})
		err = errors.BadRequest(reason.QuestionContentLessThanMinimum)
		return errorlist, err
	}

	oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)
	if tagerr != nil {
		return questionInfo, tagerr
	}

	tagNameList := make([]string, 0)
	oldtagNameList := make([]string, 0)
	for _, tag := range req.Tags {
		tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-")
		tagNameList = append(tagNameList, tag.SlugName)
	}
	for _, tag := range oldTags {
		oldtagNameList = append(oldtagNameList, tag.SlugName)
	}

	isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList)

	// If the content is the same, ignore it
	if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange {
		return
	}

	Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
	if tagerr != nil {
		return questionInfo, tagerr
	}

	// if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added.
	if !req.CanUseReservedTag {
		CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)
		if !CheckOldTag {
			errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`,
				strings.Join(CheckOldTaglist, ","))
			errorlist := make([]*validator.FormErrorField, 0)
			errorlist = append(errorlist, &validator.FormErrorField{
				ErrorField: "tags",
				ErrorMsg:   errMsg,
			})
			err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
			return errorlist, err
		}
		if !CheckNewTag {
			errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
				strings.Join(CheckNewTaglist, ","))
			errorlist := make([]*validator.FormErrorField, 0)
			errorlist = append(errorlist, &validator.FormErrorField{
				ErrorField: "tags",
				ErrorMsg:   errMsg,
			})
			err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
			return errorlist, err
		}
	}
	// Check whether mandatory labels are selected
	recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
	if err != nil {
		return
	}
	if !recommendExist {
		errorlist := make([]*validator.FormErrorField, 0)
		errorlist = append(errorlist, &validator.FormErrorField{
			ErrorField: "tags",
			ErrorMsg:   translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter),
		})
		err = errors.BadRequest(reason.RecommendTagEnter)
		return errorlist, err
	}

	// Administrators and themselves do not need to be audited

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

	if req.NoNeedReview {
		canUpdate = true
	}

	// It's not you or the administrator that needs to be reviewed
	if !canUpdate {
		revisionDTO.Status = entity.RevisionUnreviewedStatus
		revisionDTO.UserID = req.UserID // use revision userid
	} else {
		// Direct modification
		revisionDTO.Status = entity.RevisionReviewPassStatus
		// update question to db
		question.ParsedText, err = qs.questioncommon.UpdateQuestionLink(ctx, question.ID, "", question.ParsedText, question.OriginalText)
		if err != nil {
			return questionInfo, err
		}
		saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"})
		if saveerr != nil {
			return questionInfo, saveerr
		}
		objectTagData := schema.TagChange{}
		objectTagData.ObjectID = question.ID
		objectTagData.Tags = req.Tags
		objectTagData.UserID = req.UserID
		errorlist, tagerr := qs.ChangeTag(ctx, &objectTagData)
		if tagerr != nil {
			return errorlist, tagerr
		}
	}

	questionWithTagsRevision := qs.changeQuestionToRevision(ctx, question, Tags)
	infoJSON, _ := json.Marshal(questionWithTagsRevision)
	revisionDTO.Content = string(infoJSON)
	revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true)
	if err != nil {
		return
	}
	if canUpdate {
		qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           req.UserID,
			ObjectID:         question.ID,
			ActivityTypeKey:  constant.ActQuestionEdited,
			RevisionID:       revisionID,
			OriginalObjectID: question.ID,
		})
		qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionUpdate, req.UserID).TID(question.ID).
			QID(question.ID, question.UserID))
		qs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: question.ID})
	}

	questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission)
	return
}

// GetQuestion get question one
func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID string,
	per schema.QuestionPermission) (resp *schema.QuestionInfoResp, err error) {
	question, err := qs.questioncommon.Info(ctx, questionID, userID)
	if err != nil {
		return
	}
	// If the question is deleted or pending, only the administrator and the author can view it
	if (question.Status == entity.QuestionStatusDeleted ||
		question.Status == entity.QuestionStatusPending) && !per.CanReopen && question.UserID != userID {
		return nil, errors.NotFound(reason.QuestionNotFound)
	}
	if question.Show == entity.QuestionHide && !per.IsAdminModerator && question.UserID != userID {
		return nil, errors.NotFound(reason.QuestionNotFound)
	}
	if question.Status != entity.QuestionStatusClosed {
		per.CanReopen = false
	}
	if question.Status == entity.QuestionStatusClosed {
		per.CanClose = false
	}
	if question.Pin == entity.QuestionPin {
		per.CanPin = false
		per.CanHide = false
	}
	if question.Pin == entity.QuestionUnPin {
		per.CanUnPin = false
	}
	if question.Show == entity.QuestionShow {
		per.CanShow = false
	}
	if question.Show == entity.QuestionHide {
		per.CanHide = false
		per.CanPin = false
	}

	if question.Status == entity.QuestionStatusDeleted {
		operation := &schema.Operation{}
		operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionAlreadyDeleted)
		operation.Level = schema.OperationLevelDanger
		question.Operation = operation
	}
	if question.Status == entity.QuestionStatusPending {
		operation := &schema.Operation{}
		operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionUnderReview)
		operation.Level = schema.OperationLevelSecondary
		question.Operation = operation
	}

	question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240)
	question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID, question.Status,
		per.CanEdit, per.CanDelete,
		per.CanClose, per.CanReopen, per.CanPin, per.CanHide, per.CanUnPin, per.CanShow,
		per.CanRecover)
	question.ExtendsActions = permission.GetQuestionExtendsPermission(ctx, per.CanInviteOtherToAnswer)
	return question, nil
}

// GetQuestionAndAddPV get question one
func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string,
	per schema.QuestionPermission) (
	resp *schema.QuestionInfoResp, err error) {
	err = qs.questioncommon.UpdatePv(ctx, questionID)
	if err != nil {
		log.Error(err)
	}
	return qs.GetQuestion(ctx, questionID, loginUserID, per)
}

func (qs *QuestionService) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) {
	return qs.questioncommon.InviteUserInfo(ctx, questionID)
}

func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) (errorlist []*validator.FormErrorField, err error) {
	minimumTags, err := qs.tagCommon.GetMinimumTags(ctx)
	if err != nil {
		return nil, err
	}
	return qs.tagCommon.ObjectChangeTag(ctx, objectTagData, minimumTags)
}

func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) {
	return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData)
}

// PersonalQuestionPage get question list by user
func (qs *QuestionService) PersonalQuestionPage(ctx context.Context, req *schema.PersonalQuestionPageReq) (
	pageModel *pager.PageModel, err error) {
	userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
	if err != nil {
		return nil, err
	}
	if !exist {
		return nil, errors.BadRequest(reason.UserNotFound)
	}
	search := &schema.QuestionPageReq{}
	search.OrderCond = req.OrderCond
	search.Page = req.Page
	search.PageSize = req.PageSize
	search.UserIDBeSearched = userinfo.ID
	search.LoginUserID = req.LoginUserID
	// Only author and administrator can view the pending question
	if req.LoginUserID == userinfo.ID || req.IsAdmin {
		search.ShowPending = true
	}
	questionList, total, err := qs.GetQuestionPage(ctx, search)
	if err != nil {
		return nil, err
	}
	userQuestionInfoList := make([]*schema.UserQuestionInfo, 0)
	for _, item := range questionList {
		info := &schema.UserQuestionInfo{}
		_ = copier.Copy(info, item)
		status, ok := entity.AdminQuestionSearchStatusIntToString[item.Status]
		if ok {
			info.Status = status
		}
		userQuestionInfoList = append(userQuestionInfoList, info)
	}
	return pager.NewPageModel(total, userQuestionInfoList), nil
}

func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.PersonalAnswerPageReq) (
	pageModel *pager.PageModel, err error) {
	userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
	if err != nil {
		return nil, err
	}
	if !exist {
		return nil, errors.BadRequest(reason.UserNotFound)
	}
	cond := &entity.PersonalAnswerPageQueryCond{}
	cond.UserID = userinfo.ID
	cond.Page = req.Page
	cond.PageSize = req.PageSize
	cond.ShowPending = req.IsAdmin || req.LoginUserID == cond.UserID
	if req.OrderCond == "newest" {
		cond.Order = entity.AnswerSearchOrderByTime
	} else {
		cond.Order = entity.AnswerSearchOrderByDefault
	}
	questionIDs := make([]string, 0)
	answerList, total, err := qs.questioncommon.AnswerCommon.PersonalAnswerPage(ctx, cond)
	if err != nil {
		return nil, err
	}

	answerlist := make([]*schema.AnswerInfo, 0)
	userAnswerlist := make([]*schema.UserAnswerInfo, 0)
	for _, item := range answerList {
		answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item)
		answerlist = append(answerlist, answerinfo)
		questionIDs = append(questionIDs, uid.DeShortID(item.QuestionID))
	}
	questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID)
	if err != nil {
		return nil, err
	}

	for _, item := range answerlist {
		_, ok := questionMaps[item.QuestionID]
		if ok {
			item.QuestionInfo = questionMaps[item.QuestionID]
		} else {
			continue
		}
		info := &schema.UserAnswerInfo{}
		_ = copier.Copy(info, item)
		info.AnswerID = item.ID
		info.QuestionID = item.QuestionID
		if item.QuestionInfo.Status == entity.QuestionStatusDeleted {
			info.QuestionInfo.Title = "Deleted question"
		}
		userAnswerlist = append(userAnswerlist, info)
	}

	return pager.NewPageModel(total, userAnswerlist), nil
}

// PersonalCollectionPage get collection list by user
func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *schema.PersonalCollectionPageReq) (
	pageModel *pager.PageModel, err error) {
	list := make([]*schema.QuestionInfoResp, 0)
	collectionSearch := &entity.CollectionSearch{}
	collectionSearch.UserID = req.UserID
	collectionSearch.Page = req.Page
	collectionSearch.PageSize = req.PageSize
	collectionList, total, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
	if err != nil {
		return nil, err
	}
	questionIDs := make([]string, 0)
	for _, item := range collectionList {
		questionIDs = append(questionIDs, item.ObjectID)
	}

	questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.UserID)
	if err != nil {
		return nil, err
	}
	for _, id := range questionIDs {
		if handler.GetEnableShortID(ctx) {
			id = uid.EnShortID(id)
		}
		_, ok := questionMaps[id]
		if ok {
			questionMaps[id].LastAnsweredUserInfo = nil
			questionMaps[id].UpdateUserInfo = nil
			questionMaps[id].Content = ""
			questionMaps[id].HTML = ""
			if questionMaps[id].Status == entity.QuestionStatusDeleted {
				questionMaps[id].Title = "Deleted question"
			}
			list = append(list, questionMaps[id])
		}
	}

	return pager.NewPageModel(total, list), nil
}

func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName string, loginUserID string) ([]*schema.UserQuestionInfo, []*schema.UserAnswerInfo, error) {
	answerlist := make([]*schema.AnswerInfo, 0)

	userAnswerlist := make([]*schema.UserAnswerInfo, 0)
	userQuestionlist := make([]*schema.UserQuestionInfo, 0)

	userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)
	if err != nil {
		return userQuestionlist, userAnswerlist, err
	}
	if !Exist {
		return userQuestionlist, userAnswerlist, nil
	}
	search := &schema.QuestionPageReq{}
	search.OrderCond = "score"
	search.Page = 0
	search.PageSize = 5
	search.UserIDBeSearched = userinfo.ID
	search.LoginUserID = loginUserID
	questionlist, _, err := qs.GetQuestionPage(ctx, search)
	if err != nil {
		return userQuestionlist, userAnswerlist, err
	}
	answersearch := &entity.AnswerSearch{}
	answersearch.UserID = userinfo.ID
	answersearch.PageSize = 5
	answersearch.Order = entity.AnswerSearchOrderByVote
	questionIDs := make([]string, 0)
	answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
	if err != nil {
		return userQuestionlist, userAnswerlist, err
	}
	for _, item := range answerList {
		answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item)
		answerlist = append(answerlist, answerinfo)
		questionIDs = append(questionIDs, item.QuestionID)
	}
	questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
	if err != nil {
		return userQuestionlist, userAnswerlist, err
	}
	for _, item := range answerlist {
		_, ok := questionMaps[item.QuestionID]
		if ok {
			item.QuestionInfo = questionMaps[item.QuestionID]
		}
	}

	for _, item := range questionlist {
		info := &schema.UserQuestionInfo{}
		_ = copier.Copy(info, item)
		info.UrlTitle = htmltext.UrlTitle(info.Title)
		userQuestionlist = append(userQuestionlist, info)
	}

	for _, item := range answerlist {
		info := &schema.UserAnswerInfo{}
		_ = copier.Copy(info, item)
		info.AnswerID = item.ID
		info.QuestionID = item.QuestionID
		info.QuestionInfo.UrlTitle = htmltext.UrlTitle(info.QuestionInfo.Title)
		userAnswerlist = append(userAnswerlist, info)
	}

	return userQuestionlist, userAnswerlist, nil
}

// GetQuestionsByTitle get questions by title
func (qs *QuestionService) GetQuestionsByTitle(ctx context.Context, title string) (
	resp []*schema.QuestionBaseInfo, err error) {
	resp = make([]*schema.QuestionBaseInfo, 0)
	if len(title) == 0 {
		return resp, nil
	}
	// check search plugin
	var finder plugin.Search
	_ = plugin.CallSearch(func(search plugin.Search) error {
		finder = search
		return nil
	})

	var questions []*entity.Question
	if finder != nil {
		// call search plugin if available
		words := []string{title}
		res, _, err := finder.SearchQuestions(ctx, &plugin.SearchBasicCond{
			Words:    words,
			Page:     1,
			PageSize: 10,
		})
		if err != nil {
			return resp, err
		}
		// get question ids from res
		questionIDs := make([]string, 0)
		for _, question := range res {
			questionIDs = append(questionIDs, question.ID)
		}
		var questionErr error
		questions, questionErr = qs.questionRepo.FindByID(ctx, questionIDs)
		if questionErr != nil {
			return resp, questionErr
		}
	} else {
		var questionErr error
		questions, questionErr = qs.questionRepo.GetQuestionsByTitle(ctx, title, 10)
		if questionErr != nil {
			return resp, questionErr
		}
	}
	for _, question := range questions {
		item := &schema.QuestionBaseInfo{}
		item.ID = question.ID
		item.Title = question.Title
		item.UrlTitle = htmltext.UrlTitle(question.Title)
		item.ViewCount = question.ViewCount
		item.AnswerCount = question.AnswerCount
		item.CollectionCount = question.CollectionCount
		item.FollowCount = question.FollowCount
		status, ok := entity.AdminQuestionSearchStatusIntToString[question.Status]
		if ok {
			item.Status = status
		}
		if question.AcceptedAnswerID != "0" {
			item.AcceptedAnswer = true
		}
		resp = append(resp, item)
	}
	return resp, nil
}

// SimilarQuestion
func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionPageResp, int64, error) {
	question, err := qs.questioncommon.Info(ctx, questionID, loginUserID)
	if err != nil {
		return nil, 0, nil
	}
	tagNames := make([]string, 0, len(question.Tags))
	for _, tag := range question.Tags {
		tagNames = append(tagNames, tag.SlugName)
	}
	search := &schema.QuestionPageReq{}
	search.OrderCond = "hot"
	search.Page = 0
	search.PageSize = 6
	if len(tagNames) > 0 {
		search.Tag = tagNames[0]
	}
	search.LoginUserID = loginUserID
	similarQuestions, _, err := qs.GetQuestionPage(ctx, search)
	if err != nil {
		return nil, 0, err
	}
	var result []*schema.QuestionPageResp
	for _, v := range similarQuestions {
		if uid.DeShortID(v.ID) != questionID {
			result = append(result, v)
		}
	}
	return result, int64(len(result)), nil
}

// GetQuestionPage query questions page
func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.QuestionPageReq) (
	questions []*schema.QuestionPageResp, total int64, err error) {
	questions = make([]*schema.QuestionPageResp, 0)
	// query by user role
	showHidden := false
	if req.LoginUserID != "" && req.UserIDBeSearched != "" {
		showHidden = req.LoginUserID == req.UserIDBeSearched
		if !showHidden {
			userRole, err := qs.userRoleRelService.GetUserRole(ctx, req.LoginUserID)
			if err != nil {
				return nil, 0, err
			}
			showHidden = userRole == role.RoleAdminID || userRole == role.RoleModeratorID
		}
	}
	// query by tag condition
	var tagIDs = make([]string, 0)
	if len(req.Tag) > 0 {
		tagInfo, exist, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))
		if err != nil {
			return nil, 0, err
		}
		if exist {
			synTagIds, err := qs.tagCommon.GetTagIDsByMainTagID(ctx, tagInfo.ID)
			if err != nil {
				return nil, 0, err
			}
			tagIDs = append(tagIDs, synTagIds...)
			tagIDs = append(tagIDs, tagInfo.ID)
		} else {
			return questions, 0, nil
		}
	}

	// query by user condition
	if req.Username != "" {
		userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
		if err != nil {
			return nil, 0, err
		}
		if !exist {
			return questions, 0, nil
		}
		req.UserIDBeSearched = userinfo.ID
	}

	if req.OrderCond == schema.QuestionOrderCondHot {
		req.InDays = schema.HotInDays
	}

	questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize,
		tagIDs, req.UserIDBeSearched, req.OrderCond, req.InDays, showHidden, req.ShowPending)
	if err != nil {
		return nil, 0, err
	}
	questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond)
	if err != nil {
		return nil, 0, err
	}
	return questions, total, nil
}

// GetRecommendQuestionPage retrieves recommended question page based on following tags and questions.
func (qs *QuestionService) GetRecommendQuestionPage(ctx context.Context, req *schema.QuestionPageReq) (
	questions []*schema.QuestionPageResp, total int64, err error) {
	followingTagsResp, err := qs.tagService.GetFollowingTags(ctx, req.LoginUserID)
	if err != nil {
		return nil, 0, err
	}
	tagIDs := make([]string, 0, len(followingTagsResp))
	for _, tag := range followingTagsResp {
		tagIDs = append(tagIDs, tag.TagID)
	}

	activityType, err := qs.activityRepo.GetActivityTypeByObjectType(ctx, constant.QuestionObjectType, "follow")
	if err != nil {
		return nil, 0, err
	}
	activities, err := qs.activityRepo.GetUserActivitiesByActivityType(ctx, req.LoginUserID, activityType)
	if err != nil {
		return nil, 0, err
	}

	followedQuestionIDs := make([]string, 0, len(activities))
	for _, activity := range activities {
		if activity.Cancelled == entity.ActivityCancelled {
			continue
		}
		followedQuestionIDs = append(followedQuestionIDs, activity.ObjectID)
	}
	questionList, total, err := qs.questionRepo.GetRecommendQuestionPageByTags(ctx, req.LoginUserID, tagIDs, followedQuestionIDs, req.Page, req.PageSize)
	if err != nil {
		return nil, 0, err
	}

	questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, schema.QuestionOrderCondFrequent)
	if err != nil {
		return nil, 0, err
	}

	return questions, total, nil
}

func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, req *schema.AdminUpdateQuestionStatusReq) error {
	setStatus, ok := entity.AdminQuestionSearchStatus[req.Status]
	if !ok {
		return errors.BadRequest(reason.RequestFormatError)
	}
	questionInfo, exist, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID)
	if err != nil {
		return err
	}
	if !exist {
		return errors.BadRequest(reason.QuestionNotFound)
	}
	err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, setStatus)
	if err != nil {
		return err
	}

	msg := &schema.NotificationMsg{}
	if setStatus == entity.QuestionStatusDeleted {
		// #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 = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount)
		// if err != nil {
		// 	log.Errorf("admin delete question then rank rollback error %s", err.Error())
		// }
		qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           questionInfo.UserID,
			TriggerUserID:    converter.StringToInt64(req.UserID),
			ObjectID:         questionInfo.ID,
			OriginalObjectID: questionInfo.ID,
			ActivityTypeKey:  constant.ActQuestionDeleted,
		})
		msg.NotificationAction = constant.NotificationYourQuestionWasDeleted
	}
	if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed {
		qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           questionInfo.UserID,
			TriggerUserID:    converter.StringToInt64(req.UserID),
			ObjectID:         questionInfo.ID,
			OriginalObjectID: questionInfo.ID,
			ActivityTypeKey:  constant.ActQuestionReopened,
		})
	}
	if setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed {
		qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           questionInfo.UserID,
			TriggerUserID:    converter.StringToInt64(req.UserID),
			ObjectID:         questionInfo.ID,
			OriginalObjectID: questionInfo.ID,
			ActivityTypeKey:  constant.ActQuestionClosed,
		})
		msg.NotificationAction = constant.NotificationYourQuestionIsClosed
	}
	// recover
	if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusDeleted {
		qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           req.UserID,
			TriggerUserID:    converter.StringToInt64(req.UserID),
			ObjectID:         questionInfo.ID,
			OriginalObjectID: questionInfo.ID,
			ActivityTypeKey:  constant.ActQuestionUndeleted,
		})
	}

	if len(msg.NotificationAction) > 0 {
		msg.ObjectID = questionInfo.ID
		msg.Type = schema.NotificationTypeInbox
		msg.ReceiverUserID = questionInfo.UserID
		msg.TriggerUserID = req.UserID
		msg.ObjectType = constant.QuestionObjectType
		qs.notificationQueueService.Send(ctx, msg)
	}
	switch setStatus {
	case entity.QuestionStatusDeleted:
		qs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionDelete, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: questionInfo.ID})
	case entity.QuestionStatusAvailable:
		qs.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: questionInfo.ID})
	}
	return nil
}

func (qs *QuestionService) AdminQuestionPage(
	ctx context.Context, req *schema.AdminQuestionPageReq) (
	resp *pager.PageModel, err error) {
	list := make([]*schema.AdminQuestionInfo, 0)
	questionList, count, err := qs.questionRepo.AdminQuestionPage(ctx, req)
	if err != nil {
		return nil, err
	}

	userIds := make([]string, 0)
	for _, info := range questionList {
		item := &schema.AdminQuestionInfo{}
		_ = copier.Copy(item, info)
		item.CreateTime = info.CreatedAt.Unix()
		item.UpdateTime = info.PostUpdateTime.Unix()
		item.EditTime = info.UpdatedAt.Unix()
		list = append(list, item)
		userIds = append(userIds, info.UserID)
	}
	userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
	if err != nil {
		return nil, err
	}
	for _, item := range list {
		if u, ok := userInfoMap[item.UserID]; ok {
			item.UserInfo = u
		}
	}
	return pager.NewPageModel(count, list), nil
}

// AdminAnswerPage search answer list
func (qs *QuestionService) AdminAnswerPage(ctx context.Context, req *schema.AdminAnswerPageReq) (
	resp *pager.PageModel, err error) {
	answerList, count, err := qs.questioncommon.AnswerCommon.AdminSearchList(ctx, req)
	if err != nil {
		return nil, err
	}

	questionIDs := make([]string, 0)
	userIds := make([]string, 0)
	answerResp := make([]*schema.AdminAnswerInfo, 0)
	for _, item := range answerList {
		answerInfo := qs.questioncommon.AnswerCommon.AdminShowFormat(ctx, item)
		answerResp = append(answerResp, answerInfo)
		questionIDs = append(questionIDs, item.QuestionID)
		userIds = append(userIds, item.UserID)
	}
	userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
	if err != nil {
		return nil, err
	}
	questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID)
	if err != nil {
		return nil, err
	}

	for _, item := range answerResp {
		if q, ok := questionMaps[item.QuestionID]; ok {
			item.QuestionInfo.Title = q.Title
		}
		if u, ok := userInfoMap[item.UserID]; ok {
			item.UserInfo = u
		}
	}
	return pager.NewPageModel(count, answerResp), nil
}

func (qs *QuestionService) changeQuestionToRevision(_ context.Context, questionInfo *entity.Question, tags []*entity.Tag) (
	questionRevision *entity.QuestionWithTagsRevision) {
	questionRevision = &entity.QuestionWithTagsRevision{}
	questionRevision.Question = *questionInfo

	for _, tag := range tags {
		item := &entity.TagSimpleInfoForRevision{}
		_ = copier.Copy(item, tag)
		questionRevision.Tags = append(questionRevision.Tags, item)
	}
	return questionRevision
}

func (qs *QuestionService) SitemapCron(ctx context.Context) {
	siteSeo, err := qs.siteInfoService.GetSiteSeo(ctx)
	if err != nil {
		log.Error(err)
		return
	}
	ctx = context.WithValue(ctx, constant.ShortIDContextKey, siteSeo.IsShortLink())
	qs.questioncommon.SitemapCron(ctx)
}

func (qs *QuestionService) GetQuestionLink(ctx context.Context, req *schema.GetQuestionLinkReq) (
	questions []*schema.QuestionPageResp, total int64, err error) {
	if req.OrderCond == schema.QuestionOrderCondHot {
		req.InDays = schema.HotInDays
	}

	questionList, total, err := qs.questionRepo.GetQuestionLink(ctx, req.Page, req.PageSize, req.QuestionID, req.OrderCond, req.InDays)
	if err != nil {
		return nil, 0, err
	}

	questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond)
	if err != nil {
		return nil, 0, err
	}
	return questions, total, nil
}
