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

import (
	"context"
	"encoding/json"
	"fmt"
	"math"
	"strings"
	"time"

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

	"github.com/apache/answer/internal/base/constant"
	"github.com/apache/answer/internal/base/data"
	"github.com/apache/answer/internal/base/handler"
	"github.com/apache/answer/internal/base/reason"
	"github.com/apache/answer/internal/service/activity_common"
	"github.com/apache/answer/internal/service/activity_queue"
	"github.com/apache/answer/internal/service/config"
	metacommon "github.com/apache/answer/internal/service/meta_common"
	"github.com/apache/answer/internal/service/revision"
	"github.com/apache/answer/pkg/checker"
	"github.com/apache/answer/pkg/htmltext"
	"github.com/apache/answer/pkg/uid"
	"github.com/segmentfault/pacman/errors"

	"github.com/apache/answer/internal/entity"
	"github.com/apache/answer/internal/schema"
	answercommon "github.com/apache/answer/internal/service/answer_common"
	collectioncommon "github.com/apache/answer/internal/service/collection_common"
	tagcommon "github.com/apache/answer/internal/service/tag_common"
	usercommon "github.com/apache/answer/internal/service/user_common"
	"github.com/segmentfault/pacman/log"
)

// QuestionRepo question repository
type QuestionRepo interface {
	AddQuestion(ctx context.Context, question *entity.Question) (err error)
	RemoveQuestion(ctx context.Context, id string) (err error)
	UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error)
	GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error)
	GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error)
	GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) (
		questionList []*entity.Question, total int64, err error)
	GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (questionList []*entity.Question, total int64, err error)
	UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error)
	UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error)
	DeletePermanentlyQuestions(ctx context.Context) (err error)
	RecoverQuestion(ctx context.Context, questionID string) (err error)
	UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error)
	GetQuestionsByTitle(ctx context.Context, title string, pageSize int) (questionList []*entity.Question, err error)
	UpdatePvCount(ctx context.Context, questionID string) (err error)
	UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error)
	UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error)
	UpdateAccepted(ctx context.Context, question *entity.Question) (err error)
	UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)
	FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
	AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error)
	GetQuestionCount(ctx context.Context) (count int64, err error)
	GetUnansweredQuestionCount(ctx context.Context) (count int64, err error)
	GetResolvedQuestionCount(ctx context.Context) (count int64, err error)
	GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error)
	SitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
	RemoveAllUserQuestion(ctx context.Context, userID string) (err error)
	UpdateSearch(ctx context.Context, questionID string) (err error)
	LinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error)
	GetLinkedQuestionIDs(ctx context.Context, questionID string, status int) (questionIDs []string, err error)
	UpdateQuestionLinkCount(ctx context.Context, questionID string) (err error)
	RemoveQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error)
	RecoverQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error)
	UpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error)
	GetQuestionLink(ctx context.Context, page, pageSize int, questionID string, orderCond string, inDays int) (questions []*entity.Question, total int64, err error)
}

// QuestionCommon user service
type QuestionCommon struct {
	questionRepo         QuestionRepo
	answerRepo           answercommon.AnswerRepo
	voteRepo             activity_common.VoteRepo
	followCommon         activity_common.FollowRepo
	tagCommon            *tagcommon.TagCommonService
	userCommon           *usercommon.UserCommon
	collectionCommon     *collectioncommon.CollectionCommon
	AnswerCommon         *answercommon.AnswerCommon
	metaCommonService    *metacommon.MetaCommonService
	configService        *config.ConfigService
	activityQueueService activity_queue.ActivityQueueService
	revisionRepo         revision.RevisionRepo
	siteInfoService      siteinfo_common.SiteInfoCommonService
	data                 *data.Data
}

func NewQuestionCommon(questionRepo QuestionRepo,
	answerRepo answercommon.AnswerRepo,
	voteRepo activity_common.VoteRepo,
	followCommon activity_common.FollowRepo,
	tagCommon *tagcommon.TagCommonService,
	userCommon *usercommon.UserCommon,
	collectionCommon *collectioncommon.CollectionCommon,
	answerCommon *answercommon.AnswerCommon,
	metaCommonService *metacommon.MetaCommonService,
	configService *config.ConfigService,
	activityQueueService activity_queue.ActivityQueueService,
	revisionRepo revision.RevisionRepo,
	siteInfoService siteinfo_common.SiteInfoCommonService,
	data *data.Data,
) *QuestionCommon {
	return &QuestionCommon{
		questionRepo:         questionRepo,
		answerRepo:           answerRepo,
		voteRepo:             voteRepo,
		followCommon:         followCommon,
		tagCommon:            tagCommon,
		userCommon:           userCommon,
		collectionCommon:     collectionCommon,
		AnswerCommon:         answerCommon,
		metaCommonService:    metaCommonService,
		configService:        configService,
		activityQueueService: activityQueueService,
		revisionRepo:         revisionRepo,
		siteInfoService:      siteInfoService,
		data:                 data,
	}
}

func (qs *QuestionCommon) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) {
	return qs.questionRepo.GetUserQuestionCount(ctx, userID, 0)
}

func (qs *QuestionCommon) GetPersonalUserQuestionCount(ctx context.Context, loginUserID, userID string, isAdmin bool) (count int64, err error) {
	show := entity.QuestionShow
	if loginUserID == userID || isAdmin {
		show = 0
	}
	return qs.questionRepo.GetUserQuestionCount(ctx, userID, show)
}

func (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error {
	return qs.questionRepo.UpdatePvCount(ctx, questionID)
}

func (qs *QuestionCommon) UpdateAnswerCount(ctx context.Context, questionID string) error {
	count, err := qs.answerRepo.GetCountByQuestionID(ctx, questionID)
	if err != nil {
		return err
	}
	if count == 0 {
		err = qs.questionRepo.UpdateLastAnswer(ctx, &entity.Question{
			ID:           questionID,
			LastAnswerID: "0",
		})
		if err != nil {
			return err
		}
	}
	return qs.questionRepo.UpdateAnswerCount(ctx, questionID, int(count))
}

func (qs *QuestionCommon) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) {
	return qs.questionRepo.UpdateCollectionCount(ctx, questionID)
}

func (qs *QuestionCommon) UpdateAccepted(ctx context.Context, questionID, AnswerID string) error {
	question := &entity.Question{}
	question.ID = questionID
	question.AcceptedAnswerID = AnswerID
	return qs.questionRepo.UpdateAccepted(ctx, question)
}

func (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionID, AnswerID string) error {
	question := &entity.Question{}
	question.ID = questionID
	question.LastAnswerID = AnswerID
	return qs.questionRepo.UpdateLastAnswer(ctx, question)
}

func (qs *QuestionCommon) UpdatePostTime(ctx context.Context, questionID string) error {
	questioninfo := &entity.Question{}
	now := time.Now()
	questioninfo.ID = questionID
	questioninfo.PostUpdateTime = now
	return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"})
}
func (qs *QuestionCommon) UpdatePostSetTime(ctx context.Context, questionID string, setTime time.Time) error {
	questioninfo := &entity.Question{}
	questioninfo.ID = questionID
	questioninfo.PostUpdateTime = setTime
	return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"})
}

func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfoResp, error) {
	list := make(map[string]*schema.QuestionInfoResp)
	questionList, err := qs.questionRepo.FindByID(ctx, questionIDs)
	if err != nil {
		return list, err
	}
	questions, err := qs.FormatQuestions(ctx, questionList, loginUserID)
	if err != nil {
		return list, err
	}
	for _, item := range questions {
		list[item.ID] = item
	}
	return list, nil
}

func (qs *QuestionCommon) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) {
	InviteUserInfo := make([]*schema.UserBasicInfo, 0)
	dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)
	if err != nil {
		return InviteUserInfo, err
	}
	if !has {
		return InviteUserInfo, errors.NotFound(reason.QuestionNotFound)
	}
	//InviteUser
	if dbinfo.InviteUserID != "" {
		InviteUserIDs := make([]string, 0)
		err := json.Unmarshal([]byte(dbinfo.InviteUserID), &InviteUserIDs)
		if err == nil {
			inviteUserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, InviteUserIDs)
			if err == nil {
				for _, userid := range InviteUserIDs {
					_, ok := inviteUserInfoMap[userid]
					if ok {
						InviteUserInfo = append(InviteUserInfo, inviteUserInfoMap[userid])
					}
				}
			}
		}
	}
	return InviteUserInfo, nil
}

func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (resp *schema.QuestionInfoResp, err error) {
	questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)
	if err != nil {
		return resp, err
	}
	questionInfo.ID = uid.DeShortID(questionInfo.ID)
	if !has {
		return resp, errors.NotFound(reason.QuestionNotFound)
	}
	resp = qs.ShowFormat(ctx, questionInfo)
	if resp.Status == entity.QuestionStatusClosed {
		metaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey)
		if err != nil {
			log.Error(err)
		} else {
			closeMsg := &schema.CloseQuestionMeta{}
			err = json.Unmarshal([]byte(metaInfo.Value), closeMsg)
			if err != nil {
				log.Error("json.Unmarshal CloseQuestionMeta error", err.Error())
			} else {
				cfg, err := qs.configService.GetConfigByID(ctx, closeMsg.CloseType)
				if err != nil {
					log.Error("json.Unmarshal QuestionCloseJson error", err.Error())
				} else {
					reasonItem := &schema.ReasonItem{}
					_ = json.Unmarshal(cfg.GetByteValue(), reasonItem)
					reasonItem.Translate(cfg.Key, handler.GetLangByCtx(ctx))
					operation := &schema.Operation{}
					operation.Type = reasonItem.Name
					operation.Description = reasonItem.Description
					operation.Msg = closeMsg.CloseMsg
					operation.Time = metaInfo.CreatedAt.Unix()
					operation.Level = schema.OperationLevelInfo
					resp.Operation = operation
				}
			}
		}
	}

	if resp.Status != entity.QuestionStatusDeleted {
		if resp.Tags, err = qs.tagCommon.GetObjectTag(ctx, questionID); err != nil {
			return resp, err
		}
	} else {
		revisionInfo, exist, err := qs.revisionRepo.GetLastRevisionByObjectID(ctx, questionID)
		if err != nil {
			log.Errorf("get revision error %s", err)
		}
		if exist {
			questionWithTagsRevision := &entity.QuestionWithTagsRevision{}
			if err = json.Unmarshal([]byte(revisionInfo.Content), questionWithTagsRevision); err != nil {
				log.Errorf("revision parsing error %s", err)
				return resp, nil
			}
			for _, tag := range questionWithTagsRevision.Tags {
				resp.Tags = append(resp.Tags, &schema.TagResp{
					ID:              tag.ID,
					SlugName:        tag.SlugName,
					DisplayName:     tag.DisplayName,
					MainTagSlugName: tag.MainTagSlugName,
					Recommend:       tag.Recommend,
					Reserved:        tag.Reserved,
				})
			}
		}
	}

	userIds := make([]string, 0)
	if checker.IsNotZeroString(questionInfo.UserID) {
		userIds = append(userIds, questionInfo.UserID)
	}
	if checker.IsNotZeroString(questionInfo.LastEditUserID) {
		userIds = append(userIds, questionInfo.LastEditUserID)
	}
	if checker.IsNotZeroString(resp.LastAnsweredUserID) {
		userIds = append(userIds, resp.LastAnsweredUserID)
	}
	userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
	if err != nil {
		return resp, err
	}
	resp.UserInfo = userInfoMap[questionInfo.UserID]
	resp.UpdateUserInfo = userInfoMap[questionInfo.LastEditUserID]
	resp.LastAnsweredUserInfo = userInfoMap[resp.LastAnsweredUserID]
	if len(loginUserID) == 0 {
		return resp, nil
	}

	resp.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionID, loginUserID)
	resp.IsFollowed, _ = qs.followCommon.IsFollowed(ctx, loginUserID, questionID)

	ids, err := qs.AnswerCommon.SearchAnswerIDs(ctx, loginUserID, questionInfo.ID)
	if err != nil {
		log.Error("AnswerFunc.SearchAnswerIDs", err)
	}
	resp.Answered = len(ids) > 0
	if resp.Answered {
		resp.FirstAnswerId = ids[0]
	}

	collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{questionInfo.ID})
	if err != nil {
		return nil, err
	}
	if len(collectedMap) > 0 {
		resp.Collected = true
	}
	return resp, nil
}

func (qs *QuestionCommon) FormatQuestionsPage(
	ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) (
	formattedQuestions []*schema.QuestionPageResp, err error) {
	formattedQuestions = make([]*schema.QuestionPageResp, 0)
	questionIDs := make([]string, 0)
	userIDs := make([]string, 0)
	for _, questionInfo := range questionList {
		t := &schema.QuestionPageResp{
			ID:               questionInfo.ID,
			CreatedAt:        questionInfo.CreatedAt.Unix(),
			Title:            questionInfo.Title,
			UrlTitle:         htmltext.UrlTitle(questionInfo.Title),
			Description:      htmltext.FetchExcerpt(questionInfo.ParsedText, "...", 240),
			Status:           questionInfo.Status,
			ViewCount:        questionInfo.ViewCount,
			UniqueViewCount:  questionInfo.UniqueViewCount,
			VoteCount:        questionInfo.VoteCount,
			AnswerCount:      questionInfo.AnswerCount,
			CollectionCount:  questionInfo.CollectionCount,
			FollowCount:      questionInfo.FollowCount,
			AcceptedAnswerID: questionInfo.AcceptedAnswerID,
			LastAnswerID:     questionInfo.LastAnswerID,
			Pin:              questionInfo.Pin,
			Show:             questionInfo.Show,
			Operator:         &schema.QuestionPageRespOperator{ID: questionInfo.UserID},
		}

		questionIDs = append(questionIDs, questionInfo.ID)
		userIDs = append(userIDs, questionInfo.UserID)
		haveEdited, haveAnswered := false, false
		if checker.IsNotZeroString(questionInfo.LastEditUserID) {
			haveEdited = true
			userIDs = append(userIDs, questionInfo.LastEditUserID)
		}
		if checker.IsNotZeroString(questionInfo.LastAnswerID) {
			haveAnswered = true

			answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, questionInfo.LastAnswerID)
			if err == nil && exist {
				if answerInfo.LastEditUserID != "0" {
					t.LastAnsweredUserID = answerInfo.LastEditUserID
				} else {
					t.LastAnsweredUserID = answerInfo.UserID
				}
				t.LastAnsweredAt = answerInfo.CreatedAt
				userIDs = append(userIDs, t.LastAnsweredUserID)
			}
		}

		// The default operation is to ask questions
		t.OperationType = schema.QuestionPageRespOperationTypeAsked
		t.OperatedAt = questionInfo.CreatedAt.Unix()
		t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID}

		// If the order is active, the last operation time is the last edit or answer time if it exists
		if orderCond == schema.QuestionOrderCondActive {
			if haveEdited {
				t.OperationType = schema.QuestionPageRespOperationTypeModified
				t.OperatedAt = questionInfo.UpdatedAt.Unix()
				t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID}
			}
			if haveAnswered {
				if t.LastAnsweredAt.Unix() > t.OperatedAt {
					t.OperationType = schema.QuestionPageRespOperationTypeAnswered
					t.OperatedAt = t.LastAnsweredAt.Unix()
					t.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID}
				}
			}
		}

		formattedQuestions = append(formattedQuestions, t)
	}

	tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, questionIDs)
	if err != nil {
		return formattedQuestions, err
	}
	userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
	if err != nil {
		return formattedQuestions, err
	}

	for _, item := range formattedQuestions {
		tags, ok := tagsMap[item.ID]
		if ok {
			item.Tags = tags
		} else {
			item.Tags = make([]*schema.TagResp, 0)
		}
		userInfo, ok := userInfoMap[item.Operator.ID]
		if ok {
			if userInfo != nil {
				item.Operator.DisplayName = userInfo.DisplayName
				item.Operator.Username = userInfo.Username
				item.Operator.Rank = userInfo.Rank
				item.Operator.Status = userInfo.Status
				item.Operator.Avatar = userInfo.Avatar
			}
		}
	}
	return formattedQuestions, nil
}

func (qs *QuestionCommon) FormatQuestions(ctx context.Context, questionList []*entity.Question, loginUserID string) ([]*schema.QuestionInfoResp, error) {
	list := make([]*schema.QuestionInfoResp, 0)
	objectIds := make([]string, 0)
	userIds := make([]string, 0)

	for _, questionInfo := range questionList {
		item := qs.ShowFormat(ctx, questionInfo)
		list = append(list, item)
		objectIds = append(objectIds, item.ID)
		userIds = append(userIds, item.UserID, item.LastEditUserID, item.LastAnsweredUserID)
	}
	tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, objectIds)
	if err != nil {
		return list, err
	}

	userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
	if err != nil {
		return list, err
	}

	for _, item := range list {
		item.Tags = tagsMap[item.ID]
		item.UserInfo = userInfoMap[item.UserID]
		item.UpdateUserInfo = userInfoMap[item.LastEditUserID]
		item.LastAnsweredUserInfo = userInfoMap[item.LastAnsweredUserID]
	}
	if loginUserID == "" {
		return list, nil
	}

	collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, objectIds)
	if err != nil {
		return nil, err
	}
	for _, item := range list {
		item.Collected = collectedMap[item.ID]
	}
	return list, nil
}

// RemoveQuestion delete question
func (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) {
	questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
	if err != nil {
		return err
	}
	if !has {
		return nil
	}

	if questionInfo.Status == entity.QuestionStatusDeleted {
		return nil
	}

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

	userQuestionCount, err := qs.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())
		}
	}

	return nil
}

func (qs *QuestionCommon) 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
	}
	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.metaCommonService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta))
	if err != nil {
		return err
	}

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

// RemoveAnswer delete answer
func (qs *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err error) {
	answerinfo, has, err := qs.answerRepo.GetByID(ctx, id)
	if err != nil {
		return err
	}
	if !has {
		return nil
	}

	// user add question count

	err = qs.UpdateAnswerCount(ctx, answerinfo.QuestionID)
	if err != nil {
		log.Error("UpdateAnswerCount error", err.Error())
	}
	userAnswerCount, err := qs.answerRepo.GetCountByUserID(ctx, answerinfo.UserID)
	if err != nil {
		log.Error("GetCountByUserID error", err.Error())
	}
	err = qs.userCommon.UpdateAnswerCount(ctx, answerinfo.UserID, int(userAnswerCount))
	if err != nil {
		log.Error("user UpdateAnswerCount error", err.Error())
	}

	return qs.answerRepo.RemoveAnswer(ctx, id)
}

func (qs *QuestionCommon) SitemapCron(ctx context.Context) {
	questionNum, err := qs.questionRepo.GetQuestionCount(ctx)
	if err != nil {
		log.Error(err)
		return
	}
	if questionNum <= constant.SitemapMaxSize {
		_, err = qs.questionRepo.SitemapQuestions(ctx, 1, int(questionNum))
		if err != nil {
			log.Errorf("get site map question error: %v", err)
		}
		return
	}

	totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize)))
	for i := 1; i <= totalPages; i++ {
		_, err = qs.questionRepo.SitemapQuestions(ctx, i, constant.SitemapMaxSize)
		if err != nil {
			log.Errorf("get site map question error: %v", err)
			return
		}
	}
}

func (qs *QuestionCommon) SetCache(ctx context.Context, cachekey string, info interface{}) error {
	infoStr, err := json.Marshal(info)
	if err != nil {
		return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
	}

	err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashboardCacheTime)
	if err != nil {
		return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
	}
	return nil
}

func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp {
	return qs.ShowFormat(ctx, data)
}

func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp {
	info := schema.QuestionInfoResp{}
	info.ID = data.ID
	if handler.GetEnableShortID(ctx) {
		info.ID = uid.EnShortID(data.ID)
	}
	info.Title = data.Title
	info.UrlTitle = htmltext.UrlTitle(data.Title)
	info.Content = data.OriginalText
	info.HTML = data.ParsedText
	info.ViewCount = data.ViewCount
	info.UniqueViewCount = data.UniqueViewCount
	info.VoteCount = data.VoteCount
	info.AnswerCount = data.AnswerCount
	info.CollectionCount = data.CollectionCount
	info.FollowCount = data.FollowCount
	info.AcceptedAnswerID = data.AcceptedAnswerID
	info.LastAnswerID = data.LastAnswerID
	info.CreateTime = data.CreatedAt.Unix()
	info.UpdateTime = data.UpdatedAt.Unix()
	info.PostUpdateTime = data.PostUpdateTime.Unix()
	if data.PostUpdateTime.Unix() < 1 {
		info.PostUpdateTime = 0
	}
	info.QuestionUpdateTime = data.UpdatedAt.Unix()
	if data.UpdatedAt.Unix() < 1 {
		info.QuestionUpdateTime = 0
	}
	info.Status = data.Status
	info.Pin = data.Pin
	info.Show = data.Show
	info.UserID = data.UserID
	info.LastEditUserID = data.LastEditUserID
	if data.LastAnswerID != "0" {
		answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, data.LastAnswerID)
		if err == nil && exist {
			if answerInfo.LastEditUserID != "0" {
				info.LastAnsweredUserID = answerInfo.LastEditUserID
			} else {
				info.LastAnsweredUserID = answerInfo.UserID
			}
		}

	}
	info.Tags = make([]*schema.TagResp, 0)
	return &info
}
func (qs *QuestionCommon) ShowFormatWithTag(ctx context.Context, data *entity.QuestionWithTagsRevision) *schema.QuestionInfoResp {
	info := qs.ShowFormat(ctx, &data.Question)
	Tags := make([]*schema.TagResp, 0)
	for _, tag := range data.Tags {
		item := &schema.TagResp{}
		item.SlugName = tag.SlugName
		item.DisplayName = tag.DisplayName
		item.Recommend = tag.Recommend
		item.Reserved = tag.Reserved
		Tags = append(Tags, item)
	}
	info.Tags = Tags
	return info
}

func (qs *QuestionCommon) UpdateQuestionLink(ctx context.Context, questionID, answerID, parsedText, originalText string) (string, error) {
	err := qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{
		FromQuestionID: uid.DeShortID(questionID),
		FromAnswerID:   uid.DeShortID(answerID),
	})
	if err != nil {
		return parsedText, err
	}
	// Update the number of question links that have been removed
	linkedQuestionIDs, err := qs.questionRepo.GetLinkedQuestionIDs(ctx, uid.DeShortID(questionID), entity.QuestionLinkStatusDeleted)
	if err != nil {
		log.Errorf("get linked question ids error %v", err)
	} else {
		for _, id := range linkedQuestionIDs {
			if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, id); err != nil {
				log.Errorf("update question link count error %v", err)
			}
		}
	}

	links := checker.GetQuestionLink(originalText)
	if len(links) == 0 {
		return parsedText, nil
	}

	// get answer ids and question ids
	answerIDs := make([]string, 0, len(links))
	questionIDs := make([]string, 0, len(links))
	for _, link := range links {
		if link.AnswerID != "" {
			answerIDs = append(answerIDs, link.AnswerID)
		}
		if link.QuestionID != "" {
			questionIDs = append(questionIDs, link.QuestionID)
		}
	}

	// get answer info and build cache
	answerInfoList, err := qs.answerRepo.GetByIDs(ctx, answerIDs...)
	if err != nil {
		return parsedText, err
	}
	answerCache := make(map[string]string, len(answerInfoList))
	for _, ans := range answerInfoList {
		answerID := uid.DeShortID(ans.ID)
		questionID := ans.QuestionID
		answerCache[answerID] = questionID
	}

	// get question info and build cache
	questionInfoList, err := qs.questionRepo.FindByID(ctx, questionIDs)
	if err != nil {
		return parsedText, err
	}
	questionCache := make(map[string]struct{}, len(questionInfoList))
	for _, q := range questionInfoList {
		questionID := uid.DeShortID(q.ID)
		questionCache[questionID] = struct{}{}
	}

	// process links and generate new QuestionLink
	validLinks := make([]*entity.QuestionLink, 0, len(links))
	for _, link := range links {
		linkQuestionID := uid.DeShortID(link.QuestionID)
		linkAnswerID := uid.DeShortID(link.AnswerID)
		// validate question id
		if _, exists := questionCache[linkQuestionID]; linkQuestionID != "0" && !exists {
			continue
		}

		// validate answer id
		if linkAnswerID != "0" {
			linkedQuestionID, exists := answerCache[linkAnswerID]
			if !exists {
				continue
			}
			// if question id is empty, get it from answer cache
			if link.QuestionID == "" {
				link.QuestionID = linkedQuestionID
			}
		}

		// build new link
		newLink := &entity.QuestionLink{
			FromQuestionID: uid.DeShortID(questionID),
			FromAnswerID:   uid.DeShortID(answerID),
			ToQuestionID:   uid.DeShortID(link.QuestionID),
			ToAnswerID:     uid.DeShortID(link.AnswerID),
		}
		// replace link in parsed text
		if link.QuestionID != "" {
			htmlLink := fmt.Sprintf("<a href=\"/questions/%s\">#%s</a>", link.QuestionID, link.QuestionID)
			parsedText = strings.ReplaceAll(parsedText, "#"+link.QuestionID, htmlLink)
		}
		if link.AnswerID != "" {
			linkedQuestionID := answerCache[linkAnswerID]
			htmlLink := fmt.Sprintf("<a href=\"/questions/%s/%s\">#%s</a>", linkedQuestionID, link.AnswerID, link.AnswerID)
			parsedText = strings.ReplaceAll(parsedText, "#"+link.AnswerID, htmlLink)
			newLink.ToQuestionID = uid.DeShortID(linkedQuestionID)
		}
		// avoid link to self
		if newLink.FromQuestionID != newLink.ToQuestionID {
			validLinks = append(validLinks, newLink)
		}
	}

	// add new links to repo
	if len(validLinks) > 0 {
		err = qs.questionRepo.LinkQuestion(ctx, validLinks...)
		if err != nil {
			return parsedText, err
		}
	}

	// update question linked count
	for _, link := range validLinks {
		if len(link.ToQuestionID) == 0 {
			continue
		}
		if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, link.ToQuestionID); err != nil {
			log.Errorf("update question link count error %v", err)
		}
	}

	return parsedText, nil
}

// AddQuestionLinkForCloseReason When the reason about close question is a question link, add the link to the question
func (qs *QuestionCommon) AddQuestionLinkForCloseReason(ctx context.Context,
	questionInfo *entity.Question, closeMsg string) {
	questionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsg)
	if len(questionID) == 0 {
		return
	}

	linkedQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, questionID)
	if err != nil {
		log.Errorf("get question error %s", err)
		return
	}
	if !exist {
		return
	}
	err = qs.questionRepo.LinkQuestion(ctx, &entity.QuestionLink{
		FromQuestionID: questionInfo.ID,
		ToQuestionID:   linkedQuestion.ID,
		Status:         entity.QuestionLinkStatusAvailable,
	})
	if err != nil {
		log.Errorf("link question error %s", err)
	}
}

func (qs *QuestionCommon) RemoveQuestionLinkForReopen(ctx context.Context, questionInfo *entity.Question) {
	questionInfo.ID = uid.DeShortID(questionInfo.ID)
	metaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey)
	if err != nil {
		return
	}

	closeMsgMeta := &schema.CloseQuestionMeta{}
	_ = json.Unmarshal([]byte(metaInfo.Value), closeMsgMeta)

	linkedQuestionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsgMeta.CloseMsg)
	if len(linkedQuestionID) == 0 {
		return
	}
	err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{
		FromQuestionID: questionInfo.ID,
		ToQuestionID:   linkedQuestionID,
	})
	if err != nil {
		log.Errorf("remove question link error %s", err)
	}
}

func (qs *QuestionCommon) tryToGetQuestionIDFromMsg(ctx context.Context, closeMsg string) (questionID string) {
	siteGeneral, err := qs.siteInfoService.GetSiteGeneral(ctx)
	if err != nil {
		log.Errorf("get site general error %s", err)
		return
	}
	if !strings.HasPrefix(closeMsg, siteGeneral.SiteUrl) {
		return
	}
	// get question id from url
	// the url may like: https://xxx.com/questions/D1401/xxx
	// the D1401 is question id
	questionID = strings.TrimPrefix(closeMsg, siteGeneral.SiteUrl)
	questionID = strings.TrimPrefix(questionID, "/questions/")
	t := strings.Split(questionID, "/")
	if len(t) < 1 {
		return ""
	}
	questionID = t[0]
	questionID = uid.DeShortID(questionID)
	return questionID
}
