/*
 * 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"
	"fmt"
	"strings"

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

	"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/translator"
	"github.com/apache/answer/internal/entity"
	"github.com/apache/answer/internal/service/activity_type"
	"github.com/apache/answer/internal/service/comment_common"
	"github.com/apache/answer/internal/service/config"
	"github.com/apache/answer/internal/service/object_info"
	"github.com/apache/answer/pkg/htmltext"
	"github.com/segmentfault/pacman/log"

	"github.com/apache/answer/internal/base/reason"
	"github.com/apache/answer/internal/schema"
	answercommon "github.com/apache/answer/internal/service/answer_common"
	questioncommon "github.com/apache/answer/internal/service/question_common"
	"github.com/segmentfault/pacman/errors"
)

// VoteRepo activity repository
type VoteRepo interface {
	Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
	CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
	GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error)
	ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) (
		voteList []*entity.Activity, total int64, err error)
}

// VoteService user service
type VoteService struct {
	voteRepo          VoteRepo
	configService     *config.ConfigService
	questionRepo      questioncommon.QuestionRepo
	answerRepo        answercommon.AnswerRepo
	commentCommonRepo comment_common.CommentCommonRepo
	objectService     *object_info.ObjService
	eventQueueService eventqueue.Service
}

func NewVoteService(
	voteRepo VoteRepo,
	configService *config.ConfigService,
	questionRepo questioncommon.QuestionRepo,
	answerRepo answercommon.AnswerRepo,
	commentCommonRepo comment_common.CommentCommonRepo,
	objectService *object_info.ObjService,
	eventQueueService eventqueue.Service,
) *VoteService {
	return &VoteService{
		voteRepo:          voteRepo,
		configService:     configService,
		questionRepo:      questionRepo,
		answerRepo:        answerRepo,
		commentCommonRepo: commentCommonRepo,
		objectService:     objectService,
		eventQueueService: eventQueueService,
	}
}

// VoteUp vote up
func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
	objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
	if err != nil {
		return nil, err
	}
	if objectInfo.IsDeleted() {
		return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted)
	}
	// make object id must be decoded
	objectInfo.ObjectID = req.ObjectID

	// check user is voting self or not
	if objectInfo.ObjectCreatorUserID == req.UserID {
		return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
	}

	voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)

	// vote operation
	if req.IsCancel {
		err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo)
	} else {
		// cancel vote down if exist
		voteOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
		err = vs.voteRepo.CancelVote(ctx, voteOperationInfo)
		if err != nil {
			return nil, err
		}
		err = vs.voteRepo.Vote(ctx, voteUpOperationInfo)
		if err != nil {
			return nil, err
		}
	}
	if err != nil {
		return nil, err
	}

	resp = &schema.VoteResp{}
	resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)
	if err != nil {
		log.Error(err)
	}
	resp.Votes = resp.UpVotes - resp.DownVotes
	if !req.IsCancel {
		resp.VoteStatus = constant.ActVoteUp
		vs.sendEvent(ctx, req, objectInfo, resp)
	}
	return resp, nil
}

// VoteDown vote down
func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
	objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
	if err != nil {
		return nil, err
	}
	if objectInfo.IsDeleted() {
		return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted)
	}
	// make object id must be decoded
	objectInfo.ObjectID = req.ObjectID

	// check user is voting self or not
	if objectInfo.ObjectCreatorUserID == req.UserID {
		return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
	}

	// vote operation
	voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
	if req.IsCancel {
		err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo)
		if err != nil {
			return nil, err
		}
	} else {
		// cancel vote up if exist
		err = vs.voteRepo.CancelVote(ctx, vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo))
		if err != nil {
			return nil, err
		}
		err = vs.voteRepo.Vote(ctx, voteDownOperationInfo)
		if err != nil {
			return nil, err
		}
	}

	resp = &schema.VoteResp{}
	resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)
	if err != nil {
		log.Error(err)
	}
	resp.Votes = resp.UpVotes - resp.DownVotes
	if !req.IsCancel {
		resp.VoteStatus = constant.ActVoteDown
		vs.sendEvent(ctx, req, objectInfo, resp)
	}
	return resp, nil
}

// ListUserVotes list user's votes
func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (resp *pager.PageModel, err error) {
	typeKeys := []string{
		activity_type.QuestionVoteUp,
		activity_type.QuestionVoteDown,
		activity_type.AnswerVoteUp,
		activity_type.AnswerVoteDown,
	}
	activityTypes := make([]int, 0)
	activityTypeMapping := make(map[int]string, 0)

	for _, typeKey := range typeKeys {
		cfg, err := vs.configService.GetConfigByKey(ctx, typeKey)
		if err != nil {
			continue
		}
		activityTypes = append(activityTypes, cfg.ID)
		activityTypeMapping[cfg.ID] = typeKey
	}

	voteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req.Page, req.PageSize, activityTypes)
	if err != nil {
		return nil, err
	}

	lang := handler.GetLangByCtx(ctx)

	votes := make([]*schema.GetVoteWithPageResp, 0)
	for _, voteInfo := range voteList {
		objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID)
		if err != nil {
			log.Error(err)
			continue
		}

		item := &schema.GetVoteWithPageResp{
			CreatedAt:  voteInfo.CreatedAt.Unix(),
			ObjectID:   objInfo.ObjectID,
			QuestionID: objInfo.QuestionID,
			AnswerID:   objInfo.AnswerID,
			ObjectType: objInfo.ObjectType,
			Title:      objInfo.Title,
			UrlTitle:   htmltext.UrlTitle(objInfo.Title),
			Content:    objInfo.Content,
		}
		item.VoteType = translator.Tr(lang,
			activity_type.ActivityTypeFlagMapping[activityTypeMapping[voteInfo.ActivityType]])
		if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
			item.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey)
		}
		votes = append(votes, item)
	}
	return pager.NewPageModel(total, votes), err
}

func (vs *VoteService) createVoteOperationInfo(ctx context.Context,
	userID string, voteUp bool, objectInfo *schema.SimpleObjectInfo) *schema.VoteOperationInfo {
	// warp vote operation
	voteOperationInfo := &schema.VoteOperationInfo{
		ObjectID:            objectInfo.ObjectID,
		ObjectType:          objectInfo.ObjectType,
		ObjectCreatorUserID: objectInfo.ObjectCreatorUserID,
		OperatingUserID:     userID,
		VoteUp:              voteUp,
		VoteDown:            !voteUp,
	}
	voteOperationInfo.Activities = vs.getActivities(ctx, voteOperationInfo)
	return voteOperationInfo
}

func (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperationInfo) (
	activities []*schema.VoteActivity) {
	activities = make([]*schema.VoteActivity, 0)

	var actions []string
	switch op.ObjectType {
	case constant.QuestionObjectType:
		if op.VoteUp {
			actions = []string{activity_type.QuestionVoteUp, activity_type.QuestionVotedUp}
		} else {
			actions = []string{activity_type.QuestionVoteDown, activity_type.QuestionVotedDown}
		}
	case constant.AnswerObjectType:
		if op.VoteUp {
			actions = []string{activity_type.AnswerVoteUp, activity_type.AnswerVotedUp}
		} else {
			actions = []string{activity_type.AnswerVoteDown, activity_type.AnswerVotedDown}
		}
	case constant.CommentObjectType:
		actions = []string{activity_type.CommentVoteUp}
	}

	for _, action := range actions {
		t := &schema.VoteActivity{}
		cfg, err := vs.configService.GetConfigByKey(ctx, action)
		if err != nil {
			log.Warnf("get config by key error: %v", err)
			continue
		}
		t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue()

		if strings.Contains(action, "voted") {
			t.ActivityUserID = op.ObjectCreatorUserID
			t.TriggerUserID = op.OperatingUserID
		} else {
			t.ActivityUserID = op.OperatingUserID
			t.TriggerUserID = "0"
		}
		activities = append(activities, t)
	}
	return activities
}

func (vs *VoteService) sendEvent(ctx context.Context,
	req *schema.VoteReq, objectInfo *schema.SimpleObjectInfo, resp *schema.VoteResp) {
	var event *schema.EventMsg
	switch objectInfo.ObjectType {
	case constant.QuestionObjectType:
		event = schema.NewEvent(constant.EventQuestionVote, req.UserID).TID(objectInfo.QuestionID).
			QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID)
	case constant.AnswerObjectType:
		event = schema.NewEvent(constant.EventAnswerVote, req.UserID).TID(objectInfo.AnswerID).
			AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID)
	case constant.CommentObjectType:
		event = schema.NewEvent(constant.EventCommentVote, req.UserID).TID(objectInfo.CommentID).
			CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID)
	default:
		return
	}
	event.AddExtra("vote_up_amount", fmt.Sprintf("%d", resp.UpVotes))
	event.AddExtra("vote_down_amount", fmt.Sprintf("%d", resp.DownVotes))
	vs.eventQueueService.Send(ctx, event)
}
