// Package rewardnotice turns committed wallet reward facts into one idempotent activity inbox message.
package rewardnotice

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

	"google.golang.org/grpc"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	messagedomain "hyapp/services/activity-service/internal/domain/message"
	messageservice "hyapp/services/activity-service/internal/service/message"
)

const (
	rewardTypeResource = "resource"
	rewardTypeCoin     = "coin"
	millisecondsPerDay = int64(24 * 60 * 60 * 1000)
)

// Activity 固定活动英文名和稳定代码，避免各发奖链路自行拼出不同文案或幂等键。
type Activity struct {
	Code string
	Name string
}

var (
	CumulativeRecharge = Activity{Code: "cumulative_recharge", Name: "Cumulative Recharge Reward"}
	WeeklyStar         = Activity{Code: "weekly_star", Name: "Weekly Star Reward"}
	RoomRocket         = Activity{Code: "room_rocket", Name: "Room Rocket Reward"}
	SevenDayCheckIn    = Activity{Code: "seven_day_checkin", Name: "Seven-Day Check-In Reward"}
	FirstRecharge      = Activity{Code: "first_recharge", Name: "First Recharge Gift"}
	Registration       = Activity{Code: "registration", Name: "Registration Reward"}
	DailyTask          = Activity{Code: "daily_task", Name: "Daily Task Reward"}
	CPRanking          = Activity{Code: "cp_ranking", Name: "CP Ranking Reward"}
	RoomTurnover       = Activity{Code: "room_turnover", Name: "Room Turnover Reward"}
)

// Command 只接受钱包已成功返回的 grant/coin 事实；调用方不能用活动配置伪造实际到账内容。
type Command struct {
	Activity        Activity
	TargetUserID    int64
	ProducerEventID string
	AggregateType   string
	AggregateID     string
	Grants          []*walletv1.ResourceGrant
	GrantIDs        []string
	CoinAmount      int64
	SentAtMS        int64
	Metadata        map[string]any
}

// Notifier 是九类活动共享的最小依赖面，便于业务服务和 MQ 消费者统一接入。
type Notifier interface {
	NotifyReward(ctx context.Context, command Command) error
}

// ActivityNoticeWriter keeps reward formatting separate from the inbox owner.
type ActivityNoticeWriter interface {
	CreateActivityNotice(ctx context.Context, cmd messageservice.NoticeCommand) (messageservice.NoticeResult, error)
}

// ResourceGrantReader 供异步房间火箭事件按精确 grant_id 补取钱包发放快照。
type ResourceGrantReader interface {
	GetResourceGrant(ctx context.Context, req *walletv1.GetResourceGrantRequest, opts ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error)
}

// Service owns reward snapshot conversion and activity-message idempotency.
type Service struct {
	notices ActivityNoticeWriter
	wallet  ResourceGrantReader
}

func New(notices ActivityNoticeWriter, wallet ResourceGrantReader) *Service {
	return &Service{notices: notices, wallet: wallet}
}

// NotifyReward creates exactly one activity message for one business reward event.
// 钱包成功、消息失败时调用方保留 pending/failed 状态并重试；稳定 producer_event_id 会让重试只补齐一次消息。
func (s *Service) NotifyReward(ctx context.Context, command Command) error {
	if s == nil || s.notices == nil {
		return xerr.New(xerr.Unavailable, "reward notice service is not configured")
	}
	command = normalizeCommand(command)
	if err := validateCommand(command); err != nil {
		return err
	}

	grants := append([]*walletv1.ResourceGrant(nil), command.Grants...)
	for _, grantID := range command.GrantIDs {
		if s.wallet == nil {
			return xerr.New(xerr.Unavailable, "wallet resource grant reader is not configured")
		}
		// grant_id 来自已提交的 room outbox；精确读取避免分页扫描误拿同用户其它活动奖励。
		resp, err := s.wallet.GetResourceGrant(ctx, &walletv1.GetResourceGrantRequest{
			RequestId: "reward-notice:" + command.Activity.Code + ":" + command.ProducerEventID + ":" + grantID,
			AppCode:   appcode.FromContext(ctx),
			GrantId:   grantID,
		})
		if err != nil {
			return err
		}
		if resp.GetGrant() == nil {
			return xerr.New(xerr.Unavailable, "wallet resource grant response is empty")
		}
		grants = append(grants, resp.GetGrant())
	}
	for _, grant := range grants {
		if grant == nil || grant.GetTargetUserId() != command.TargetUserID {
			// 站内信收件人必须与钱包 grant 的真实归属完全一致，不能让事件或调用方传错用户后展示他人奖励。
			return xerr.New(xerr.Conflict, "reward notice target does not match wallet grant")
		}
	}

	rewardItems, grantIDs, err := rewardItemsFromFacts(grants, command.CoinAmount)
	if err != nil {
		return err
	}
	imageURLs := rewardImageURLs(rewardItems)
	metadata := cloneMetadata(command.Metadata)
	metadata["activity_code"] = command.Activity.Code
	metadata["activity_name"] = command.Activity.Name
	if len(grantIDs) > 0 {
		metadata["wallet_grant_ids"] = grantIDs
	}
	if command.CoinAmount > 0 {
		metadata["coin_amount"] = command.CoinAmount
	}
	body := "You received the " + command.Activity.Name + ":"
	_, err = s.notices.CreateActivityNotice(ctx, messageservice.NoticeCommand{
		TargetUserID:      command.TargetUserID,
		Producer:          "activity-reward",
		ProducerEventID:   "reward:" + command.Activity.Code + ":" + command.ProducerEventID,
		ProducerEventType: "activity_reward_granted",
		AggregateType:     command.AggregateType,
		AggregateID:       command.AggregateID,
		Title:             command.Activity.Name,
		Summary:           body,
		Body:              body,
		ImageURLs:         imageURLs,
		RewardItems:       rewardItems,
		SentAtMS:          command.SentAtMS,
		Metadata:          metadata,
	})
	return err
}

func normalizeCommand(command Command) Command {
	command.Activity.Code = strings.ToLower(strings.TrimSpace(command.Activity.Code))
	command.Activity.Name = strings.TrimSpace(command.Activity.Name)
	command.ProducerEventID = strings.TrimSpace(command.ProducerEventID)
	command.AggregateType = strings.TrimSpace(command.AggregateType)
	command.AggregateID = strings.TrimSpace(command.AggregateID)
	if command.AggregateType == "" {
		command.AggregateType = "activity_reward"
	}
	if command.AggregateID == "" {
		command.AggregateID = command.ProducerEventID
	}
	grantIDs := make([]string, 0, len(command.GrantIDs))
	seen := make(map[string]struct{}, len(command.GrantIDs))
	for _, grantID := range command.GrantIDs {
		grantID = strings.TrimSpace(grantID)
		if grantID == "" {
			continue
		}
		if _, exists := seen[grantID]; exists {
			continue
		}
		seen[grantID] = struct{}{}
		grantIDs = append(grantIDs, grantID)
	}
	command.GrantIDs = grantIDs
	return command
}

func validateCommand(command Command) error {
	if command.Activity.Code == "" || command.Activity.Name == "" || command.TargetUserID <= 0 || command.ProducerEventID == "" {
		return xerr.New(xerr.InvalidArgument, "reward notice command is incomplete")
	}
	if command.CoinAmount < 0 {
		return xerr.New(xerr.InvalidArgument, "reward notice coin amount cannot be negative")
	}
	if command.CoinAmount == 0 && len(command.Grants) == 0 && len(command.GrantIDs) == 0 {
		return xerr.New(xerr.InvalidArgument, "reward notice facts are required")
	}
	return nil
}

func rewardItemsFromFacts(grants []*walletv1.ResourceGrant, coinAmount int64) ([]messagedomain.RewardItem, []string, error) {
	items := make([]messagedomain.RewardItem, 0)
	grantIDs := make([]string, 0, len(grants))
	totalCoin := coinAmount
	for _, grant := range grants {
		if grant == nil {
			continue
		}
		if grantID := strings.TrimSpace(grant.GetGrantId()); grantID != "" {
			grantIDs = append(grantIDs, grantID)
		}
		for _, grantItem := range grant.GetItems() {
			if grantItem == nil {
				continue
			}
			snapshot := parseSnapshot(grantItem.GetResourceSnapshotJson())
			if strings.EqualFold(grantItem.GetResultType(), "wallet_credit") {
				amount := grantItem.GetQuantity()
				// 普通 wallet-credit 资源的 quantity 是资源份数，实际金币为快照单份金额乘份数；
				// 资源组原生 wallet_asset 项的 quantity 已经是最终入账额，不能再次相乘。
				if grantItem.GetResourceId() > 0 {
					perItem := snapshotInt64(snapshot, "WalletAssetAmount", "wallet_asset_amount")
					if perItem > 0 {
						if grantItem.GetQuantity() > math.MaxInt64/perItem {
							return nil, nil, xerr.New(xerr.InvalidArgument, "reward coin amount overflows")
						}
						amount = perItem * grantItem.GetQuantity()
					}
				}
				if amount > math.MaxInt64-totalCoin {
					return nil, nil, xerr.New(xerr.InvalidArgument, "reward coin amount overflows")
				}
				totalCoin += amount
				continue
			}
			items = append(items, messagedomain.RewardItem{
				RewardType:   rewardTypeResource,
				Name:         firstText(snapshotString(snapshot, "Name", "name"), "Reward"),
				ImageURL:     firstText(snapshotString(snapshot, "PreviewURL", "preview_url"), snapshotString(snapshot, "AssetURL", "asset_url"), snapshotString(snapshot, "AnimationURL", "animation_url")),
				Quantity:     grantItem.GetQuantity(),
				DurationDays: durationDays(grantItem.GetDurationMs()),
			})
		}
	}
	if totalCoin > 0 {
		items = append(items, messagedomain.RewardItem{RewardType: rewardTypeCoin, Name: "Coins", Quantity: totalCoin})
	}
	if len(items) == 0 {
		return nil, nil, xerr.New(xerr.InvalidArgument, "reward notice has no displayable items")
	}
	return items, grantIDs, nil
}

func rewardImageURLs(items []messagedomain.RewardItem) []string {
	result := make([]string, 0, len(items))
	for _, item := range items {
		if item.RewardType == rewardTypeResource && strings.TrimSpace(item.ImageURL) != "" {
			result = append(result, item.ImageURL)
		}
	}
	return result
}

func durationDays(durationMS int64) int64 {
	if durationMS <= 0 {
		return 0
	}
	// 非整日历史配置向上取整，避免实际仍有效的奖励在消息里显示 0 天。
	return (durationMS + millisecondsPerDay - 1) / millisecondsPerDay
}

func parseSnapshot(raw string) map[string]any {
	var snapshot map[string]any
	if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &snapshot); err != nil || snapshot == nil {
		return map[string]any{}
	}
	return snapshot
}

func snapshotString(snapshot map[string]any, keys ...string) string {
	for _, key := range keys {
		if value, exists := snapshot[key]; exists {
			if text := strings.TrimSpace(fmt.Sprint(value)); text != "" && text != "<nil>" {
				return text
			}
		}
	}
	return ""
}

func snapshotInt64(snapshot map[string]any, keys ...string) int64 {
	for _, key := range keys {
		switch value := snapshot[key].(type) {
		case float64:
			return int64(value)
		case int64:
			return value
		case json.Number:
			parsed, _ := value.Int64()
			return parsed
		}
	}
	return 0
}

func cloneMetadata(source map[string]any) map[string]any {
	result := make(map[string]any, len(source)+3)
	for key, value := range source {
		result[key] = value
	}
	return result
}

func firstText(values ...string) string {
	for _, value := range values {
		if value = strings.TrimSpace(value); value != "" {
			return value
		}
	}
	return ""
}
