package service

import (
	"context"
	"fmt"
	"strings"
	"time"

	activityv1 "hyapp.local/api/proto/activity/v1"
	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/tencentim"
	"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"
)

type giftFlow struct {
	s     *Service
	ctx   context.Context
	req   *roomv1.SendGiftRequest
	robot robotGiftOptions

	cmd command.SendGift

	roomMeta     RoomMeta
	rocketConfig RoomRocketConfig
	billing      *walletv1.DebitGiftResponse
	// targetBillings 保存 wallet-service 按目标结算后的账务事实；后续事件、麦位热度和 batch 展示都必须从这里读取，不能重新按客户端输入推导价格。
	targetBillings []giftTargetBilling
	walletDebitMS  int64

	settledCommand command.SendGift
	luckyEnabled   bool
	luckyGift      *roomv1.LuckyGiftDrawResult
	luckyGifts     []*roomv1.LuckyGiftDrawResult

	heatValue               int64
	targetGiftValueDeltas   map[int64]int64
	targetCurrentGiftValues map[int64]int64
	rocketApply             rocketGiftApplyResult
	commandPayload          []byte

	giftEvents                 []outbox.Record
	records                    []outbox.Record
	robotDisplayRecords        []outbox.Record
	rocketProgress             *roomRocketProgressPending
	batchDisplay               *roomv1.SendGiftBatchDisplay
	batchDisplayEnabled        bool
	suppressDirectIMEventTypes map[string]bool
}

func newGiftFlow(s *Service, ctx context.Context, req *roomv1.SendGiftRequest, options giftSendOptions) *giftFlow {
	displayMode := ""
	if options.BatchDisplay {
		displayMode = "batch"
	}
	// SendGift 的账务不在 room-service 内结算，但房间表现、热度和榜单由 Room Cell 同步更新。
	cmd := command.SendGift{
		Base:            baseFromMeta(req.GetMeta()),
		TargetType:      normalizeGiftTargetType(req.GetTargetType()),
		TargetUserIDs:   normalizeGiftTargetUserIDs(req.GetTargetUserId(), req.GetTargetUserIds()),
		GiftID:          req.GetGiftId(),
		PoolID:          strings.TrimSpace(req.GetPoolId()),
		GiftCount:       req.GetGiftCount(),
		EntitlementID:   strings.TrimSpace(req.GetEntitlementId()),
		Source:          strings.TrimSpace(req.GetSource()),
		DisplayMode:     displayMode,
		SenderCountryID: req.GetSenderCountryId(),
		SenderRegionID:  req.GetSenderRegionId(),
		TargetIsHost:    req.GetTargetIsHost(),
		// 工资区域由 gateway 从 user-service host profile 注入；房间 visible_region_id 只用于房间/礼物可见性。
		TargetHostRegionID:          req.GetTargetHostRegionId(),
		TargetAgencyOwnerUserID:     req.GetTargetAgencyOwnerUserId(),
		TargetHostScopes:            giftTargetHostScopesFromProto(req.GetTargetHostScopes()),
		SenderDisplayProfile:        giftDisplayProfileFromProto(req.GetSenderDisplayProfile()),
		TargetDisplayProfiles:       giftDisplayProfilesFromProto(req.GetTargetDisplayProfiles()),
		RobotGift:                   options.Robot.Enabled && !options.Robot.RealRoomHeat,
		RobotWalletGift:             options.Robot.Enabled,
		SyntheticLuckyGift:          options.Robot.Enabled && strings.TrimSpace(req.GetPoolId()) != "",
		SyntheticLuckyRewardCoins:   firstPositiveInt64(options.Robot.SyntheticRewardCoins, 0),
		SyntheticLuckyMultiplierPPM: firstPositiveInt64(options.Robot.SyntheticMultiplierPPM, 1000000),
	}
	if cmd.TargetType == "" {
		cmd.TargetType = "user"
	}
	if cmd.TargetType == "user" && len(cmd.TargetUserIDs) > 0 {
		cmd.TargetUserID = cmd.TargetUserIDs[0]
	}
	return &giftFlow{s: s, ctx: ctx, req: req, robot: options.Robot, cmd: cmd, settledCommand: cmd, batchDisplayEnabled: options.BatchDisplay}
}

func (f *giftFlow) validateBeforeDebit(current *state.RoomState) error {
	if _, exists := current.OnlineUsers[f.cmd.ActorUserID()]; !exists {
		// 送礼者必须在房间业务 presence 内；没有进入 Room Cell 的用户不能触发钱包扣费。
		return xerr.New(xerr.NotFound, "sender not in room")
	}
	if f.cmd.TargetType != "user" {
		// v1 HTTP 契约已预留 all_mic/all_room/couple；账务拆单和房间表现策略补齐前先 fail-close。
		return xerr.New(xerr.InvalidArgument, "target_type is unsupported")
	}
	if len(f.cmd.TargetUserIDs) == 0 || f.cmd.TargetUserID <= 0 {
		return xerr.New(xerr.InvalidArgument, "target_user_ids is required")
	}
	if f.batchDisplayEnabled && len(f.cmd.TargetUserIDs) < 2 {
		// batch-send 是多人展示入口，必须明确至少两个收礼目标；单目标继续走普通 SendGift，避免展示协议和账务事实混用。
		return xerr.New(xerr.InvalidArgument, "batch gift requires multiple targets")
	}
	for _, targetUserID := range f.cmd.TargetUserIDs {
		if _, exists := current.OnlineUsers[targetUserID]; !exists {
			// 每个接收方都必须在房间 presence 内；批量扣费前先全部校验，避免钱包出现无房间表现的入账。
			return xerr.New(xerr.NotFound, "target not in room")
		}
	}
	if f.robot.Enabled {
		if err := requireRobotGiftParticipants(current, f.cmd.ActorUserID(), f.cmd.TargetUserIDs, f.robot.RobotUserIDs, !f.robot.RealRoomHeat); err != nil {
			return err
		}
	}
	if f.cmd.GiftID == "" || f.cmd.GiftCount <= 0 {
		// 礼物 ID 和数量是钱包查服务端价格的最小输入，客户端不再提交礼物单价。
		return xerr.New(xerr.InvalidArgument, "gift_id and gift_count must be valid")
	}
	if strings.EqualFold(f.cmd.Source, "bag") && strings.TrimSpace(f.cmd.EntitlementID) == "" {
		// 背包送礼只改变扣费来源，不改变房间事件；因此必须在扣费前明确指定要扣的权益，不能让 wallet 猜测某个库存。
		return xerr.New(xerr.InvalidArgument, "entitlement_id is required for bag gift")
	}
	return nil
}

func (f *giftFlow) prepareRoomFacts() error {
	roomMeta, exists, err := f.s.repository.GetRoomMeta(f.ctx, f.cmd.RoomID())
	if err != nil {
		return err
	}
	if !exists {
		return xerr.New(xerr.NotFound, "room not found")
	}
	f.roomMeta = roomMeta
	if f.cmd.PoolID != "" && !f.cmd.SyntheticLuckyGift {
		// 客户端显式传 pool_id 时必须先检查活动规则；规则未启用就拒绝，避免先扣费再发现不能抽奖。
		if f.s.luckyGift == nil {
			return xerr.New(xerr.Unavailable, "lucky gift service is not configured")
		}
		checkResp, err := f.s.luckyGift.CheckLuckyGift(f.ctx, &activityv1.CheckLuckyGiftRequest{
			Meta:   activityMetaFromRoom(f.ctx, f.req.GetMeta()),
			UserId: f.cmd.ActorUserID(),
			RoomId: f.cmd.RoomID(),
			GiftId: f.cmd.GiftID,
			PoolId: f.cmd.PoolID,
		})
		if err != nil {
			return err
		}
		if checkResp == nil || !checkResp.GetEnabled() {
			return xerr.New(xerr.RuleNotActive, "lucky gift rule is not active")
		}
		// Check 只说明“允许按该 pool 抽奖”，真实价格、区域可用性和扣费仍以 wallet-service 为准。
		f.luckyEnabled = true
	}
	rocketConfig, err := f.s.roomRocketConfig(f.ctx)
	if err != nil {
		return err
	}
	f.rocketConfig = rocketConfig
	return nil
}

func (f *giftFlow) debitAndSettle(now time.Time) error {
	return f.s.withLuckyGiftSendLock(f.ctx, f.luckyEnabled, f.cmd.ActorUserID(), func() error {
		// 钱包扣费在房间状态变更前完成；失败时不写 command log、不进入 Room Cell 提交态。
		walletStartedAt := time.Now()
		var err error
		f.billing, f.targetBillings, err = f.s.debitGiftTargets(f.ctx, f.cmd, f.roomMeta)
		f.walletDebitMS = elapsedMS(walletStartedAt)
		if err != nil {
			return err
		}
		if f.billing == nil {
			return xerr.New(xerr.Unavailable, "wallet debit response is empty")
		}
		f.settledCommand.BillingReceiptID = f.billing.GetBillingReceiptId()
		f.settledCommand.CoinSpent = f.billing.GetCoinSpent()
		f.settledCommand.HeatValue = f.billing.GetHeatValue()
		f.settledCommand.PriceVersion = f.billing.GetPriceVersion()
		f.settledCommand.GiftTypeCode = f.billing.GetGiftTypeCode()
		f.settledCommand.HostPeriodDiamondAdded = f.billing.GetHostPeriodDiamondAdded()
		f.settledCommand.HostPeriodCycleKey = f.billing.GetHostPeriodCycleKey()
		if f.cmd.SyntheticLuckyGift {
			f.luckyGift = syntheticLuckyGiftResult(f.cmd, f.targetBillings, now)
			if f.luckyGift != nil {
				f.luckyGifts = []*roomv1.LuckyGiftDrawResult{f.luckyGift}
			}
		} else if !f.luckyEnabled {
			// 未显式传 pool_id 的旧客户端仍可通过钱包礼物类型触发默认幸运礼物逻辑；新客户端应传明确 pool_id。
			f.luckyEnabled = f.s.shouldDrawLuckyGift(f.cmd.PoolID, f.billing.GetGiftTypeCode())
		}
		if !f.cmd.SyntheticLuckyGift && f.luckyEnabled {
			f.luckyGifts, err = f.s.executeLuckyGiftDraws(f.ctx, f.req.GetMeta(), f.cmd, f.roomMeta, f.targetBillings, now)
			if err != nil {
				// 抽奖失败时整笔送礼失败并依赖 wallet 幂等重试；不能落普通礼物后丢失幸运礼物抽奖事实。
				return err
			}
			if len(f.luckyGifts) > 0 {
				// lucky_gifts 保留每个收礼目标的独立抽奖事实；lucky_gift 返回整次送礼的聚合表现，复用已有批量送礼的倍率累加语义。
				f.luckyGift = aggregateLuckyGiftResults(f.cmd.ID(), f.luckyGifts)
			}
		}
		return nil
	})
}

func (f *giftFlow) applyRoomState(now time.Time, current *state.RoomState, localRank *rank.LocalRank) error {
	f.heatValue = f.billing.GetHeatValue()
	// 扣费成功后，Room Cell 同步更新热度和本地礼物榜；这些状态才是 IM 和 snapshot 的权威来源。
	current.Heat += f.heatValue
	current.GiftRank = localRank.ApplyGift(f.cmd.ActorUserID(), f.heatValue, now)
	f.targetGiftValueDeltas = make(map[int64]int64, len(f.targetBillings))
	f.targetCurrentGiftValues = make(map[int64]int64, len(f.targetBillings))
	for _, targetBilling := range f.targetBillings {
		if targetBilling.Billing == nil || targetBilling.TargetUserID <= 0 {
			continue
		}
		targetGiftValue := targetBilling.Billing.GetHeatValue()
		if userState := current.OnlineUsers[targetBilling.TargetUserID]; userState != nil {
			// 收礼用户热度完全使用 wallet-service 已结算的 HeatValue，room-service 不重新判断普通/幸运/超级幸运比例。
			if targetGiftValue > 0 {
				userState.GiftValue += targetGiftValue
				f.targetGiftValueDeltas[targetBilling.TargetUserID] += targetGiftValue
			}
			f.targetCurrentGiftValues[targetBilling.TargetUserID] = userState.GiftValue
		}
	}
	if len(f.targetGiftValueDeltas) > 0 {
		// 命令日志只保存每个目标本次增量；累计值由在线用户态按 replay 顺序重建。
		f.settledCommand.TargetGiftValues = f.targetGiftValueDeltas
	}
	current.Version++
	rocketApply, err := f.s.applyRoomRocketGift(now, current, f.rocketConfig, f.cmd, &f.settledCommand, f.billing, f.roomMeta)
	if err != nil {
		return err
	}
	f.rocketApply = rocketApply
	commandPayload, err := command.Serialize(f.settledCommand)
	if err != nil {
		return err
	}
	f.commandPayload = commandPayload
	return nil
}

func (f *giftFlow) buildMutationResult(now time.Time, current *state.RoomState) (mutationResult, []outbox.Record, error) {
	giftEvents, err := buildRoomGiftSentRecords(current.RoomID, current.Version, now, f.cmd, f.roomMeta, f.targetBillings, f.targetCurrentGiftValues)
	if err != nil {
		return mutationResult{}, nil, err
	}
	f.giftEvents = giftEvents
	f.records = make([]outbox.Record, 0, len(f.giftEvents)+4)
	f.robotDisplayRecords = make([]outbox.Record, 0, len(f.giftEvents)+5)
	// RobotWalletGift 只表示这笔礼物由机器人钱包结算；真实房机器人仍会同步写房间贡献、火箭和麦位热度，但展示事件改为提交后直发 IM，避免机器人流量抢占真人主 outbox。
	robotDisplayGift := f.cmd.RobotWalletGift
	if robotDisplayGift {
		f.robotDisplayRecords = append(f.robotDisplayRecords, f.giftEvents...)
	} else {
		f.records = append(f.records, f.giftEvents...)
	}
	heatEvent, err := buildRoomHeatChangedRecord(current.RoomID, current.Version, now, f.heatValue, current.Heat)
	if err != nil {
		return mutationResult{}, nil, err
	}
	rankItem := findRankItem(current.GiftRank, f.cmd.ActorUserID())
	rankEvent, err := buildRoomRankChangedRecord(current.RoomID, current.Version, now, rankItem)
	if err != nil {
		return mutationResult{}, nil, err
	}
	if robotDisplayGift {
		// 房间热度和麦位收礼热度仍要给客户端展示；是否真实计入统计由 RobotGift 语义单独控制，不能再拿投递通道反推业务真实性。
		f.robotDisplayRecords = append(f.robotDisplayRecords, heatEvent, rankEvent)
	} else {
		f.records = append(f.records, heatEvent, rankEvent)
	}
	if !f.cmd.RobotGift {
		// 真实房间礼物才广播火箭进度；真人房机器人送礼使用机器人钱包结算，但仍是真实房间贡献，所以火箭事件跟随机器人展示直发通道投递。
		if f.rocketApply.progressEvent != nil {
			progressRecord, err := outbox.Build(current.RoomID, "RoomRocketFuelChanged", current.Version, now, f.rocketApply.progressEvent)
			if err != nil {
				return mutationResult{}, nil, err
			}
			progressRecord.AppCode = appcode.FromContext(f.ctx)
			if progressRecord.Envelope != nil {
				progressRecord.Envelope.AppCode = progressRecord.AppCode
			}
			// 燃料变化是高频展示事件，不逐笔写 outbox；命令提交成功后由内存合并器按 1 秒窗口只落最大进度。
			f.rocketProgress = &roomRocketProgressPending{
				record:      progressRecord,
				robotLane:   robotDisplayGift,
				rocketID:    f.rocketApply.progressEvent.GetRocketId(),
				level:       f.rocketApply.progressEvent.GetLevel(),
				currentFuel: f.rocketApply.progressEvent.GetCurrentFuel(),
				roomVersion: current.Version,
			}
		}
		if f.rocketApply.ignited != nil {
			ignitedEvent, err := outbox.Build(current.RoomID, "RoomRocketIgnited", current.Version, now, f.rocketApply.ignited)
			if err != nil {
				return mutationResult{}, nil, err
			}
			if robotDisplayGift {
				f.robotDisplayRecords = append(f.robotDisplayRecords, ignitedEvent)
			} else {
				f.records = append(f.records, ignitedEvent)
			}
		}
	}
	if f.cmd.SyntheticLuckyGift && f.luckyGift != nil {
		drawEvent, err := outbox.Build(current.RoomID, "RoomRobotLuckyGiftDrawn", current.Version, now, &roomeventsv1.RoomRobotLuckyGiftDrawn{
			DrawId:                    f.luckyGift.GetDrawId(),
			CommandId:                 f.cmd.ID(),
			SenderUserId:              f.cmd.ActorUserID(),
			TargetUserId:              f.cmd.TargetUserID,
			GiftId:                    f.cmd.GiftID,
			GiftCount:                 f.cmd.GiftCount,
			PoolId:                    f.cmd.PoolID,
			MultiplierPpm:             f.luckyGift.GetMultiplierPpm(),
			BaseRewardCoins:           f.luckyGift.GetBaseRewardCoins(),
			RoomAtmosphereRewardCoins: f.luckyGift.GetRoomAtmosphereRewardCoins(),
			ActivitySubsidyCoins:      f.luckyGift.GetActivitySubsidyCoins(),
			EffectiveRewardCoins:      f.luckyGift.GetEffectiveRewardCoins(),
			CreatedAtMs:               f.luckyGift.GetCreatedAtMs(),
			VisibleRegionId:           f.roomMeta.VisibleRegionID,
			IsRobot:                   true,
			Synthetic:                 true,
		})
		if err != nil {
			return mutationResult{}, nil, err
		}
		if robotDisplayGift {
			f.robotDisplayRecords = append(f.robotDisplayRecords, drawEvent)
		} else {
			f.records = append(f.records, drawEvent)
		}
	}
	if f.batchDisplayEnabled {
		// 新 batch-send 只合并房间展示消息：逐目标 RoomGiftSent 已经在 giftEvents 中保留为 durable 业务事实。
		f.batchDisplay = buildSendGiftBatchDisplay(f.cmd, f.billing, f.targetBillings, f.targetCurrentGiftValues, f.luckyGifts, current.Heat)
		batchEvent := roomGiftBatchSentEventFromDisplay(f.batchDisplay, f.roomMeta.VisibleRegionID)
		batchRecord, err := outbox.Build(current.RoomID, "RoomGiftBatchSent", current.Version, now, batchEvent)
		if err != nil {
			return mutationResult{}, nil, err
		}
		f.batchDisplay.EventId = batchRecord.EventID
		f.records = append(f.records, batchRecord)
		f.suppressDirectIMEventTypes = map[string]bool{"RoomGiftSent": true}
	}
	return f.toMutationResult(now, current), f.records, nil
}

func (f *giftFlow) toMutationResult(now time.Time, current *state.RoomState) mutationResult {
	finalCoinBalanceAfter := giftFinalCoinBalanceAfter(f.billing)
	firstTargetDisplayProfile := giftDisplayProfileForUser(f.cmd.TargetDisplayProfiles, f.cmd.TargetUserID)
	result := mutationResult{
		snapshot:                   current.ToProto(),
		billingReceiptID:           f.billing.GetBillingReceiptId(),
		coinBalanceAfter:           finalCoinBalanceAfter,
		giftIncomeBalanceAfter:     f.billing.GetGiftIncomeBalanceAfter(),
		roomHeat:                   current.Heat,
		giftRank:                   cloneProtoRank(current.GiftRank),
		luckyGift:                  f.luckyGift,
		luckyGifts:                 f.luckyGifts,
		batchDisplay:               f.batchDisplay,
		suppressDirectIMEventTypes: f.suppressDirectIMEventTypes,
		commandPayload:             f.commandPayload,
		walletDebitMS:              f.walletDebitMS,
		robotDisplayRecords:        f.robotDisplayRecords,
		roomRocketProgress:         f.rocketProgress,
		roomGiftLeaderboard: &RoomGiftLeaderboardIncrement{
			AppCode:      appcode.FromContext(f.ctx),
			RoomID:       current.RoomID,
			CoinSpent:    roomGiftLeaderboardValue(f.billing),
			OccurredAtMS: now.UnixMilli(),
		},
		roomUserGiftStats: []RoomUserGiftStatIncrement{
			{
				AppCode:      appcode.FromContext(f.ctx),
				RoomID:       current.RoomID,
				UserID:       f.cmd.ActorUserID(),
				GiftValue:    f.heatValue,
				LastGiftAtMS: now.UnixMilli(),
			},
		},
		roomWeeklyContribution: &RoomWeeklyContributionIncrement{
			AppCode:    appcode.FromContext(f.ctx),
			RoomID:     current.RoomID,
			GiftValue:  f.heatValue,
			OccurredMS: now.UnixMilli(),
		},
		syncEvent: &tencentim.RoomEvent{
			// 同步广播只选 GiftSent 主事件作为客户端房间系统消息，Heat/Rank 通过字段携带。
			EventID:      f.giftEvents[0].EventID,
			RoomID:       current.RoomID,
			EventType:    "room_gift_sent",
			ActorUserID:  f.cmd.ActorUserID(),
			TargetUserID: f.cmd.TargetUserID,
			GiftValue:    f.heatValue,
			RoomHeat:     current.Heat,
			RoomVersion:  current.Version,
			Attributes: map[string]string{
				"gift_id":                         f.cmd.GiftID,
				"pool_id":                         f.cmd.PoolID,
				"gift_count":                      fmt.Sprintf("%d", f.cmd.GiftCount),
				"billing_receipt_id":              f.billing.GetBillingReceiptId(),
				"coin_spent":                      fmt.Sprintf("%d", f.settledCommand.CoinSpent),
				"target_count":                    fmt.Sprintf("%d", len(f.cmd.TargetUserIDs)),
				"target_user_ids":                 giftTargetUserIDsAttribute(f.cmd.TargetUserIDs),
				"price_version":                   f.settledCommand.PriceVersion,
				"host_period_diamond_added":       fmt.Sprintf("%d", f.settledCommand.HostPeriodDiamondAdded),
				"host_period_cycle_key":           f.settledCommand.HostPeriodCycleKey,
				"gift_type_code":                  f.billing.GetGiftTypeCode(),
				"cp_relation_type":                f.billing.GetCpRelationType(),
				"gift_name":                       f.billing.GetGiftName(),
				"gift_icon_url":                   f.billing.GetGiftIconUrl(),
				"gift_animation_url":              f.billing.GetGiftAnimationUrl(),
				"target_gift_value":               fmt.Sprintf("%d", f.targetCurrentGiftValues[f.cmd.TargetUserID]),
				"is_robot_gift":                   fmt.Sprintf("%t", f.cmd.RobotWalletGift),
				"synthetic_lucky_gift":            fmt.Sprintf("%t", f.cmd.SyntheticLuckyGift),
				"sender_name":                     giftDisplayName(f.cmd.SenderDisplayProfile),
				"sender_avatar":                   strings.TrimSpace(f.cmd.SenderDisplayProfile.Avatar),
				"sender_display_user_id":          strings.TrimSpace(f.cmd.SenderDisplayProfile.DisplayUserID),
				"sender_pretty_display_user_id":   strings.TrimSpace(f.cmd.SenderDisplayProfile.PrettyDisplayUserID),
				"receiver_nickname":               giftDisplayName(firstTargetDisplayProfile),
				"receiver_avatar":                 strings.TrimSpace(firstTargetDisplayProfile.Avatar),
				"receiver_display_user_id":        strings.TrimSpace(firstTargetDisplayProfile.DisplayUserID),
				"receiver_pretty_display_user_id": strings.TrimSpace(firstTargetDisplayProfile.PrettyDisplayUserID),
			},
		},
	}
	if f.cmd.RobotGift {
		// 机器人房间可以增加房间热度和麦位收礼热度，但不进入跨房礼物榜和房间送礼统计。
		result.roomGiftLeaderboard = nil
		result.roomUserGiftStats = nil
		result.roomWeeklyContribution = nil
	}
	return result
}
