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

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

	"github.com/apache/answer/internal/service/activity_common"
	metacommon "github.com/apache/answer/internal/service/meta_common"

	"github.com/apache/answer/internal/base/constant"
	"github.com/apache/answer/internal/base/handler"
	"github.com/apache/answer/internal/base/reason"
	"github.com/apache/answer/internal/entity"
	"github.com/apache/answer/internal/schema"
	"github.com/apache/answer/internal/service/comment_common"
	"github.com/apache/answer/internal/service/config"
	"github.com/apache/answer/internal/service/object_info"
	"github.com/apache/answer/internal/service/revision_common"
	"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/obj"
	"github.com/apache/answer/pkg/uid"
	"github.com/segmentfault/pacman/errors"
	"github.com/segmentfault/pacman/log"
)

// ActivityRepo activity repository
type ActivityRepo interface {
	GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (activityList []*entity.Activity, err error)
}

// ActivityService activity service
type ActivityService struct {
	activityRepo          ActivityRepo
	userCommon            *usercommon.UserCommon
	activityCommonService *activity_common.ActivityCommon
	tagCommonService      *tag_common.TagCommonService
	objectInfoService     *object_info.ObjService
	commentCommonService  *comment_common.CommentCommonService
	revisionService       *revision_common.RevisionService
	metaService           *metacommon.MetaCommonService
	configService         *config.ConfigService
}

// NewActivityService new activity service
func NewActivityService(
	activityRepo ActivityRepo,
	userCommon *usercommon.UserCommon,
	activityCommonService *activity_common.ActivityCommon,
	tagCommonService *tag_common.TagCommonService,
	objectInfoService *object_info.ObjService,
	commentCommonService *comment_common.CommentCommonService,
	revisionService *revision_common.RevisionService,
	metaService *metacommon.MetaCommonService,
	configService *config.ConfigService,
) *ActivityService {
	return &ActivityService{
		objectInfoService:     objectInfoService,
		activityRepo:          activityRepo,
		userCommon:            userCommon,
		activityCommonService: activityCommonService,
		tagCommonService:      tagCommonService,
		commentCommonService:  commentCommonService,
		revisionService:       revisionService,
		metaService:           metaService,
		configService:         configService,
	}
}

// GetObjectTimeline get object timeline
func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.GetObjectTimelineReq) (
	resp *schema.GetObjectTimelineResp, err error) {
	if err = as.ensureTimelineObjectVisible(ctx, req.ObjectID, req.UserID, req.IsAdminModerator); err != nil {
		return nil, err
	}

	resp = &schema.GetObjectTimelineResp{
		ObjectInfo: &schema.ActObjectInfo{},
		Timeline:   make([]*schema.ActObjectTimeline, 0),
	}

	resp.ObjectInfo, err = as.getTimelineMainObjInfo(ctx, req.ObjectID)
	if err != nil {
		return nil, err
	}

	activityList, err := as.activityRepo.GetObjectAllActivity(ctx, req.ObjectID, req.ShowVote)
	if err != nil {
		return nil, err
	}
	for _, act := range activityList {
		item := &schema.ActObjectTimeline{
			ActivityID: act.ID,
			RevisionID: converter.IntToString(act.RevisionID),
			CreatedAt:  act.CreatedAt.Unix(),
			Cancelled:  act.Cancelled == entity.ActivityCancelled,
			ObjectID:   act.ObjectID,
			UserInfo:   &schema.UserBasicInfo{},
		}
		item.ObjectType, _ = obj.GetObjectTypeStrByObjectID(act.ObjectID)
		if item.Cancelled {
			item.CancelledAt = act.CancelledAt.Unix()
		}

		if item.ObjectType == constant.QuestionObjectType || item.ObjectType == constant.AnswerObjectType {
			if handler.GetEnableShortID(ctx) {
				item.ObjectID = uid.EnShortID(act.ObjectID)
			}
		}

		cfg, err := as.configService.GetConfigByID(ctx, act.ActivityType)
		if err != nil {
			log.Errorf("fail to get config by id: %d, err: %v, act id is: %s", act.ActivityType, err, act.ID)
		} else {
			// database save activity type is number, change to activity type string is like "question.asked".
			// so we need to cut the front part of '.', only need string like 'asked'
			_, item.ActivityType, _ = strings.Cut(cfg.Key, ".")
			// format activity type string to show
			if isHidden, formattedActivityType := formatActivity(item.ActivityType); isHidden {
				continue
			} else {
				item.ActivityType = formattedActivityType
			}
		}

		// if activity is down vote, only admin can see who does it.
		if item.ActivityType == constant.ActDownVote && !req.IsAdmin {
			item.UserInfo.Username = "N/A"
			item.UserInfo.DisplayName = "N/A"
		} else {
			if act.TriggerUserID > 0 {
				item.UserInfo.ID = fmt.Sprintf("%d", act.TriggerUserID)
			} else {
				item.UserInfo.ID = act.UserID
			}
		}

		item.Comment = as.getTimelineActivityComment(ctx, item.ObjectID, item.ObjectType, item.ActivityType, item.RevisionID)
		resp.Timeline = append(resp.Timeline, item)
	}
	as.formatTimelineUserInfo(ctx, resp.Timeline)
	return
}

func (as *ActivityService) getTimelineMainObjInfo(ctx context.Context, objectID string) (
	resp *schema.ActObjectInfo, err error) {
	resp = &schema.ActObjectInfo{}
	objInfo, err := as.objectInfoService.GetInfo(ctx, objectID)
	if err != nil {
		return nil, err
	}
	resp.Title = objInfo.Title
	if objInfo.ObjectType == constant.TagObjectType {
		tag, exist, _ := as.tagCommonService.GetTagByID(ctx, objInfo.TagID)
		if exist {
			resp.Title = tag.SlugName
			resp.MainTagSlugName = tag.MainTagSlugName
		}
	}
	resp.ObjectType = objInfo.ObjectType
	resp.QuestionID = objInfo.QuestionID
	resp.AnswerID = objInfo.AnswerID
	if len(objInfo.ObjectCreatorUserID) > 0 {
		// get object creator user info
		userBasicInfo, exist, err := as.userCommon.GetUserBasicInfoByID(ctx, objInfo.ObjectCreatorUserID)
		if err != nil {
			return nil, err
		}
		if exist {
			resp.Username = userBasicInfo.Username
			resp.DisplayName = userBasicInfo.DisplayName
		}
	}
	return resp, nil
}

func (as *ActivityService) getTimelineActivityComment(ctx context.Context, objectID, objectType,
	activityType, revisionID string) (comment string) {
	if objectType == constant.CommentObjectType {
		commentInfo, err := as.commentCommonService.GetComment(ctx, objectID)
		if err != nil {
			log.Error(err)
		} else {
			return commentInfo.ParsedText
		}
		return
	}

	if activityType == constant.ActEdited {
		revision, err := as.revisionService.GetRevision(ctx, revisionID)
		if err != nil {
			log.Error(err)
		} else {
			return converter.Markdown2HTML(revision.Log)
		}
		return
	}
	if activityType == constant.ActClosed {
		// only question can be closed
		metaInfo, err := as.metaService.GetMetaByObjectIdAndKey(ctx, objectID, entity.QuestionCloseReasonKey)
		if err != nil {
			log.Error(err)
		} else {
			closeMsg := &schema.CloseQuestionMeta{}
			if err := json.Unmarshal([]byte(metaInfo.Value), closeMsg); err == nil {
				return converter.Markdown2HTML(closeMsg.CloseMsg)
			}
		}
	}
	return ""
}

func (as *ActivityService) formatTimelineUserInfo(ctx context.Context, timeline []*schema.ActObjectTimeline) {
	userExist := make(map[string]bool)
	userIDs := make([]string, 0)
	for _, info := range timeline {
		if len(info.UserInfo.ID) == 0 || userExist[info.UserInfo.ID] {
			continue
		}
		userIDs = append(userIDs, info.UserInfo.ID)
	}
	if len(userIDs) == 0 {
		return
	}
	userInfoMapping, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
	if err != nil {
		log.Error(err)
		return
	}
	for _, info := range timeline {
		if len(info.UserInfo.ID) == 0 {
			continue
		}
		info.UserInfo = userInfoMapping[info.UserInfo.ID]
	}
}

// GetObjectTimelineDetail get object timeline
func (as *ActivityService) GetObjectTimelineDetail(ctx context.Context, req *schema.GetObjectTimelineDetailReq) (
	resp *schema.GetObjectTimelineDetailResp, err error) {
	if err = as.ensureTimelineRevisionVisible(ctx, req.NewRevisionID, req.UserID, req.IsAdminModerator); err != nil {
		return nil, err
	}
	if err = as.ensureTimelineRevisionVisible(ctx, req.OldRevisionID, req.UserID, req.IsAdminModerator); err != nil {
		return nil, err
	}

	resp = &schema.GetObjectTimelineDetailResp{}
	resp.OldRevision, _ = as.getOneObjectDetail(ctx, req.OldRevisionID)
	resp.NewRevision, _ = as.getOneObjectDetail(ctx, req.NewRevisionID)
	return resp, nil
}

func (as *ActivityService) ensureTimelineRevisionVisible(ctx context.Context, revisionID, userID string,
	isAdminModerator bool) error {
	if revisionID == "0" {
		return nil
	}
	revisionInfo, err := as.revisionService.GetRevision(ctx, revisionID)
	if err != nil {
		return err
	}
	return as.ensureTimelineObjectVisible(ctx, revisionInfo.ObjectID, userID, isAdminModerator)
}

func (as *ActivityService) ensureTimelineObjectVisible(ctx context.Context, objectID, userID string,
	isAdminModerator bool) error {
	objInfo, err := as.objectInfoService.GetInfo(ctx, objectID)
	if err != nil {
		return err
	}

	var parentQuestionInfo *schema.SimpleObjectInfo
	if objInfo.ObjectType != constant.QuestionObjectType && len(objInfo.QuestionID) > 0 && objInfo.QuestionID != "0" {
		parentQuestionInfo, err = as.objectInfoService.GetInfo(ctx, objInfo.QuestionID)
		if err != nil {
			return err
		}
	}

	return validateTimelineObjectVisibility(objInfo, parentQuestionInfo, userID, isAdminModerator)
}

func validateTimelineObjectVisibility(objInfo, parentQuestionInfo *schema.SimpleObjectInfo,
	userID string, isAdminModerator bool) error {
	if objInfo == nil {
		return errors.NotFound(reason.ObjectNotFound)
	}
	if isTimelineObjectRestricted(objInfo) &&
		!canViewRestrictedTimelineObject(objInfo.ObjectType, objInfo.ObjectCreatorUserID, userID, isAdminModerator) {
		return errors.NotFound(timelineNotFoundReasonByObjectType(objInfo.ObjectType))
	}
	if parentQuestionInfo != nil && isTimelineQuestionRestricted(parentQuestionInfo) &&
		!canViewRestrictedTimelineObject(parentQuestionInfo.ObjectType, parentQuestionInfo.ObjectCreatorUserID,
			userID, isAdminModerator) {
		return errors.NotFound(reason.QuestionNotFound)
	}
	return nil
}

func canViewRestrictedTimelineObject(objectType, creatorUserID, userID string, isAdminModerator bool) bool {
	if isAdminModerator {
		return true
	}
	switch objectType {
	case constant.QuestionObjectType, constant.AnswerObjectType, constant.CommentObjectType:
		return creatorUserID == userID
	default:
		return false
	}
}

func isTimelineObjectRestricted(objInfo *schema.SimpleObjectInfo) bool {
	switch objInfo.ObjectType {
	case constant.QuestionObjectType:
		return isTimelineQuestionRestricted(objInfo)
	case constant.AnswerObjectType:
		return objInfo.AnswerStatus == entity.AnswerStatusDeleted || objInfo.AnswerStatus == entity.AnswerStatusPending
	case constant.CommentObjectType:
		return objInfo.CommentStatus == entity.CommentStatusDeleted || objInfo.CommentStatus == entity.CommentStatusPending
	case constant.TagObjectType:
		return objInfo.TagStatus == entity.TagStatusDeleted
	default:
		return false
	}
}

func isTimelineQuestionRestricted(questionInfo *schema.SimpleObjectInfo) bool {
	return questionInfo.QuestionStatus == entity.QuestionStatusDeleted ||
		questionInfo.QuestionStatus == entity.QuestionStatusPending ||
		questionInfo.QuestionShow == entity.QuestionHide
}

func timelineNotFoundReasonByObjectType(objectType string) string {
	switch objectType {
	case constant.QuestionObjectType:
		return reason.QuestionNotFound
	case constant.AnswerObjectType:
		return reason.AnswerNotFound
	case constant.CommentObjectType:
		return reason.CommentNotFound
	case constant.TagObjectType:
		return reason.TagNotFound
	default:
		return reason.ObjectNotFound
	}
}

// getOneObjectDetail get object detail
func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID string) (
	resp *schema.ObjectTimelineDetail, err error) {
	resp = &schema.ObjectTimelineDetail{Tags: make([]*schema.ObjectTimelineTag, 0)}

	// if request revision is 0, return null object detail.
	if revisionID == "0" {
		return nil, nil
	}

	revision, err := as.revisionService.GetRevision(ctx, revisionID)
	if err != nil {
		log.Warn(err)
		return nil, nil
	}
	objInfo, err := as.objectInfoService.GetInfo(ctx, revision.ObjectID)
	if err != nil {
		return nil, err
	}

	switch objInfo.ObjectType {
	case constant.QuestionObjectType:
		data := &entity.QuestionWithTagsRevision{}
		if err = json.Unmarshal([]byte(revision.Content), data); err != nil {
			log.Errorf("revision parsing error %s", err)
			return resp, nil
		}
		for _, tag := range data.Tags {
			resp.Tags = append(resp.Tags, &schema.ObjectTimelineTag{
				SlugName:        tag.SlugName,
				DisplayName:     tag.DisplayName,
				MainTagSlugName: tag.MainTagSlugName,
				Recommend:       tag.Recommend,
				Reserved:        tag.Reserved,
			})
		}
		resp.Title = data.Title
		resp.OriginalText = data.OriginalText
	case constant.AnswerObjectType:
		data := &entity.Answer{}
		if err = json.Unmarshal([]byte(revision.Content), data); err != nil {
			log.Errorf("revision parsing error %s", err)
			return resp, nil
		}
		resp.Title = objInfo.Title // answer show question title
		resp.OriginalText = data.OriginalText
	case constant.TagObjectType:
		data := &entity.Tag{}
		if err = json.Unmarshal([]byte(revision.Content), data); err != nil {
			log.Errorf("revision parsing error %s", err)
			return resp, nil
		}
		resp.Title = data.DisplayName
		resp.OriginalText = data.OriginalText
		resp.SlugName = data.SlugName
		resp.MainTagSlugName = data.MainTagSlugName
	default:
		log.Errorf("unknown object type %s", objInfo.ObjectType)
	}
	return resp, nil
}

func formatActivity(activityType string) (isHidden bool, formattedActivityType string) {
	if activityType == constant.ActVotedUp ||
		activityType == constant.ActVotedDown ||
		activityType == constant.ActFollow {
		return true, ""
	}
	if activityType == constant.ActVoteUp {
		return false, constant.ActUpVote
	}
	if activityType == constant.ActVoteDown {
		return false, constant.ActDownVote
	}
	if activityType == constant.ActAccepted {
		return false, constant.ActAccept
	}
	return false, activityType
}
