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

import (
	"context"
	"fmt"
	"time"

	"github.com/apache/answer/internal/base/translator"
	"github.com/apache/answer/internal/service/siteinfo_common"
	"github.com/apache/answer/internal/service/user_external_login"
	"github.com/apache/answer/pkg/display"

	"github.com/apache/answer/internal/base/constant"
	"github.com/apache/answer/internal/base/data"
	"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/activity_common"
	"github.com/apache/answer/internal/service/noticequeue"
	"github.com/apache/answer/internal/service/object_info"
	usercommon "github.com/apache/answer/internal/service/user_common"
	"github.com/apache/answer/pkg/uid"
	"github.com/apache/answer/plugin"
	"github.com/goccy/go-json"
	"github.com/jinzhu/copier"
	"github.com/segmentfault/pacman/errors"
	"github.com/segmentfault/pacman/log"
)

type NotificationRepo interface {
	AddNotification(ctx context.Context, notification *entity.Notification) (err error)
	GetNotificationPage(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error)
	ClearUnRead(ctx context.Context, userID string, notificationType int) (err error)
	ClearIDUnRead(ctx context.Context, userID string, id string) (err error)
	GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error)
	UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error)
	GetById(ctx context.Context, id string) (*entity.Notification, bool, error)
	CountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error)
	DeleteNotification(ctx context.Context, userID string) (err error)
	DeleteUserNotificationConfig(ctx context.Context, userID string) (err error)
}

type NotificationCommon struct {
	data                     *data.Data
	notificationRepo         NotificationRepo
	activityRepo             activity_common.ActivityRepo
	followRepo               activity_common.FollowRepo
	userCommon               *usercommon.UserCommon
	objectInfoService        *object_info.ObjService
	notificationQueueService noticequeue.Service
	userExternalLoginRepo    user_external_login.UserExternalLoginRepo
	siteInfoService          siteinfo_common.SiteInfoCommonService
}

func NewNotificationCommon(
	data *data.Data,
	notificationRepo NotificationRepo,
	userCommon *usercommon.UserCommon,
	activityRepo activity_common.ActivityRepo,
	followRepo activity_common.FollowRepo,
	objectInfoService *object_info.ObjService,
	notificationQueueService noticequeue.Service,
	userExternalLoginRepo user_external_login.UserExternalLoginRepo,
	siteInfoService siteinfo_common.SiteInfoCommonService,
) *NotificationCommon {
	notification := &NotificationCommon{
		data:                     data,
		notificationRepo:         notificationRepo,
		activityRepo:             activityRepo,
		followRepo:               followRepo,
		userCommon:               userCommon,
		objectInfoService:        objectInfoService,
		notificationQueueService: notificationQueueService,
		userExternalLoginRepo:    userExternalLoginRepo,
		siteInfoService:          siteInfoService,
	}
	notificationQueueService.RegisterHandler(notification.AddNotification)
	return notification
}

// AddNotification
// need set
// LoginUserID
// Type  1 inbox 2 achievement
// [inbox] Activity
// [achievement] Rank
// ObjectInfo.Title
// ObjectInfo.ObjectID
// ObjectInfo.ObjectType
func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) (err error) {
	if msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() {
		return nil
	}
	req := &schema.NotificationContent{
		TriggerUserID:  msg.TriggerUserID,
		ReceiverUserID: msg.ReceiverUserID,
		ObjectInfo: schema.ObjectInfo{
			Title:      msg.Title,
			ObjectID:   uid.DeShortID(msg.ObjectID),
			ObjectType: msg.ObjectType,
		},
		NotificationAction: msg.NotificationAction,
		Type:               msg.Type,
	}
	var questionID string // just for notify all followers
	var objInfo *schema.SimpleObjectInfo
	if msg.ObjectType == constant.BadgeAwardObjectType {
		req.ObjectInfo.Title = msg.Title
		objectMap := make(map[string]string)
		objectMap["badge_id"] = msg.ExtraInfo["badge_id"]
		req.ObjectInfo.ObjectMap = objectMap
	} else {
		objInfo, err = ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID)
		if err != nil {
			log.Error(err)
			return err
		} else {
			req.ObjectInfo.Title = objInfo.Title
			questionID = objInfo.QuestionID
			objectMap := make(map[string]string)
			objectMap["question"] = uid.DeShortID(objInfo.QuestionID)
			objectMap["answer"] = uid.DeShortID(objInfo.AnswerID)
			objectMap["comment"] = objInfo.CommentID
			req.ObjectInfo.ObjectMap = objectMap
		}
	}

	if msg.Type == schema.NotificationTypeAchievement {
		notificationInfo, exist, err := ns.notificationRepo.GetByUserIdObjectIdTypeId(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID, req.Type)
		if err != nil {
			return fmt.Errorf("get by user id object id type id error: %w", err)
		}
		rank, err := ns.activityRepo.GetUserIDObjectIDActivitySum(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID)
		if err != nil {
			return fmt.Errorf("get user id object id activity sum error: %w", err)
		}
		req.Rank = rank
		if exist {
			// modify notification
			updateContent := &schema.NotificationContent{}
			err := json.Unmarshal([]byte(notificationInfo.Content), updateContent)
			if err != nil {
				return fmt.Errorf("unmarshal notification content error: %w", err)
			}
			updateContent.Rank = rank
			content, _ := json.Marshal(updateContent)
			notificationInfo.Content = string(content)
			err = ns.notificationRepo.UpdateNotificationContent(ctx, notificationInfo)
			if err != nil {
				return fmt.Errorf("update notification content error: %w", err)
			}
			return nil
		}
	}

	info := &entity.Notification{}
	now := time.Now()
	info.UserID = req.ReceiverUserID
	info.Type = req.Type
	info.IsRead = schema.NotificationNotRead
	info.Status = schema.NotificationStatusNormal
	info.CreatedAt = now
	info.UpdatedAt = now
	info.ObjectID = req.ObjectInfo.ObjectID

	userBasicInfo, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, req.TriggerUserID)
	if err != nil {
		return fmt.Errorf("get user basic info error: %w", err)
	}
	if !exist {
		return fmt.Errorf("user not exist: %s", req.TriggerUserID)
	}
	req.UserInfo = userBasicInfo
	content, _ := json.Marshal(req)
	_, ok := constant.NotificationMsgTypeMapping[req.NotificationAction]
	if ok {
		info.MsgType = constant.NotificationMsgTypeMapping[req.NotificationAction]
	}
	info.Content = string(content)
	err = ns.notificationRepo.AddNotification(ctx, info)
	if err != nil {
		return fmt.Errorf("add notification error: %w", err)
	}
	err = ns.addRedDot(ctx, info.UserID, msg.Type)
	if err != nil {
		log.Error("addRedDot Error", err.Error())
	}
	if req.ObjectInfo.ObjectType == constant.BadgeAwardObjectType {
		err = ns.AddBadgeAwardAlertCache(ctx, info.UserID, info.ID, req.ObjectInfo.ObjectMap["badge_id"])
		if err != nil {
			log.Error("AddBadgeAwardAlertCache Error", err.Error())
		}
	}

	go ns.SendNotificationToAllFollower(ctx, msg, questionID)

	if msg.Type == schema.NotificationTypeInbox {
		ns.syncNotificationToPlugin(ctx, objInfo, msg)
	}
	return nil
}

func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, noticeType int) error {
	var key string
	if noticeType == schema.NotificationTypeInbox {
		key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID)
	} else {
		key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID)
	}
	_, exist, err := ns.data.Cache.GetInt64(ctx, key)
	if err != nil {
		return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
	}
	if exist {
		if _, err := ns.data.Cache.Increase(ctx, key, 1); err != nil {
			return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
		}
		return nil
	}
	err = ns.data.Cache.SetInt64(ctx, key, 1, constant.RedDotCacheTime)
	if err != nil {
		return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
	}
	return nil
}

func (ns *NotificationCommon) DecreaseRedDot(ctx context.Context, userID string, notificationType int) error {
	var key string
	if notificationType == schema.NotificationTypeInbox {
		key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID)
	} else {
		key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID)
	}
	_, exist, err := ns.data.Cache.GetInt64(ctx, key)
	if err != nil {
		return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
	}
	if !exist {
		return nil
	}
	res, err := ns.data.Cache.Decrease(ctx, key, 1)
	if err != nil {
		return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
	}
	if res <= 0 {
		return ns.DeleteRedDot(ctx, userID, notificationType)
	}
	return nil
}

func (ns *NotificationCommon) DeleteRedDot(ctx context.Context, userID string, notificationType int) error {
	var key string
	if notificationType == schema.NotificationTypeInbox {
		key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID)
	} else {
		key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID)
	}
	err := ns.data.Cache.Del(ctx, key)
	if err != nil {
		return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
	}
	return nil
}

// AddBadgeAwardAlertCache add badge award alert cache
func (ns *NotificationCommon) AddBadgeAwardAlertCache(ctx context.Context, userID, notificationID, badgeID string) (err error) {
	key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID)
	cacheData, exist, err := ns.data.Cache.GetString(ctx, key)
	if err != nil {
		return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
	}
	if !exist {
		c := schema.NewRedDotBadgeAwardCache()
		c.AddBadgeAward(&schema.RedDotBadgeAward{
			NotificationID: notificationID,
			BadgeID:        badgeID,
		})
		return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime)
	}
	c := schema.NewRedDotBadgeAwardCache()
	c.FromJSON(cacheData)
	c.AddBadgeAward(&schema.RedDotBadgeAward{
		NotificationID: notificationID,
		BadgeID:        badgeID,
	})
	return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime)
}

// RemoveBadgeAwardAlertCache remove badge award alert cache
func (ns *NotificationCommon) RemoveBadgeAwardAlertCache(ctx context.Context, userID, notificationID string) (err error) {
	key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID)
	cacheData, exist, err := ns.data.Cache.GetString(ctx, key)
	if err != nil {
		return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
	}
	if !exist {
		return nil
	}
	c := schema.NewRedDotBadgeAwardCache()
	c.FromJSON(cacheData)
	c.RemoveBadgeAward(notificationID)
	if len(c.BadgeAwardList) == 0 {
		return ns.data.Cache.Del(ctx, key)
	}
	return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime)
}

// SendNotificationToAllFollower send notification to all followers
func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context, msg *schema.NotificationMsg,
	questionID string) {
	if msg.NoNeedPushAllFollow || len(questionID) == 0 {
		return
	}
	if msg.NotificationAction != constant.NotificationUpdateQuestion &&
		msg.NotificationAction != constant.NotificationAnswerTheQuestion &&
		msg.NotificationAction != constant.NotificationUpdateAnswer &&
		msg.NotificationAction != constant.NotificationAcceptAnswer {
		return
	}
	condObjectID := msg.ObjectID
	if len(questionID) > 0 {
		condObjectID = uid.DeShortID(questionID)
	}
	userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, condObjectID)
	if err != nil {
		log.Error(err)
		return
	}
	log.Infof("send notification to all followers: %s %d", condObjectID, len(userIDs))
	for _, userID := range userIDs {
		t := &schema.NotificationMsg{}
		_ = copier.Copy(t, msg)
		t.ReceiverUserID = userID
		t.TriggerUserID = msg.TriggerUserID
		t.NoNeedPushAllFollow = true
		ns.notificationQueueService.Send(ctx, t)
	}
}

func (ns *NotificationCommon) syncNotificationToPlugin(ctx context.Context, objInfo *schema.SimpleObjectInfo,
	msg *schema.NotificationMsg) {
	if objInfo == nil {
		return
	}
	siteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx)
	if err != nil {
		log.Errorf("get site general info failed: %v", err)
		return
	}
	seoInfo, err := ns.siteInfoService.GetSiteSeo(ctx)
	if err != nil {
		log.Errorf("get site seo info failed: %v", err)
		return
	}
	interfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx)
	if err != nil {
		log.Errorf("get site interface info failed: %v", err)
		return
	}

	objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)
	objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)
	pluginNotificationMsg := plugin.NotificationMessage{
		Type:           plugin.NotificationType(msg.NotificationAction),
		ReceiverUserID: msg.ReceiverUserID,
		TriggerUserID:  msg.TriggerUserID,
		QuestionTitle:  objInfo.Title,
	}

	if len(objInfo.QuestionID) > 0 {
		pluginNotificationMsg.QuestionUrl =
			display.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title)
	}
	if len(objInfo.AnswerID) > 0 {
		pluginNotificationMsg.AnswerUrl =
			display.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID)
	}
	if len(objInfo.CommentID) > 0 {
		pluginNotificationMsg.CommentUrl =
			display.CommentURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, objInfo.CommentID)
	}

	if len(msg.TriggerUserID) > 0 {
		triggerUser, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, msg.TriggerUserID)
		if err != nil {
			log.Errorf("get trigger user basic info failed: %v", err)
			return
		}
		if exist {
			pluginNotificationMsg.TriggerUserID = triggerUser.ID
			pluginNotificationMsg.TriggerUserDisplayName = triggerUser.DisplayName
			pluginNotificationMsg.TriggerUserUrl = display.UserURL(siteInfo.SiteUrl, triggerUser.Username)
		}
	}

	if len(pluginNotificationMsg.ReceiverLang) == 0 && len(msg.ReceiverUserID) > 0 {
		userInfo, _, _ := ns.userCommon.GetUserBasicInfoByID(ctx, msg.ReceiverUserID)
		if userInfo != nil {
			pluginNotificationMsg.ReceiverLang = userInfo.Language
		}
		// If receiver not set language, use site default language.
		if len(pluginNotificationMsg.ReceiverLang) == 0 || pluginNotificationMsg.ReceiverLang == translator.DefaultLangOption {
			pluginNotificationMsg.ReceiverLang = interfaceInfo.Language
		}
	}

	externalLogins, err := ns.userExternalLoginRepo.GetUserExternalLoginList(ctx, msg.ReceiverUserID)
	if err != nil {
		log.Errorf("get user external login list failed for user %s: %v", msg.ReceiverUserID, err)
	} else if len(externalLogins) > 0 {
		pluginNotificationMsg.ReceiverExternalID = externalLogins[0].ExternalID
	}

	_ = plugin.CallNotification(func(fn plugin.Notification) error {
		userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, msg.ReceiverUserID)
		if err != nil {
			log.Errorf("get user external login info failed: %v", err)
			return nil
		}
		if exist {
			pluginNotificationMsg.ReceiverExternalID = userInfo.ExternalID
		}
		fn.Notify(pluginNotificationMsg)
		return nil
	})
}
