/*
 * 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/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/entity"
	"github.com/apache/answer/internal/schema"
	"github.com/apache/answer/internal/service/activity"
	"github.com/apache/answer/internal/service/activityqueue"
	answercommon "github.com/apache/answer/internal/service/answer_common"
	"github.com/apache/answer/internal/service/noticequeue"
	"github.com/apache/answer/internal/service/object_info"
	questioncommon "github.com/apache/answer/internal/service/question_common"
	"github.com/apache/answer/internal/service/report_common"
	"github.com/apache/answer/internal/service/review"
	"github.com/apache/answer/internal/service/revision"
	"github.com/apache/answer/internal/service/tag_common"
	usercommon "github.com/apache/answer/internal/service/user_common"
	"github.com/apache/answer/pkg/converter"
	"github.com/apache/answer/pkg/htmltext"
	"github.com/apache/answer/pkg/obj"
	"github.com/apache/answer/pkg/uid"
	"github.com/jinzhu/copier"
	"github.com/segmentfault/pacman/errors"
	"github.com/segmentfault/pacman/log"
)

// RevisionService user service
type RevisionService struct {
	revisionRepo             revision.RevisionRepo
	userCommon               *usercommon.UserCommon
	questionCommon           *questioncommon.QuestionCommon
	answerService            *AnswerService
	objectInfoService        *object_info.ObjService
	questionRepo             questioncommon.QuestionRepo
	answerRepo               answercommon.AnswerRepo
	tagRepo                  tag_common.TagRepo
	tagCommon                *tag_common.TagCommonService
	notificationQueueService noticequeue.Service
	activityQueueService     activityqueue.Service
	reportRepo               report_common.ReportRepo
	reviewService            *review.ReviewService
	reviewActivity           activity.ReviewActivityRepo
}

func NewRevisionService(
	revisionRepo revision.RevisionRepo,
	userCommon *usercommon.UserCommon,
	questionCommon *questioncommon.QuestionCommon,
	answerService *AnswerService,
	objectInfoService *object_info.ObjService,
	questionRepo questioncommon.QuestionRepo,
	answerRepo answercommon.AnswerRepo,
	tagRepo tag_common.TagRepo,
	tagCommon *tag_common.TagCommonService,
	notificationQueueService noticequeue.Service,
	activityQueueService activityqueue.Service,
	reportRepo report_common.ReportRepo,
	reviewService *review.ReviewService,
	reviewActivity activity.ReviewActivityRepo,
) *RevisionService {
	return &RevisionService{
		revisionRepo:             revisionRepo,
		userCommon:               userCommon,
		questionCommon:           questionCommon,
		answerService:            answerService,
		objectInfoService:        objectInfoService,
		questionRepo:             questionRepo,
		answerRepo:               answerRepo,
		tagRepo:                  tagRepo,
		tagCommon:                tagCommon,
		notificationQueueService: notificationQueueService,
		activityQueueService:     activityQueueService,
		reportRepo:               reportRepo,
		reviewService:            reviewService,
		reviewActivity:           reviewActivity,
	}
}

func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.RevisionAuditReq) (err error) {
	revisioninfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, req.ID)
	if err != nil {
		return
	}
	if !exist {
		return
	}
	if revisioninfo.Status != entity.RevisionUnreviewedStatus {
		return
	}
	objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID)
	if objectTypeerr != nil {
		return objectTypeerr
	}
	if req.Operation == schema.RevisionAuditReject {
		if err = checkRevisionAuditPermission(req, objectType); err != nil {
			return err
		}
		err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewRejectStatus, req.UserID)
		return
	}
	if req.Operation == schema.RevisionAuditApprove {
		if err = checkRevisionAuditPermission(req, objectType); err != nil {
			return err
		}
		revisionitem := &schema.GetRevisionResp{}
		_ = copier.Copy(revisionitem, revisioninfo)
		rs.parseItem(ctx, revisionitem)
		var saveErr error
		switch objectType {
		case constant.QuestionObjectType:
			saveErr = rs.revisionAuditQuestion(ctx, revisionitem)
		case constant.AnswerObjectType:
			saveErr = rs.revisionAuditAnswer(ctx, revisionitem)
		case constant.TagObjectType:
			saveErr = rs.revisionAuditTag(ctx, revisionitem)
		}
		if saveErr != nil {
			return saveErr
		}
		err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewPassStatus, req.UserID)
		if err != nil {
			return err
		}
		err = rs.reviewActivity.Review(ctx, &schema.PassReviewActivity{
			UserID:           revisioninfo.UserID,
			TriggerUserID:    req.UserID,
			ObjectID:         revisioninfo.ObjectID,
			OriginalObjectID: "0",
			RevisionID:       revisioninfo.ID,
		})
		if err != nil {
			log.Errorf("add review activity failed: %v", err)
		}

		msg := &schema.NotificationMsg{
			TriggerUserID:  req.UserID,
			ReceiverUserID: revisioninfo.UserID,
			Type:           schema.NotificationTypeAchievement,
			ObjectID:       revisioninfo.ObjectID,
			ObjectType:     objectType,
		}
		rs.notificationQueueService.Send(ctx, msg)
		return
	}

	return nil
}

func checkRevisionAuditPermission(req *schema.RevisionAuditReq, objectType string) error {
	switch objectType {
	case constant.QuestionObjectType:
		if !req.CanReviewQuestion {
			return errors.BadRequest(reason.RevisionNoPermission)
		}
	case constant.AnswerObjectType:
		if !req.CanReviewAnswer {
			return errors.BadRequest(reason.RevisionNoPermission)
		}
	case constant.TagObjectType:
		if !req.CanReviewTag {
			return errors.BadRequest(reason.RevisionNoPermission)
		}
	}
	return nil
}

func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {
	questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfoResp)
	if ok {
		var PostUpdateTime time.Time
		dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, questioninfo.ID)
		if dberr != nil || !exist {
			return
		}

		PostUpdateTime = time.Unix(questioninfo.UpdateTime, 0)
		if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() {
			PostUpdateTime = dbquestion.PostUpdateTime
		}
		question := &entity.Question{}
		question.ID = questioninfo.ID
		question.Title = questioninfo.Title
		question.OriginalText = questioninfo.Content
		question.ParsedText = questioninfo.HTML
		question.UpdatedAt = time.Unix(questioninfo.UpdateTime, 0)
		question.PostUpdateTime = PostUpdateTime
		question.LastEditUserID = revisionitem.UserID
		saveerr := rs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"})
		if saveerr != nil {
			return saveerr
		}
		objectTagTags := make([]*schema.TagItem, 0)
		for _, tag := range questioninfo.Tags {
			item := &schema.TagItem{}
			item.SlugName = tag.SlugName
			objectTagTags = append(objectTagTags, item)
		}
		objectTagData := schema.TagChange{}
		objectTagData.ObjectID = question.ID
		objectTagData.Tags = objectTagTags
		minimumTags, err := rs.tagCommon.GetMinimumTags(ctx)
		if err != nil {
			return err
		}
		_, saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData, minimumTags)
		if saveerr != nil {
			return saveerr
		}
		rs.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           revisionitem.UserID,
			ObjectID:         revisionitem.ObjectID,
			ActivityTypeKey:  constant.ActQuestionEdited,
			RevisionID:       revisionitem.ID,
			OriginalObjectID: revisionitem.ObjectID,
		})
	}
	return nil
}

func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {
	answerinfo, ok := revisionitem.ContentParsed.(*schema.AnswerInfo)
	if ok {
		var PostUpdateTime time.Time
		dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID)
		if dberr != nil || !exist {
			return
		}

		PostUpdateTime = time.Unix(answerinfo.UpdateTime, 0)
		if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() {
			PostUpdateTime = dbquestion.PostUpdateTime
		}

		insertData := new(entity.Answer)
		insertData.ID = answerinfo.ID
		insertData.OriginalText = answerinfo.Content
		insertData.ParsedText = answerinfo.HTML
		insertData.UpdatedAt = time.Unix(answerinfo.UpdateTime, 0)
		insertData.LastEditUserID = revisionitem.UserID
		saveerr := rs.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"})
		if saveerr != nil {
			return saveerr
		}
		saveerr = rs.questionCommon.UpdatePostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime)
		if saveerr != nil {
			return saveerr
		}
		questionInfo, exist, err := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID)
		if err != nil {
			return err
		}
		if !exist {
			return errors.BadRequest(reason.QuestionNotFound)
		}
		msg := &schema.NotificationMsg{
			TriggerUserID:  revisionitem.UserID,
			ReceiverUserID: questionInfo.UserID,
			Type:           schema.NotificationTypeInbox,
			ObjectID:       answerinfo.ID,
		}
		msg.ObjectType = constant.AnswerObjectType
		msg.NotificationAction = constant.NotificationUpdateAnswer
		rs.notificationQueueService.Send(ctx, msg)

		rs.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           revisionitem.UserID,
			ObjectID:         insertData.ID,
			OriginalObjectID: insertData.ID,
			ActivityTypeKey:  constant.ActAnswerEdited,
			RevisionID:       revisionitem.ID,
		})
	}
	return nil
}

func (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {
	taginfo, ok := revisionitem.ContentParsed.(*schema.GetTagResp)
	if ok {
		tag := &entity.Tag{}
		tag.ID = taginfo.TagID
		tag.OriginalText = taginfo.OriginalText
		tag.ParsedText = taginfo.ParsedText
		saveerr := rs.tagRepo.UpdateTag(ctx, tag)
		if saveerr != nil {
			return saveerr
		}

		tagInfo, exist, err := rs.tagCommon.GetTagByID(ctx, taginfo.TagID)
		if err != nil {
			return err
		}
		if !exist {
			return errors.BadRequest(reason.TagNotFound)
		}
		if tagInfo.MainTagID == 0 && len(tagInfo.SlugName) > 0 {
			log.Debugf("tag %s update slug_name", tagInfo.SlugName)
			tagList, err := rs.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)})
			if err != nil {
				return err
			}
			updateTagSlugNames := make([]string, 0)
			for _, tag := range tagList {
				updateTagSlugNames = append(updateTagSlugNames, tag.SlugName)
			}
			err = rs.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName)
			if err != nil {
				return err
			}
		}

		rs.activityQueueService.Send(ctx, &schema.ActivityMsg{
			UserID:           revisionitem.UserID,
			ObjectID:         taginfo.TagID,
			OriginalObjectID: taginfo.TagID,
			ActivityTypeKey:  constant.ActTagEdited,
			RevisionID:       revisionitem.ID,
		})
	}
	return nil
}

// GetUnreviewedRevisionPage get unreviewed list
func (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *schema.RevisionSearch) (
	resp *pager.PageModel, err error) {
	revisionResp := make([]*schema.GetUnreviewedRevisionResp, 0)
	if len(req.GetCanReviewObjectTypes()) == 0 {
		return pager.NewPageModel(0, revisionResp), nil
	}
	revisionPage, total, err := rs.revisionRepo.GetUnreviewedRevisionPage(
		ctx, req.Page, 1, req.GetCanReviewObjectTypes())
	if err != nil {
		return nil, err
	}
	for _, rev := range revisionPage {
		item := &schema.GetUnreviewedRevisionResp{}
		_, ok := constant.ObjectTypeNumberMapping[rev.ObjectType]
		if !ok {
			continue
		}
		item.Type = constant.ObjectTypeNumberMapping[rev.ObjectType]
		info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, rev.ObjectID)
		if err != nil {
			return nil, err
		}
		item.Info = info
		revisionitem := &schema.GetRevisionResp{}
		_ = copier.Copy(revisionitem, rev)
		rs.parseItem(ctx, revisionitem)
		item.UnreviewedInfo = revisionitem

		// get user info
		userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, revisionitem.UserID)
		if e != nil {
			return nil, e
		}
		if exists {
			var uinfo schema.UserBasicInfo
			_ = copier.Copy(&uinfo, userInfo)
			item.UnreviewedInfo.UserInfo = uinfo
		}
		item.Info.UrlTitle = htmltext.UrlTitle(item.Info.Title)
		item.UnreviewedInfo.UrlTitle = htmltext.UrlTitle(item.UnreviewedInfo.Title)
		revisionResp = append(revisionResp, item)
	}
	return pager.NewPageModel(total, revisionResp), nil
}

// GetRevisionList get revision list all
func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) {
	var (
		rev  entity.Revision
		revs []entity.Revision
	)

	resp = []schema.GetRevisionResp{}
	objInfo, infoErr := rs.objectInfoService.GetInfo(ctx, req.ObjectID)
	if infoErr != nil {
		return nil, infoErr
	}
	if err := objInfo.CheckVisibility(req.UserID, req.IsAdmin); err != nil {
		return nil, err
	}

	_ = copier.Copy(&rev, req)

	revs, err = rs.revisionRepo.GetRevisionList(ctx, &rev)
	if err != nil {
		return
	}

	for _, r := range revs {
		var (
			uinfo schema.UserBasicInfo
			item  schema.GetRevisionResp
		)

		_ = copier.Copy(&item, r)
		rs.parseItem(ctx, &item)

		// get user info
		userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, item.UserID)
		if e != nil {
			return nil, e
		}
		if exists {
			err = copier.Copy(&uinfo, userInfo)
			item.UserInfo = uinfo
		}
		resp = append(resp, item)
	}
	return
}

func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisionResp) {
	var (
		err          error
		question     entity.QuestionWithTagsRevision
		questionInfo *schema.QuestionInfoResp
		answer       entity.Answer
		answerInfo   *schema.AnswerInfo
		tag          entity.Tag
		tagInfo      *schema.GetTagResp
	)

	shortID := handler.GetEnableShortID(ctx)
	if shortID {
		item.ObjectID = uid.EnShortID(item.ObjectID)
	}
	switch item.ObjectType {
	case constant.ObjectTypeStrMapping["question"]:
		err = json.Unmarshal([]byte(item.Content), &question)
		if err != nil {
			break
		}
		questionInfo = rs.questionCommon.ShowFormatWithTag(ctx, &question)
		if shortID {
			questionInfo.ID = uid.EnShortID(questionInfo.ID)
		}
		item.ContentParsed = questionInfo
	case constant.ObjectTypeStrMapping["answer"]:
		err = json.Unmarshal([]byte(item.Content), &answer)
		if err != nil {
			break
		}
		answerInfo = rs.answerService.ShowFormat(ctx, &answer)
		if shortID {
			answerInfo.ID = uid.EnShortID(answerInfo.ID)
			answerInfo.QuestionID = uid.EnShortID(answerInfo.QuestionID)
		}
		item.ContentParsed = answerInfo
	case constant.ObjectTypeStrMapping["tag"]:
		err = json.Unmarshal([]byte(item.Content), &tag)
		if err != nil {
			break
		}
		tagInfo = &schema.GetTagResp{
			TagID:         tag.ID,
			CreatedAt:     tag.CreatedAt.Unix(),
			UpdatedAt:     tag.UpdatedAt.Unix(),
			SlugName:      tag.SlugName,
			DisplayName:   tag.DisplayName,
			OriginalText:  tag.OriginalText,
			ParsedText:    tag.ParsedText,
			FollowCount:   tag.FollowCount,
			QuestionCount: tag.QuestionCount,
			Recommend:     tag.Recommend,
			Reserved:      tag.Reserved,
		}
		tagInfo.GetExcerpt()
		item.ContentParsed = tagInfo
	}

	if err != nil {
		item.ContentParsed = item.Content
	}
	item.CreatedAtParsed = item.CreatedAt.Unix()
}

// CheckCanUpdateRevision can check revision
func (rs *RevisionService) CheckCanUpdateRevision(ctx context.Context, req *schema.CheckCanQuestionUpdate) (
	resp *schema.ErrTypeData, err error) {
	_, exist, err := rs.revisionRepo.ExistUnreviewedByObjectID(ctx, req.ID)
	if err != nil {
		return nil, nil
	}
	if exist {
		return &schema.ErrTypeToast, errors.BadRequest(reason.RevisionReviewUnderway)
	}
	return nil, nil
}

// GetReviewingType get reviewing type
func (rs *RevisionService) GetReviewingType(ctx context.Context, req *schema.GetReviewingTypeReq) (resp []*schema.GetReviewingTypeResp, err error) {
	resp = make([]*schema.GetReviewingTypeResp, 0)

	// get queue amount
	if req.IsAdmin {
		reviewCount, err := rs.reviewService.GetReviewPendingCount(ctx)
		if err != nil {
			log.Errorf("get report count failed: %v", err)
		} else {
			resp = append(resp, &schema.GetReviewingTypeResp{
				Name:       string(constant.QueuedPost),
				Label:      translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewQueuedPostLabel),
				TodoAmount: reviewCount,
			})
		}
	}

	// get flag amount
	if req.IsAdmin {
		reportCount, err := rs.reportRepo.GetReportCount(ctx)
		if err != nil {
			log.Errorf("get report count failed: %v", err)
		} else {
			resp = append(resp, &schema.GetReviewingTypeResp{
				Name:       string(constant.FlaggedPost),
				Label:      translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewFlaggedPostLabel),
				TodoAmount: reportCount,
			})
		}
	}

	// get suggestion amount
	countUnreviewedRevision, err := rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes())
	if err != nil {
		log.Errorf("get unreviewed revision count failed: %v", err)
	} else {
		resp = append(resp, &schema.GetReviewingTypeResp{
			Name:       string(constant.SuggestedPostEdit),
			Label:      translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewSuggestedPostEditLabel),
			TodoAmount: countUnreviewedRevision,
		})
	}
	return resp, nil
}
