package service

import (
	"context"
	"math"
	"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/giftlimits"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/integration"
	"hyapp/services/room-service/internal/room/outbox"
	"hyapp/services/room-service/internal/room/state"
)

const (
	// 机器人送礼量远高于礼物配置变更频率；短缓存把每次展示的 wallet/MySQL 只读查询收敛为每 App/区域每分钟一次。
	robotGiftCatalogTTL = time.Minute
	// wallet 故障只短暂负缓存，既让同批机器人共享失败，也能在服务恢复后迅速重试。
	robotGiftCatalogErrorTTL     = time.Second
	robotGiftCatalogQueryTimeout = 3 * time.Second
	robotGiftCatalogPageSize     = int32(500)
	robotGiftCatalogMaxPages     = int32(20)
)

type robotGiftCatalogKey struct {
	appCode  string
	regionID int64
}

type robotGiftCatalogEntry struct {
	expiresAtMS int64
	gifts       map[string]*walletv1.GiftConfig
	loadErr     error
}

type robotGiftCatalogLoad struct {
	done chan struct{}
}

type RobotSendGiftInput struct {
	Meta                   *roomv1.RequestMeta
	TargetUserID           int64
	GiftID                 string
	GiftCount              int32
	PoolID                 string
	RobotUserIDs           []int64
	SyntheticRewardCoins   int64
	SyntheticMultiplierPPM int64
	// RealRoomHeat 是历史字段名；当前只表示“真人房机器人”校验范围，绝不再产生真实热度或任何持久化贡献。
	RealRoomHeat bool
}

type robotGiftOptions struct {
	Enabled                bool
	RobotUserIDs           []int64
	SyntheticRewardCoins   int64
	SyntheticMultiplierPPM int64
	// RealRoomHeat 仅供 validateBeforeDebit 区分专属机器人房和真人房机器人池，不能进入状态/账务结算。
	RealRoomHeat bool
}

func (s *Service) RobotSendGift(ctx context.Context, input RobotSendGiftInput) (*roomv1.SendGiftResponse, error) {
	req := &roomv1.SendGiftRequest{
		Meta:          input.Meta,
		TargetUserId:  input.TargetUserID,
		TargetUserIds: []int64{input.TargetUserID},
		GiftId:        strings.TrimSpace(input.GiftID),
		GiftCount:     input.GiftCount,
		TargetType:    "user",
		PoolId:        input.PoolID,
	}
	ctx = contextFromMeta(ctx, req.GetMeta())
	flow := newGiftFlow(s, ctx, req, giftSendOptions{
		Robot: robotGiftOptions{
			Enabled:                true,
			RobotUserIDs:           input.RobotUserIDs,
			SyntheticRewardCoins:   input.SyntheticRewardCoins,
			SyntheticMultiplierPPM: input.SyntheticMultiplierPPM,
			RealRoomHeat:           input.RealRoomHeat,
		},
	})
	// RobotGift 永远是纯展示语义。RealRoomHeat 只兼容真人房机器人参与者校验，不能再授权热度、榜单、火箭或统计落库。
	flow.cmd.RobotGift = true
	flow.cmd.RobotWalletGift = true

	if flow.cmd.RoomID() == "" || flow.cmd.ActorUserID() <= 0 || !giftlimits.ValidCommandID(flow.cmd.ID()) {
		return nil, xerr.New(xerr.InvalidArgument, "command meta is incomplete")
	}
	if s.isDraining() {
		return nil, xerr.New(xerr.Unavailable, "room service is draining")
	}

	// 只读取当前 Room Cell 做 presence 与机器人池校验；ensureCell 仅续租 Redis owner lease，
	// 不进入 mutateRoom，因此不会写 room_gift_operations、command log、snapshot、账本或房间统计表。
	snapshot, leaseToken, err := s.robotGiftOwnerSnapshot(ctx, flow.cmd.RoomID())
	if err != nil {
		return nil, err
	}
	current := state.FromProto(snapshot)
	if err := flow.validateBeforeDebit(current); err != nil {
		return nil, err
	}

	// 礼物目录调用是只读快照查询，替代历史 DebitRobotGift；返回值仅用于 IM 图标、动画、类型和展示价值。
	giftConfig, err := s.robotGiftConfig(ctx, flow.cmd.GiftID, snapshot.GetVisibleRegionId())
	if err != nil {
		return nil, err
	}
	billing, err := robotGiftDisplayBilling(giftConfig, flow.cmd.GiftCount)
	if err != nil {
		return nil, err
	}
	targetBillings := []giftTargetBilling{{
		TargetUserID: flow.cmd.TargetUserID,
		CommandID:    flow.cmd.ID(),
		Billing:      billing,
	}}
	targetGiftValues := map[int64]int64{}
	if target := current.OnlineUsers[flow.cmd.TargetUserID]; target != nil {
		// 纯展示礼物不能改变麦位/在线用户累计值；事件携带当前值，客户端不会把机器人价值误并入真实状态。
		targetGiftValues[flow.cmd.TargetUserID] = target.GiftValue
	}

	now := s.clock.Now()
	records, err := buildRoomGiftSentRecords(flow.cmd.RoomID(), snapshot.GetVersion(), now, flow.cmd, RoomMeta{
		RoomID:          flow.cmd.RoomID(),
		VisibleRegionID: snapshot.GetVisibleRegionId(),
	}, snapshot.GetLocked(), snapshot.GetOwnerUserId(), targetBillings, targetGiftValues)
	if err != nil {
		return nil, err
	}

	var luckyGift *roomv1.LuckyGiftDrawResult
	var luckyGifts []*roomv1.LuckyGiftDrawResult
	if flow.cmd.SyntheticLuckyGift {
		luckyGift = syntheticLuckyGiftResult(flow.cmd, targetBillings, now)
		if luckyGift != nil {
			luckyGifts = []*roomv1.LuckyGiftDrawResult{luckyGift}
			drawRecord, buildErr := outbox.Build(flow.cmd.RoomID(), "RoomRobotLuckyGiftDrawn", snapshot.GetVersion(), now, &roomeventsv1.RoomRobotLuckyGiftDrawn{
				DrawId:               luckyGift.GetDrawId(),
				CommandId:            flow.cmd.ID(),
				SenderUserId:         flow.cmd.ActorUserID(),
				TargetUserId:         flow.cmd.TargetUserID,
				GiftId:               flow.cmd.GiftID,
				GiftCount:            flow.cmd.GiftCount,
				PoolId:               flow.cmd.PoolID,
				MultiplierPpm:        luckyGift.GetMultiplierPpm(),
				BaseRewardCoins:      luckyGift.GetBaseRewardCoins(),
				EffectiveRewardCoins: luckyGift.GetEffectiveRewardCoins(),
				CreatedAtMs:          luckyGift.GetCreatedAtMs(),
				VisibleRegionId:      snapshot.GetVisibleRegionId(),
				IsRobot:              true,
				Synthetic:            true,
			})
			if buildErr != nil {
				return nil, buildErr
			}
			records = append(records, drawRecord)
		}
	}

	// outbox.Record 在这里仅是复用 protobuf 信封的内存载体；不调用 repository/outboxPublisher，
	// 只经过采样器后直接交给生产环境的 TencentIMPublisher，因此不会生成任何 RocketMQ 消息。
	records = s.sampleRobotDisplayRecords(ctx, records)
	if len(records) > 0 {
		// wallet 目录查询最多持续 3 秒；发布前用同一 token 再做 fencing，防止查询期间房间已被另一节点接管。
		if err := s.verifyRobotGiftOwner(ctx, flow.cmd.RoomID(), leaseToken); err != nil {
			return nil, err
		}
	}
	s.publishRobotDisplayRecordsBestEffort(ctx, records)

	return &roomv1.SendGiftResponse{
		Result:     commandResult(true, snapshot.GetVersion(), now),
		RoomHeat:   snapshot.GetHeat(),
		GiftRank:   snapshot.GetGiftRank(),
		Room:       snapshot,
		Rocket:     snapshot.GetRocket(),
		LuckyGift:  luckyGift,
		LuckyGifts: luckyGifts,
	}, nil
}

func (s *Service) robotGiftOwnerSnapshot(ctx context.Context, roomID string) (*roomv1.RoomSnapshot, string, error) {
	if s.directory == nil {
		// 极简内部测试允许没有 lease 目录；生产 HealthCheck 会拒绝这种装配。
		snapshot, err := s.currentSnapshot(ctx, roomID)
		if err != nil {
			return nil, "", err
		}
		if snapshot == nil {
			return nil, "", xerr.New(xerr.NotFound, "room not found")
		}
		return snapshot, "", nil
	}
	roomCell, lease, err := s.ensureCell(ctx, roomID)
	if err != nil {
		return nil, "", err
	}
	current, _, err := roomCell.Snapshot(ctx)
	if err != nil {
		return nil, "", err
	}
	return snapshotWithApp(ctx, current.ToProto()), lease.LeaseToken, nil
}

func (s *Service) verifyRobotGiftOwner(ctx context.Context, roomID string, leaseToken string) error {
	if s.directory == nil {
		return nil
	}
	owned, err := s.directory.VerifyOwner(ctx, runtimeRoomKeyFromContext(ctx, roomID), s.nodeID, leaseToken, s.clock.Now())
	if err != nil {
		return err
	}
	if !owned {
		return xerr.New(xerr.Conflict, "room owner lease changed before robot display")
	}
	return nil
}

func (s *Service) robotGiftConfig(ctx context.Context, giftID string, regionID int64) (*walletv1.GiftConfig, error) {
	key := robotGiftCatalogKey{appCode: appcode.FromContext(ctx), regionID: regionID}
	for {
		nowMS := s.clock.Now().UnixMilli()
		s.robotGiftCatalogMu.Lock()
		if cached, exists := s.robotGiftCatalog[key]; exists && cached.expiresAtMS > nowMS {
			s.robotGiftCatalogMu.Unlock()
			if cached.loadErr != nil {
				return nil, cached.loadErr
			}
			return robotGiftConfigOrNotFound(cached.gifts[giftID])
		}
		if loading := s.robotGiftCatalogLoads[key]; loading != nil {
			done := loading.done
			s.robotGiftCatalogMu.Unlock()
			select {
			case <-done:
				// loader 已把成功或短负缓存结果写入 catalog；循环一次统一读取。
				continue
			case <-ctx.Done():
				return nil, ctx.Err()
			}
		}
		if s.robotGiftCatalogLoads == nil {
			s.robotGiftCatalogLoads = make(map[robotGiftCatalogKey]*robotGiftCatalogLoad)
		}
		loading := &robotGiftCatalogLoad{done: make(chan struct{})}
		s.robotGiftCatalogLoads[key] = loading
		s.robotGiftCatalogMu.Unlock()
		// loader 脱离首个 caller 的取消信号但仍受 3 秒 RPC timeout；所有调用者都按自己的 ctx 等待同一 done。
		go s.completeRobotGiftCatalogLoad(context.WithoutCancel(ctx), key, loading)
		select {
		case <-loading.done:
			continue
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}
}

func (s *Service) completeRobotGiftCatalogLoad(ctx context.Context, key robotGiftCatalogKey, loading *robotGiftCatalogLoad) {
	// 网络查询在全局缓存锁外执行；同 key 等待 done，不同 key 可并行请求 wallet。
	gifts, loadErr := s.loadRobotGiftCatalog(ctx, key)
	now := s.clock.Now()
	expiresAt := now.Add(robotGiftCatalogTTL)
	if loadErr != nil {
		expiresAt = now.Add(robotGiftCatalogErrorTTL)
	}
	s.robotGiftCatalogMu.Lock()
	if s.robotGiftCatalog == nil {
		s.robotGiftCatalog = make(map[robotGiftCatalogKey]robotGiftCatalogEntry)
	}
	s.robotGiftCatalog[key] = robotGiftCatalogEntry{expiresAtMS: expiresAt.UnixMilli(), gifts: gifts, loadErr: loadErr}
	delete(s.robotGiftCatalogLoads, key)
	close(loading.done)
	s.pruneRobotGiftCatalogLocked(now.UnixMilli())
	s.robotGiftCatalogMu.Unlock()
}

func (s *Service) pruneRobotGiftCatalogLocked(nowMS int64) {
	if len(s.robotGiftCatalog) <= 1024 {
		return
	}
	for key, entry := range s.robotGiftCatalog {
		if entry.expiresAtMS <= nowMS && s.robotGiftCatalogLoads[key] == nil {
			delete(s.robotGiftCatalog, key)
		}
	}
}

func robotGiftConfigOrNotFound(gift *walletv1.GiftConfig) (*walletv1.GiftConfig, error) {
	if gift == nil {
		return nil, xerr.New(xerr.NotFound, "robot gift config is not active in room region")
	}
	return gift, nil
}

func (s *Service) loadRobotGiftCatalog(ctx context.Context, key robotGiftCatalogKey) (map[string]*walletv1.GiftConfig, error) {
	client, ok := s.wallet.(integration.WalletGiftCatalogClient)
	if !ok || client == nil {
		return nil, xerr.New(xerr.Unavailable, "wallet gift catalog is not configured")
	}

	queryCtx, cancel := context.WithTimeout(ctx, robotGiftCatalogQueryTimeout)
	defer cancel()
	gifts := make(map[string]*walletv1.GiftConfig)
	var loaded int64
	for page := int32(1); page <= robotGiftCatalogMaxPages; page++ {
		resp, err := client.ListGiftConfigs(queryCtx, &walletv1.ListGiftConfigsRequest{
			AppCode:      key.appCode,
			Page:         page,
			PageSize:     robotGiftCatalogPageSize,
			ActiveOnly:   true,
			RegionId:     key.regionID,
			FilterRegion: true,
		})
		if err != nil {
			return nil, err
		}
		for _, gift := range resp.GetGifts() {
			if id := strings.TrimSpace(gift.GetGiftId()); id != "" {
				gifts[id] = gift
			}
		}
		loaded += int64(len(resp.GetGifts()))
		if loaded >= resp.GetTotal() || len(resp.GetGifts()) < int(robotGiftCatalogPageSize) {
			return gifts, nil
		}
	}
	return nil, xerr.New(xerr.Unavailable, "wallet gift catalog exceeds robot display limit")
}

func robotGiftDisplayBilling(gift *walletv1.GiftConfig, giftCount int32) (*walletv1.DebitGiftResponse, error) {
	if gift == nil || giftCount <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "robot gift display config is invalid")
	}
	coinSpent, err := checkedRobotGiftDisplayValue(gift.GetCoinPrice(), giftCount)
	if err != nil {
		return nil, err
	}
	resource := gift.GetResource()
	iconURL := ""
	animationURL := ""
	name := strings.TrimSpace(gift.GetName())
	if resource != nil {
		iconURL = strings.TrimSpace(resource.GetPreviewUrl())
		if iconURL == "" {
			iconURL = strings.TrimSpace(resource.GetAssetUrl())
		}
		animationURL = strings.TrimSpace(resource.GetAnimationUrl())
		if name == "" {
			name = strings.TrimSpace(resource.GetName())
		}
	}
	// 该返回对象只是复用 RoomGiftSent 构造器的展示快照，不包含 receipt/transaction/balance，也没有任何账务含义。
	// HeatValue 必须保持 0：Flutter 会把 gift_value 增加到成员和麦位的本地值，纯展示事件不能制造短暂的假贡献。
	return &walletv1.DebitGiftResponse{
		CoinSpent:        coinSpent,
		ChargeAmount:     coinSpent,
		HeatValue:        0,
		GiftTypeCode:     gift.GetGiftTypeCode(),
		CpRelationType:   gift.GetCpRelationType(),
		GiftName:         name,
		GiftIconUrl:      iconURL,
		GiftAnimationUrl: animationURL,
		GiftEffectTypes:  append([]string(nil), gift.GetEffectTypes()...),
		PriceVersion:     gift.GetPriceVersion(),
	}, nil
}

func checkedRobotGiftDisplayValue(unitValue int64, giftCount int32) (int64, error) {
	if unitValue < 0 || giftCount <= 0 || unitValue > math.MaxInt64/int64(giftCount) {
		return 0, xerr.New(xerr.InvalidArgument, "robot gift display value is invalid")
	}
	return unitValue * int64(giftCount), nil
}

func requireRobotGiftParticipants(current *state.RoomState, senderUserID int64, targetUserIDs []int64, runtimeRobotUserIDs []int64, requireRobotRoom bool) error {
	if current == nil {
		return xerr.New(xerr.PermissionDenied, "robot gift requires room state")
	}
	if requireRobotRoom && (current == nil || current.RoomExt[roomExtRobotRoomKey] != "true") {
		return xerr.New(xerr.PermissionDenied, "robot gift requires robot room")
	}
	allowed := parseInt64CSV(current.RoomExt[roomExtRobotUserIDsKey])
	for _, userID := range runtimeRobotUserIDs {
		if userID > 0 {
			allowed[userID] = true
		}
	}
	if !allowed[senderUserID] {
		return xerr.New(xerr.PermissionDenied, "robot sender is not allowed")
	}
	for _, targetUserID := range targetUserIDs {
		if !allowed[targetUserID] {
			return xerr.New(xerr.PermissionDenied, "robot gift target is not allowed")
		}
	}
	return nil
}

func normalizeGiftTargetType(raw string) string {
	raw = strings.TrimSpace(raw)
	switch raw {
	case "", "user", "all_mic", "all_room", "couple":
		return raw
	default:
		return raw
	}
}
