package service

import (
	"context"
	"encoding/json"
	"math"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"time"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/integration"
	"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 (
	roomDecorationTypeBorder      = "room_border"
	roomDecorationTypeColoredName = "colored_room_name"
	roomDecorationResourceBorder  = "room_border"
	roomDecorationResourceName    = "room_name_style"
)

var roomDecorationColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$`)

type roomDecorationMetadata struct {
	Format           string    `json:"format"`
	TextColors       []string  `json:"text_colors"`
	TextColorsLegacy []string  `json:"textColors"`
	Stops            []float64 `json:"stops"`
	AngleDegrees     *float64  `json:"angle_degrees"`
	AngleLegacy      *float64  `json:"angleDegrees"`
}

// ApplyRoomDecoration 把 wallet 当前仍授权的资源快照串行写入 Room Cell。
// entitlement 与 VIP benefit 都必须精确绑定同一 resource，防止会员降级后复用历史高等级样式。
func (s *Service) ApplyRoomDecoration(ctx context.Context, req *roomv1.ApplyRoomDecorationRequest) (*roomv1.ApplyRoomDecorationResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	decorationType, resourceType, benefitCode, err := normalizeRoomDecorationRequest(req)
	if err != nil {
		return nil, err
	}
	cmd := command.ApplyRoomDecoration{
		Base: baseFromMeta(req.GetMeta()), DecorationType: decorationType,
		ResourceID: req.GetResourceId(), EntitlementID: strings.TrimSpace(req.GetEntitlementId()),
	}
	// 已提交命令必须先于 wallet 当前态重放；否则 VIP/entitlement 后续到期会把合法重试改成失败。
	if _, record, seen, checkErr := s.commandRecordForIdempotency(ctx, cmd); checkErr != nil {
		return nil, checkErr
	} else if seen {
		stored, deserializeErr := command.Deserialize(record.CommandType, record.Payload)
		if deserializeErr != nil {
			return nil, deserializeErr
		}
		storedCommand, ok := stored.(*command.ApplyRoomDecoration)
		if !ok {
			return nil, xerr.New(xerr.Internal, "stored room decoration command is invalid")
		}
		cmd = *storedCommand
	} else {
		if err := s.requireRoomOwnerMeta(ctx, cmd.RoomID(), cmd.ActorUserID()); err != nil {
			return nil, err
		}
		resource, resolveErr := s.resolveRoomDecoration(ctx, req.GetMeta().GetRequestId(), cmd.ActorUserID(), cmd.ResourceID, cmd.EntitlementID, resourceType, benefitCode)
		if resolveErr != nil {
			return nil, resolveErr
		}
		cmd.Resource = resource
	}

	result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if err := requireActiveRoom(current); err != nil {
			return mutationResult{}, nil, err
		}
		if current.OwnerUserID != cmd.ActorUserID() {
			return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "permission denied")
		}
		if cmd.Resource.ResourceID <= 0 || (cmd.Resource.ExpiresAtMS > 0 && now.UnixMilli() >= cmd.Resource.ExpiresAtMS) {
			return mutationResult{}, nil, xerr.NewWithMetadata(xerr.VIPBenefitRequired, "room decoration entitlement expired", map[string]string{
				"benefit_code": benefitCode, "required_level": "0", "current_effective_level": "0", "action": "apply_room_decoration",
			})
		}
		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")
		}
		assignmentChange := roomDecorationAssignmentChange(current, cmd.DecorationType, benefitCode, cmd.Resource, now.UnixMilli())
		if *currentDecoration != nil && reflect.DeepEqual(**currentDecoration, cmd.Resource) {
			return mutationResult{snapshot: current.ToProtoAt(now.UnixMilli()), roomDecorationAssignment: assignmentChange}, nil, nil
		}
		decoration := cmd.Resource
		*currentDecoration = &decoration
		current.Version++
		event, err := outbox.Build(current.RoomID, "RoomDecorationChanged", current.Version, now, &roomeventsv1.RoomDecorationChanged{
			ActorUserId: cmd.ActorUserID(), DecorationType: cmd.DecorationType, ResourceId: decoration.ResourceID,
			ResourceCode: decoration.ResourceCode, ResourceType: decoration.ResourceType, Name: decoration.Name,
			AssetUrl: decoration.AssetURL, PreviewUrl: decoration.PreviewURL, AnimationUrl: decoration.AnimationURL,
			Format: decoration.Format, MetadataJson: decoration.MetadataJSON, TextColors: append([]string(nil), decoration.TextColors...),
			Stops: append([]float64(nil), decoration.Stops...), AngleDegrees: decoration.AngleDegrees,
			EntitlementId: decoration.EntitlementID, ExpiresAtMs: decoration.ExpiresAtMS,
		})
		if err != nil {
			return mutationResult{}, nil, err
		}
		return mutationResult{snapshot: current.ToProtoAt(now.UnixMilli()), roomDecorationAssignment: assignmentChange}, []outbox.Record{event}, nil
	})
	if err != nil {
		return nil, err
	}
	resource := roomDecorationToProto(&cmd.Resource, s.clock.Now().UnixMilli())
	return &roomv1.ApplyRoomDecorationResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot,
		DecorationType: decorationType, Resource: resource, ServerTimeMs: s.clock.Now().UnixMilli(),
	}, nil
}

func roomDecorationAssignmentChange(current *state.RoomState, decorationType string, benefitCode string, resource state.RoomDecoration, nowMS int64) *RoomDecorationAssignmentChange {
	assignment := &RoomDecorationAssignment{
		// AppCode 由 SaveMutation 使用当前请求上下文写入，避免把租户字段复制进 RoomState。
		RoomID: current.RoomID, DecorationType: decorationType, OwnerUserID: current.OwnerUserID,
		BenefitCode: benefitCode, ResourceID: resource.ResourceID, EntitlementID: resource.EntitlementID,
		ExpiresAtMS: resource.ExpiresAtMS, UpdatedAtMS: nowMS,
	}
	return &RoomDecorationAssignmentChange{DecorationType: decorationType, Assignment: assignment}
}

func normalizeRoomDecorationRequest(req *roomv1.ApplyRoomDecorationRequest) (string, string, string, error) {
	if req == nil || req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetCommandId()) == "" || req.GetResourceId() <= 0 {
		return "", "", "", xerr.New(xerr.InvalidArgument, "room decoration request is incomplete")
	}
	switch normalizeVIPCode(req.GetDecorationType()) {
	case roomDecorationTypeBorder:
		return roomDecorationTypeBorder, roomDecorationResourceBorder, vipBenefitRoomBorder, nil
	case roomDecorationTypeColoredName:
		return roomDecorationTypeColoredName, roomDecorationResourceName, vipBenefitColoredRoomName, nil
	default:
		return "", "", "", xerr.New(xerr.InvalidArgument, "decoration_type is invalid")
	}
}

func (s *Service) resolveRoomDecoration(ctx context.Context, requestID string, userID int64, resourceID int64, entitlementID string, resourceType string, benefitCode string) (state.RoomDecoration, error) {
	checker, ok := s.wallet.(integration.WalletVIPBenefitClient)
	if !ok {
		return state.RoomDecoration{}, xerr.New(xerr.Unavailable, "wallet vip benefit client is unavailable")
	}
	lookupCtx, cancel := context.WithTimeout(ctx, roomVIPLookupTimeout)
	defer cancel()
	decision, err := checker.CheckVipBenefit(lookupCtx, &walletv1.CheckVipBenefitRequest{
		RequestId: strings.TrimSpace(requestID), AppCode: appcode.FromContext(ctx), UserId: userID, BenefitCode: benefitCode,
		// 具体资源坐标让 wallet owner 判定“资源等级 <= 当前有效 VIP 等级”；room-service
		// 只核对返回快照，不复制付费/体验卡、目录状态或继承算法。
		ResourceId: resourceID, ResourceType: resourceType,
	})
	if err != nil {
		return state.RoomDecoration{}, xerr.New(xerr.Unavailable, "check room decoration vip benefit failed")
	}
	benefit := decision.GetBenefit()
	catalogResource := benefit.GetResource()
	if decision == nil || !decision.GetAllowed() || benefit == nil || benefit.GetResourceId() != resourceID ||
		normalizeVIPCode(benefit.GetResourceType()) != resourceType || catalogResource == nil ||
		catalogResource.GetResourceId() != resourceID || normalizeVIPCode(catalogResource.GetResourceType()) != resourceType ||
		normalizeVIPCode(catalogResource.GetStatus()) != "active" {
		requiredLevel, currentLevel := int32(0), int32(0)
		if decision != nil {
			requiredLevel, currentLevel = decision.GetRequiredLevel(), decision.GetCurrentEffectiveLevel()
		}
		return state.RoomDecoration{}, xerr.NewWithMetadata(xerr.VIPBenefitRequired, "room decoration vip benefit is required", map[string]string{
			"benefit_code": benefitCode, "required_level": strconv.FormatInt(int64(requiredLevel), 10),
			"current_effective_level": strconv.FormatInt(int64(currentLevel), 10), "action": "apply_room_decoration",
		})
	}
	resources, ok := s.wallet.(integration.WalletUserResourceClient)
	if !ok {
		return state.RoomDecoration{}, xerr.New(xerr.Unavailable, "wallet user resource client is unavailable")
	}
	resourceCtx, resourceCancel := context.WithTimeout(ctx, roomVIPLookupTimeout)
	defer resourceCancel()
	resourceResponse, err := resources.ListUserResources(resourceCtx, &walletv1.ListUserResourcesRequest{
		RequestId: strings.TrimSpace(requestID), AppCode: appcode.FromContext(ctx), UserId: userID,
		ResourceType: resourceType, ActiveOnly: true,
	})
	if err != nil {
		return state.RoomDecoration{}, xerr.New(xerr.Unavailable, "list room decoration resources failed")
	}
	nowMS := s.clock.Now().UnixMilli()
	for _, entitlement := range resourceResponse.GetResources() {
		entitlementResource := entitlement.GetResource()
		if entitlement.GetResourceId() != resourceID || entitlementResource == nil || entitlementResource.GetResourceId() != resourceID ||
			normalizeVIPCode(entitlementResource.GetResourceType()) != resourceType {
			continue
		}
		if entitlementID != "" && strings.TrimSpace(entitlement.GetEntitlementId()) != entitlementID {
			continue
		}
		if normalizeVIPCode(entitlement.GetStatus()) != "active" || entitlement.GetEffectiveAtMs() > nowMS ||
			(entitlement.GetExpiresAtMs() > 0 && entitlement.GetExpiresAtMs() <= nowMS) || entitlement.GetRemainingQuantity() <= 0 {
			continue
		}
		// entitlement 只证明该用户仍有权使用 resource_id；展示内容必须取 CheckVipBenefit 返回的
		// 当前 active 目录资源（可能来自任一不高于当前 VIP 的等级）。后台更新素材后会刷新房间，
		// 且不会继续渲染 entitlement 的历史快照。
		metadata, metadataErr := parseRoomDecorationMetadata(resourceType, catalogResource.GetMetadataJson())
		if metadataErr != nil {
			return state.RoomDecoration{}, metadataErr
		}
		vipExpiresAtMS := int64(0)
		if decision.GetState() != nil && decision.GetState().GetEffectiveVip() != nil {
			vipExpiresAtMS = decision.GetState().GetEffectiveVip().GetExpiresAtMs()
		}
		angleDegrees := float64(0)
		if metadata.AngleDegrees != nil {
			angleDegrees = *metadata.AngleDegrees
		}
		return state.RoomDecoration{
			ResourceID: catalogResource.ResourceId, ResourceCode: catalogResource.ResourceCode, ResourceType: catalogResource.ResourceType,
			Name: catalogResource.Name, AssetURL: catalogResource.AssetUrl, PreviewURL: catalogResource.PreviewUrl, AnimationURL: catalogResource.AnimationUrl,
			Format: metadata.Format, MetadataJSON: catalogResource.MetadataJson, EntitlementID: entitlement.EntitlementId,
			TextColors: metadata.TextColors, Stops: metadata.Stops, AngleDegrees: angleDegrees,
			ExpiresAtMS: minNonZeroMS(entitlement.GetExpiresAtMs(), vipExpiresAtMS),
		}, nil
	}
	return state.RoomDecoration{}, xerr.New(xerr.NotFound, "active room decoration entitlement not found")
}

func parseRoomDecorationMetadata(resourceType string, raw string) (roomDecorationMetadata, error) {
	var metadata roomDecorationMetadata
	if strings.TrimSpace(raw) != "" && json.Unmarshal([]byte(raw), &metadata) != nil {
		return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room decoration metadata_json is invalid")
	}
	if len(metadata.TextColors) == 0 {
		metadata.TextColors = metadata.TextColorsLegacy
	}
	if metadata.AngleDegrees == nil {
		metadata.AngleDegrees = metadata.AngleLegacy
	}
	metadata.Format = strings.ToLower(strings.TrimSpace(metadata.Format))
	if metadata.AngleDegrees != nil && (math.IsNaN(*metadata.AngleDegrees) || math.IsInf(*metadata.AngleDegrees, 0)) {
		return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room decoration angle is invalid")
	}
	if resourceType == roomDecorationResourceName {
		if len(metadata.TextColors) == 0 || len(metadata.TextColors) > 8 || (len(metadata.Stops) != 0 && len(metadata.Stops) != len(metadata.TextColors)) {
			return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room name style colors are invalid")
		}
		for _, color := range metadata.TextColors {
			if !roomDecorationColorPattern.MatchString(strings.TrimSpace(color)) {
				return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room name style color is invalid")
			}
		}
		previous := -1.0
		for _, stop := range metadata.Stops {
			if math.IsNaN(stop) || math.IsInf(stop, 0) || stop < 0 || stop > 1 || stop < previous {
				return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room name style stops are invalid")
			}
			previous = stop
		}
	}
	return metadata, nil
}

func minNonZeroMS(left int64, right int64) int64 {
	if left <= 0 {
		return right
	}
	if right <= 0 || left < right {
		return left
	}
	return right
}

func roomDecorationToProto(resource *state.RoomDecoration, nowMS int64) *roomv1.RoomDecorationResource {
	if resource == nil || resource.ResourceID <= 0 || (resource.ExpiresAtMS > 0 && nowMS >= resource.ExpiresAtMS) {
		return nil
	}
	return &roomv1.RoomDecorationResource{
		ResourceId: resource.ResourceID, ResourceCode: resource.ResourceCode, ResourceType: resource.ResourceType,
		Name: resource.Name, AssetUrl: resource.AssetURL, PreviewUrl: resource.PreviewURL, AnimationUrl: resource.AnimationURL,
		Format: resource.Format, MetadataJson: resource.MetadataJSON, EntitlementId: resource.EntitlementID,
		TextColors: append([]string(nil), resource.TextColors...), Stops: append([]float64(nil), resource.Stops...),
		AngleDegrees: resource.AngleDegrees, ExpiresAtMs: resource.ExpiresAtMS,
	}
}
