package service

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"reflect"
	"sort"
	"strings"
	"time"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/walletmq"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/room/command"
	"hyapp/services/room-service/internal/room/outbox"
	"hyapp/services/room-service/internal/room/rank"
	"hyapp/services/room-service/internal/room/state"
)

const (
	walletEventResourceChanged       = "ResourceChanged"
	walletEventResourceGrantRevoked  = "ResourceGrantRevoked"
	walletEventUserResourceChanged   = "UserResourceChanged"
	walletEventVipEffectiveChanged   = "VipEffectiveChanged"
	walletEventVipProgramChanged     = "VipProgramChanged"
	roomDecorationReconcileBatchSize = 100
)

// HandleWalletDecorationEvent 消费 wallet owner 已提交的 VIP/资源变化：VIP 等级只更新列表读模型，装扮仍通过 Room Cell 命令收敛。
// 专用 assignment 表提供反向索引；每个房间的 command log 和全局 consumption 表共同承担 at-least-once 幂等。
func (s *Service) HandleWalletDecorationEvent(ctx context.Context, message walletmq.WalletOutboxMessage) error {
	queries, decorationHandled, err := walletDecorationAssignmentQueries(message)
	vipProjectionHandled := strings.TrimSpace(message.EventType) == walletmq.EventTypeVIPActivated || strings.TrimSpace(message.EventType) == walletEventVipEffectiveChanged
	if err != nil || (!decorationHandled && !vipProjectionHandled) {
		return err
	}
	store, ok := s.repository.(RoomDecorationReconciliationStore)
	if !ok {
		return xerr.New(xerr.Unavailable, "room decoration reconciliation store is unavailable")
	}
	ctx = appcode.WithContext(ctx, message.AppCode)
	consumed, err := store.HasConsumedWalletDecorationEvent(ctx, message.EventID)
	if err != nil || consumed {
		return err
	}
	if vipProjectionHandled {
		if message.UserID <= 0 {
			return fmt.Errorf("%s has no user_id", message.EventType)
		}
		// 事件 payload 在体验卡卸下时不承诺携带回落等级；重新读取 wallet effective state 才能避免复制合并算法。
		if err := s.refreshRoomOwnerVIPProjection(ctx, message.EventID, message.UserID); err != nil {
			return err
		}
	}

	if decorationHandled {
		assignments, err := listWalletEventDecorationAssignments(ctx, store, queries)
		if err != nil {
			return err
		}
		for _, assignment := range assignments {
			if err := s.reconcileRoomDecorationAssignment(ctx, message, assignment); err != nil {
				return err
			}
		}
	}
	return store.MarkWalletDecorationEventConsumed(ctx, message.EventID, message.EventType, s.clock.Now().UnixMilli())
}

func walletDecorationAssignmentQueries(message walletmq.WalletOutboxMessage) ([]RoomDecorationAssignmentQuery, bool, error) {
	switch strings.TrimSpace(message.EventType) {
	case walletEventResourceChanged:
		resourceID := walletPayloadInt64(message.PayloadJSON, "resource_id", "resourceid")
		if resourceID <= 0 {
			return nil, true, fmt.Errorf("ResourceChanged payload has no resource_id")
		}
		return []RoomDecorationAssignmentQuery{{ResourceID: resourceID}}, true, nil
	case walletEventResourceGrantRevoked, walletEventUserResourceChanged, walletEventVipEffectiveChanged:
		if message.UserID <= 0 {
			return nil, true, fmt.Errorf("%s has no user_id", message.EventType)
		}
		return []RoomDecorationAssignmentQuery{{OwnerUserID: message.UserID}}, true, nil
	case walletEventVipProgramChanged:
		resourceIDs := walletPayloadInt64Slice(message.PayloadJSON,
			"resource_ids", "old_resource_ids", "new_resource_ids", "oldresourceids", "newresourceids")
		if len(resourceIDs) == 0 {
			// program status/level_count 变化可能影响所有当前装扮；这里只分页扫描专用 assignment 小表。
			return []RoomDecorationAssignmentQuery{{}}, true, nil
		}
		queries := make([]RoomDecorationAssignmentQuery, 0, len(resourceIDs))
		for _, resourceID := range resourceIDs {
			queries = append(queries, RoomDecorationAssignmentQuery{ResourceID: resourceID})
		}
		return queries, true, nil
	default:
		// legacy Tag 兼容订阅会收到同 topic 的其它钱包事实，非装扮事件必须快速确认且不能写消费表。
		return nil, false, nil
	}
}

func listWalletEventDecorationAssignments(ctx context.Context, store RoomDecorationReconciliationStore, queries []RoomDecorationAssignmentQuery) ([]RoomDecorationAssignment, error) {
	byKey := make(map[string]RoomDecorationAssignment)
	for _, query := range queries {
		query.Limit = roomDecorationReconcileBatchSize
		for {
			page, err := store.ListRoomDecorationAssignments(ctx, query)
			if err != nil {
				return nil, err
			}
			for _, assignment := range page {
				byKey[assignment.RoomID+"\x00"+assignment.DecorationType] = assignment
			}
			if len(page) < query.Limit {
				break
			}
			last := page[len(page)-1]
			query.AfterRoomID, query.AfterDecorationType = last.RoomID, last.DecorationType
		}
	}
	assignments := make([]RoomDecorationAssignment, 0, len(byKey))
	for _, assignment := range byKey {
		assignments = append(assignments, assignment)
	}
	sort.Slice(assignments, func(i, j int) bool {
		if assignments[i].RoomID != assignments[j].RoomID {
			return assignments[i].RoomID < assignments[j].RoomID
		}
		return assignments[i].DecorationType < assignments[j].DecorationType
	})
	return assignments, nil
}

func (s *Service) reconcileRoomDecorationAssignment(ctx context.Context, message walletmq.WalletOutboxMessage, assignment RoomDecorationAssignment) error {
	resourceType, fallbackBenefitCode, err := roomDecorationContract(assignment.DecorationType)
	if err != nil {
		return err
	}
	benefitCode := strings.TrimSpace(assignment.BenefitCode)
	if benefitCode == "" {
		benefitCode = fallbackBenefitCode
	}
	resource, resolveErr := s.resolveRoomDecoration(
		ctx, message.EventID, assignment.OwnerUserID, assignment.ResourceID, assignment.EntitlementID, resourceType, benefitCode,
	)
	active := resolveErr == nil
	if resolveErr != nil && !walletDecorationInvalidationError(resolveErr) {
		// wallet 暂不可用不能被误判成撤销；返回错误让 RocketMQ 保持重试。
		return resolveErr
	}

	cmd := command.ReconcileRoomDecoration{
		Base: command.Base{
			AppCode: appcode.FromContext(ctx), RequestID: message.EventID,
			CommandID: walletDecorationCommandID(message.EventID, assignment.RoomID, assignment.DecorationType),
			ActorID:   assignment.OwnerUserID, Room: assignment.RoomID, GatewayNodeID: "wallet_outbox",
			SentAtMS: message.OccurredAtMS,
		},
		DecorationType: assignment.DecorationType, ExpectedResourceID: assignment.ResourceID,
		BenefitCode: benefitCode, SourceEventID: message.EventID, Active: active, Resource: resource,
	}
	_, err = s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		var currentDecoration **state.RoomDecoration
		switch cmd.DecorationType {
		case roomDecorationTypeBorder:
			currentDecoration = &current.RoomBorder
		case roomDecorationTypeColoredName:
			currentDecoration = &current.RoomNameStyle
		default:
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "decoration_type is invalid")
		}
		if *currentDecoration == nil || (*currentDecoration).ResourceID != cmd.ExpectedResourceID {
			// assignment 查询后房主可能已切换资源；条件删除旧索引，不触碰当前新装扮。
			return mutationResult{
				snapshot: current.ToProtoAt(now.UnixMilli()),
				roomDecorationAssignment: &RoomDecorationAssignmentChange{
					DecorationType: cmd.DecorationType, ExpectedResourceID: cmd.ExpectedResourceID,
				},
			}, nil, nil
		}
		if cmd.Active && reflect.DeepEqual(**currentDecoration, cmd.Resource) {
			return mutationResult{
				snapshot:                 current.ToProtoAt(now.UnixMilli()),
				roomDecorationAssignment: roomDecorationAssignmentChange(current, cmd.DecorationType, cmd.BenefitCode, cmd.Resource, now.UnixMilli()),
			}, nil, nil
		}

		if cmd.Active {
			decoration := cmd.Resource
			*currentDecoration = decoration.Clone()
		} else {
			*currentDecoration = nil
		}
		current.Version++
		changed := cmd.Resource
		if !cmd.Active {
			changed = state.RoomDecoration{}
		}
		event, buildErr := outbox.Build(current.RoomID, "RoomDecorationChanged", current.Version, now, &roomeventsv1.RoomDecorationChanged{
			ActorUserId: cmd.ActorUserID(), DecorationType: cmd.DecorationType, ResourceId: changed.ResourceID,
			ResourceCode: changed.ResourceCode, ResourceType: changed.ResourceType, Name: changed.Name,
			AssetUrl: changed.AssetURL, PreviewUrl: changed.PreviewURL, AnimationUrl: changed.AnimationURL,
			Format: changed.Format, MetadataJson: changed.MetadataJSON, TextColors: append([]string(nil), changed.TextColors...),
			Stops: append([]float64(nil), changed.Stops...), AngleDegrees: changed.AngleDegrees,
			EntitlementId: changed.EntitlementID, ExpiresAtMs: changed.ExpiresAtMS,
		})
		if buildErr != nil {
			return mutationResult{}, nil, buildErr
		}
		assignmentChange := &RoomDecorationAssignmentChange{
			DecorationType: cmd.DecorationType, ExpectedResourceID: cmd.ExpectedResourceID,
		}
		if cmd.Active {
			assignmentChange = roomDecorationAssignmentChange(current, cmd.DecorationType, cmd.BenefitCode, cmd.Resource, now.UnixMilli())
		}
		return mutationResult{
			snapshot: current.ToProtoAt(now.UnixMilli()), roomDecorationAssignment: assignmentChange,
		}, []outbox.Record{event}, nil
	})
	if xerr.IsCode(err, xerr.NotFound) {
		// 房间与 assignment 删除并发时，删除事务已经完成 owner 清理，该 wallet 事件可安全确认。
		return nil
	}
	return err
}

func roomDecorationContract(decorationType string) (string, string, error) {
	switch strings.TrimSpace(decorationType) {
	case roomDecorationTypeBorder:
		return roomDecorationResourceBorder, vipBenefitRoomBorder, nil
	case roomDecorationTypeColoredName:
		return roomDecorationResourceName, vipBenefitColoredRoomName, nil
	default:
		return "", "", xerr.New(xerr.InvalidArgument, "decoration_type is invalid")
	}
}

func walletDecorationInvalidationError(err error) bool {
	switch xerr.CodeOf(err) {
	case xerr.VIPBenefitRequired, xerr.NotFound, xerr.InvalidArgument:
		return true
	default:
		return false
	}
}

func walletDecorationCommandID(eventID string, roomID string, decorationType string) string {
	sum := sha256.Sum256([]byte(strings.Join([]string{eventID, roomID, decorationType}, "\x00")))
	return "wallet_deco_" + hex.EncodeToString(sum[:16])
}

func walletPayloadInt64(payload string, keys ...string) int64 {
	values := walletPayloadObject(payload)
	for _, key := range keys {
		if raw, exists := values[normalizeWalletPayloadKey(key)]; exists {
			var value int64
			if json.Unmarshal(raw, &value) == nil && value > 0 {
				return value
			}
		}
	}
	return 0
}

func walletPayloadInt64Slice(payload string, keys ...string) []int64 {
	values := walletPayloadObject(payload)
	seen := make(map[int64]struct{})
	for _, key := range keys {
		raw, exists := values[normalizeWalletPayloadKey(key)]
		if !exists {
			continue
		}
		var items []int64
		if json.Unmarshal(raw, &items) != nil {
			continue
		}
		for _, item := range items {
			if item > 0 {
				seen[item] = struct{}{}
			}
		}
	}
	out := make([]int64, 0, len(seen))
	for item := range seen {
		out = append(out, item)
	}
	sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
	return out
}

func walletPayloadObject(payload string) map[string]json.RawMessage {
	var decoded map[string]json.RawMessage
	if json.Unmarshal([]byte(payload), &decoded) != nil {
		return nil
	}
	out := make(map[string]json.RawMessage, len(decoded))
	for key, value := range decoded {
		out[normalizeWalletPayloadKey(key)] = value
	}
	return out
}

func normalizeWalletPayloadKey(value string) string {
	return strings.ReplaceAll(strings.ToLower(strings.TrimSpace(value)), "_", "")
}
