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

import (
	"context"
	"strings"

	"github.com/apache/answer/internal/base/handler"
	"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/siteinfo_common"
	"github.com/apache/answer/pkg/converter"
	"github.com/apache/answer/pkg/uid"
	"github.com/gin-gonic/gin"
	"github.com/segmentfault/pacman/errors"
	"github.com/segmentfault/pacman/log"
)

type BadgeRepo interface {
	GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error)
	GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error)

	ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error)
	ListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error)
	ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error)

	UpdateStatus(ctx context.Context, id string, status int8) (err error)
	UpdateAwardCount(ctx context.Context, badgeID string, awardCount int) (err error)
}

type BadgeService struct {
	badgeRepo             BadgeRepo
	badgeGroupRepo        BadgeGroupRepo
	badgeAwardRepo        BadgeAwardRepo
	badgeEventService     *BadgeEventService
	siteInfoCommonService siteinfo_common.SiteInfoCommonService
}

func NewBadgeService(
	badgeRepo BadgeRepo,
	badgeGroupRepo BadgeGroupRepo,
	badgeAwardRepo BadgeAwardRepo,
	badgeEventService *BadgeEventService,
	siteInfoCommonService siteinfo_common.SiteInfoCommonService,
) *BadgeService {
	return &BadgeService{
		badgeRepo:             badgeRepo,
		badgeGroupRepo:        badgeGroupRepo,
		badgeAwardRepo:        badgeAwardRepo,
		badgeEventService:     badgeEventService,
		siteInfoCommonService: siteInfoCommonService,
	}
}

// ListByGroup list all badges group by group
func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []*schema.GetBadgeListResp, err error) {
	var (
		groups       []*entity.BadgeGroup
		badges       []*entity.Badge
		earnedCounts []*entity.BadgeEarnedCount

		groupMap  = make(map[int64]string, 0)
		badgesMap = make(map[int64][]*schema.BadgeListInfo, 0)
	)
	resp = make([]*schema.GetBadgeListResp, 0)

	groups, err = b.badgeGroupRepo.ListGroups(ctx)
	if err != nil {
		return
	}
	badges, _, err = b.badgeRepo.ListActivated(ctx, 0, 0)
	if err != nil {
		return
	}

	if len(userID) > 0 {
		earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, userID)
		if err != nil {
			return
		}
	}

	for _, group := range groups {
		groupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name)
	}

	for _, badge := range badges {
		// check is earned
		var earned int64 = 0
		if len(earnedCounts) > 0 {
			for _, earnedCount := range earnedCounts {
				if badge.ID == earnedCount.BadgeID && earnedCount.EarnedCount > 0 {
					earned = earnedCount.EarnedCount
					break
				}
			}
		}

		badgesMap[badge.BadgeGroupID] = append(badgesMap[badge.BadgeGroupID], &schema.BadgeListInfo{
			ID:          uid.EnShortID(badge.ID),
			Name:        translator.Tr(handler.GetLangByCtx(ctx), badge.Name),
			Icon:        badge.Icon,
			AwardCount:  badge.AwardCount,
			EarnedCount: earned,
			Level:       badge.Level,
		})
	}

	for _, group := range groups {
		resp = append(resp, &schema.GetBadgeListResp{
			GroupName: translator.Tr(handler.GetLangByCtx(ctx), group.Name),
			Badges:    badgesMap[converter.StringToInt64(group.ID)],
		})
	}

	return
}

// ListPaged list all badges by page
func (b *BadgeService) ListPaged(ctx context.Context, req *schema.GetBadgeListPagedReq) (resp []*schema.GetBadgeListPagedResp, total int64, err error) {
	var (
		groups   []*entity.BadgeGroup
		badges   []*entity.Badge
		badge    *entity.Badge
		exists   bool
		groupMap = make(map[int64]string, 0)
	)

	total = 0

	if len(req.Query) > 0 {
		isID := strings.Index(req.Query, "badge:")
		if isID != 0 {
			badges, err = b.searchByName(ctx, req.Query)
			if err != nil {
				return
			}
			// paged result
			count := len(badges)
			total = int64(count)
			start := (req.Page - 1) * req.PageSize
			end := req.Page * req.PageSize
			if start >= count {
				start = count
				end = count
			}
			if end > count {
				end = count
			}
			badges = badges[start:end]
		} else {
			req.Query = strings.TrimSpace(strings.TrimLeft(req.Query, "badge:"))
			id := uid.DeShortID(req.Query)
			if len(id) == 0 {
				return
			}
			badge, exists, err = b.badgeRepo.GetByID(ctx, id)
			if err != nil || !exists {
				return
			}
			badges = append(badges, badge)
		}
	} else {
		switch req.Status {
		case schema.BadgeStatusActive:
			badges, total, err = b.badgeRepo.ListActivated(ctx, req.Page, req.PageSize)
		case schema.BadgeStatusInactive:
			badges, total, err = b.badgeRepo.ListInactivated(ctx, req.Page, req.PageSize)
		default:
			badges, total, err = b.badgeRepo.ListPaged(ctx, req.Page, req.PageSize)
		}
		if err != nil {
			return
		}
	}

	// find all group and build group map
	groups, err = b.badgeGroupRepo.ListGroups(ctx)
	if err != nil {
		return
	}
	for _, group := range groups {
		groupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name)
	}

	resp = make([]*schema.GetBadgeListPagedResp, len(badges))

	general, siteErr := b.siteInfoCommonService.GetSiteGeneral(ctx)
	baseURL := ""
	if siteErr == nil {
		baseURL = general.SiteUrl
	}

	for i, badge := range badges {
		resp[i] = &schema.GetBadgeListPagedResp{
			ID:          uid.EnShortID(badge.ID),
			Name:        translator.Tr(handler.GetLangByCtx(ctx), badge.Name),
			Description: translator.TrWithData(handler.GetLangByCtx(ctx), badge.Description, &schema.BadgeTplData{ProfileURL: baseURL + "/users/settings/profile"}),
			Icon:        badge.Icon,
			AwardCount:  badge.AwardCount,
			Level:       badge.Level,
			GroupName:   groupMap[badge.BadgeGroupID],
			Status:      schema.BadgeStatusMap[badge.Status],
		}
	}
	return
}

// searchByName
func (b *BadgeService) searchByName(ctx context.Context, name string) (result []*entity.Badge, err error) {
	var badges []*entity.Badge
	name = strings.ToLower(name)
	result = make([]*entity.Badge, 0)

	badges, _, err = b.badgeRepo.ListPaged(ctx, 0, 0)
	for _, badge := range badges {
		tn := strings.ToLower(translator.Tr(handler.GetLangByCtx(ctx), badge.Name))
		if strings.Contains(tn, name) {
			result = append(result, badge)
		}
	}
	return
}

// GetBadgeInfo get badge info
func (b *BadgeService) GetBadgeInfo(ctx *gin.Context, id string, userID string) (info *schema.GetBadgeInfoResp, err error) {
	badge, exists, err := b.badgeRepo.GetByID(ctx, id)
	if err != nil {
		return nil, err
	}

	if !exists || badge.Status == entity.BadgeStatusInactive {
		return nil, errors.BadRequest(reason.BadgeObjectNotFound)
	}

	var earnedTotal int64
	if len(userID) > 0 {
		earnedTotal = b.badgeAwardRepo.CountByUserIdAndBadgeId(ctx, userID, badge.ID)
	}

	baseURL := ""
	general, siteErr := b.siteInfoCommonService.GetSiteGeneral(ctx)
	if siteErr == nil {
		baseURL = general.SiteUrl
	}

	info = &schema.GetBadgeInfoResp{
		ID:          uid.EnShortID(badge.ID),
		Name:        translator.Tr(handler.GetLangByCtx(ctx), badge.Name),
		Description: translator.TrWithData(handler.GetLangByCtx(ctx), badge.Description, &schema.BadgeTplData{ProfileURL: baseURL + "/users/settings/profile"}),
		Icon:        badge.Icon,
		AwardCount:  badge.AwardCount,
		EarnedCount: earnedTotal,
		IsSingle:    badge.Single == entity.BadgeSingleAward,
		Level:       badge.Level,
	}
	return
}

// UpdateStatus update badge status
func (b *BadgeService) UpdateStatus(ctx *gin.Context, req *schema.UpdateBadgeStatusReq) (err error) {
	req.ID = uid.DeShortID(req.ID)

	badge, exists, err := b.badgeRepo.GetByID(ctx, req.ID)
	if err != nil {
		return err
	}
	if !exists {
		return errors.BadRequest(reason.BadgeObjectNotFound)
	}

	// check duplicate action
	status, ok := schema.BadgeStatusEMap[req.Status]
	if !ok {
		err = errors.BadRequest(reason.StatusInvalid)
		return
	}
	if badge.Status == status {
		return
	}

	err = b.badgeRepo.UpdateStatus(ctx, req.ID, status)
	if err != nil {
		return err
	}

	if status == entity.BadgeStatusActive {
		count, err := b.badgeAwardRepo.CountByBadgeID(ctx, badge.ID)
		if err != nil {
			log.Errorf("count badge award failed: %v", err)
			return nil
		}
		err = b.badgeRepo.UpdateAwardCount(ctx, badge.ID, int(count))
		if err != nil {
			log.Errorf("update badge award count failed: %v", err)
			return nil
		}
	}
	return nil
}
