package service

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"strings"
	"time"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	"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"
)

// MicUp 把目标用户放到指定麦位。
func (s *Service) MicUp(ctx context.Context, req *roomv1.MicUpRequest) (*roomv1.MicUpResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	// MicUp 以 actor 作为上麦目标，管理员代操作走 MicDown/ChangeMicSeat 等显式 target 命令。
	cmd := command.MicUp{
		Base:                 baseFromMeta(req.GetMeta()),
		SeatNo:               req.GetSeatNo(),
		TargetDisplayProfile: userDisplayProfileFromProto(req.GetTargetDisplayProfile()),
	}
	cmd.MicSessionID = micSessionIDForCommand(cmd.RoomID(), cmd.ID())
	cmd.PublishDeadlineMS = s.clock.Now().Add(s.micPublishTimeout).UnixMilli()

	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 _, exists := current.OnlineUsers[cmd.ActorUserID()]; !exists {
			// 不在 room-service presence 的用户不能上麦。
			return mutationResult{}, nil, xerr.New(xerr.NotFound, "user not in room")
		}
		if current.MuteUsers[cmd.ActorUserID()] {
			// 禁言同时约束公屏和新上麦；校验必须落在 Room Cell 命令内，不能依赖客户端收到事件后自行拦截。
			return mutationResult{}, nil, xerr.New(xerr.RoomUserMuted, "user is muted")
		}

		if seat, exists := current.SeatByUser(cmd.ActorUserID()); exists {
			// MicUp 是客户端“确保自己在麦上”的入口；重复点击或网络重试如果换了 command_id，
			// 当前业务目标已经满足，直接返回现有麦位会话，避免把首次成功覆盖成用户可见失败。
			// 真正换麦必须继续走 ChangeMicSeat，不能让重复 MicUp 隐式迁移麦位。
			return mutationResult{
				snapshot:          current.ToProto(),
				seatNo:            seat.SeatNo,
				micSessionID:      seat.MicSessionID,
				publishDeadlineMS: seat.PublishDeadlineMS,
			}, nil, nil
		}

		index := current.SeatIndex(cmd.SeatNo)
		if index < 0 {
			// 麦位编号必须来自房间初始化的 MicSeats。
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "seat does not exist")
		}

		if current.MicSeats[index].Locked {
			// 锁定麦位不能被普通上麦命令占用。
			return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "seat is locked")
		}

		if current.MicSeats[index].UserID != 0 {
			// 非空麦位不能被覆盖，避免两个用户同时占同一麦位。
			return mutationResult{}, nil, xerr.New(xerr.Conflict, "seat is occupied")
		}

		// MicUp 只代表业务占麦成功；RTC 发流必须再通过 ConfirmMicPublishing 确认。
		current.Version++
		current.MicSeats[index].UserID = cmd.ActorUserID()
		current.MicSeats[index].PublishState = state.MicPublishPending
		current.MicSeats[index].MicSessionID = cmd.MicSessionID
		current.MicSeats[index].PublishDeadlineMS = cmd.PublishDeadlineMS
		current.MicSeats[index].MicSessionRoomVersion = current.Version
		// 新 session 尚未接受任何 RTC/客户端事件；不能用服务端 MicUp 时间初始化，
		// 否则客户端设备时间略慢时，合法的首次确认会被误判为旧事件。
		current.MicSeats[index].LastPublishEventTimeMS = 0
		current.MicSeats[index].MicHeartbeatAtMS = 0
		current.MicSeats[index].MicMuted = false
		seatStatus := current.MicSeats[index].SeatStatus()
		targetGiftValue := roomUserGiftValue(current, cmd.ActorUserID())

		// RoomMicChanged 是房间外消费者和腾讯云 IM 共同使用的麦位事件源；展示快照固化在事件体，direct IM 和 outbox 补偿共用。
		micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
			ActorUserId:       cmd.ActorUserID(),
			TargetUserId:      cmd.ActorUserID(),
			FromSeat:          0,
			ToSeat:            cmd.SeatNo,
			Action:            "up",
			MicSessionId:      cmd.MicSessionID,
			PublishState:      state.MicPublishPending,
			PublishDeadlineMs: cmd.PublishDeadlineMS,
			SeatStatus:        seatStatus,
			TargetGiftValue:   targetGiftValue,
		}, cmd.TargetDisplayProfile)
		micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, micChanged)
		if err != nil {
			return mutationResult{}, nil, err
		}

		return mutationResult{
			snapshot:          current.ToProto(),
			seatNo:            cmd.SeatNo,
			micSessionID:      cmd.MicSessionID,
			publishDeadlineMS: cmd.PublishDeadlineMS,
			syncEvent: &tencentim.RoomEvent{
				EventID:      micEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_mic_up",
				ActorUserID:  tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				TargetUserID: tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				SeatNo:       cmd.SeatNo,
				RoomVersion:  current.Version,
				Attributes:   roomMicChangedIMAttributes(micChanged),
			},
		}, []outbox.Record{micEvent}, nil
	})
	if err != nil {
		return nil, err
	}
	if result.micSessionID == "" {
		// 幂等重试不会重新执行 mutate，需要从当前快照补回首次 MicUp 生成的会话信息。
		if seat := protoSeatByUser(result.snapshot, cmd.ActorUserID()); seat != nil {
			result.seatNo = seat.GetSeatNo()
			result.micSessionID = seat.GetMicSessionId()
			result.publishDeadlineMS = seat.GetPublishDeadlineMs()
		}
	}

	return &roomv1.MicUpResponse{
		Result:            commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		SeatNo:            result.seatNo,
		Room:              result.snapshot,
		MicSessionId:      result.micSessionID,
		PublishDeadlineMs: result.publishDeadlineMS,
	}, nil
}

// MicDown 把目标用户从当前麦位移下。
func (s *Service) MicDown(ctx context.Context, req *roomv1.MicDownRequest) (*roomv1.MicDownResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	// MicDown 支持操作者和目标用户不同，权限策略后续可在命令入口扩展。
	cmd := command.MicDown{
		Base:                 baseFromMeta(req.GetMeta()),
		TargetUserID:         req.GetTargetUserId(),
		Reason:               req.GetReason(),
		TargetDisplayProfile: userDisplayProfileFromProto(req.GetTargetDisplayProfile()),
	}
	if cmd.TargetUserID == 0 {
		// target 缺省时按主动下麦处理；管理别人下麦必须显式传 target_user_id。
		cmd.TargetUserID = cmd.ActorUserID()
	}

	result, err := s.micDown(ctx, cmd)
	if err != nil {
		return nil, err
	}

	return &roomv1.MicDownResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		SeatNo: result.seatNo,
		Room:   result.snapshot,
	}, nil
}

func (s *Service) micDown(ctx context.Context, cmd command.MicDown) (mutationResult, error) {
	return 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 cmd.TargetUserID <= 0 {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
		}
		if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
			return mutationResult{}, nil, err
		}
		if err := canSelfOrManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
			return mutationResult{}, nil, err
		}

		seat, exists := current.SeatByUser(cmd.TargetUserID)
		if !exists {
			// 目标用户不在麦上时不能生成下麦事件。
			return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not on seat")
		}

		if cmd.MicSessionID != "" && seat.MicSessionID != cmd.MicSessionID {
			// 超时任务携带旧 mic_session_id 时只能成为 no-op，不能清掉用户新的上麦会话。
			return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
		}

		// SeatByUser 返回的是值拷贝，真正修改需要再定位切片下标。
		index := current.SeatIndex(seat.SeatNo)
		micSessionID := current.MicSeats[index].MicSessionID
		current.ClearMicSession(index)
		current.Version++
		seatStatus := current.MicSeats[index].SeatStatus()

		micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
			ActorUserId:  cmd.ActorUserID(),
			TargetUserId: cmd.TargetUserID,
			FromSeat:     seat.SeatNo,
			ToSeat:       0,
			Action:       "down",
			MicSessionId: micSessionID,
			PublishState: state.MicPublishIdle,
			Reason:       cmd.Reason,
			SeatStatus:   seatStatus,
		}, cmd.TargetDisplayProfile)
		micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, micChanged)
		if err != nil {
			return mutationResult{}, nil, err
		}

		return mutationResult{
			snapshot: current.ToProto(),
			seatNo:   seat.SeatNo,
			syncEvent: &tencentim.RoomEvent{
				EventID:      micEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_mic_down",
				ActorUserID:  tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				TargetUserID: tencentim.FormatOptionalUserID(cmd.TargetUserID),
				SeatNo:       seat.SeatNo,
				RoomVersion:  current.Version,
				Attributes:   roomMicChangedIMAttributes(micChanged),
			},
		}, []outbox.Record{micEvent}, nil
	})
}

// ChangeMicSeat 调整指定用户所在麦位。
func (s *Service) ChangeMicSeat(ctx context.Context, req *roomv1.ChangeMicSeatRequest) (*roomv1.ChangeMicSeatResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	// ChangeMicSeat 是原子换位：从原麦位清空和写入新麦位必须同版本完成。
	cmd := command.ChangeMicSeat{
		Base:                 baseFromMeta(req.GetMeta()),
		TargetUserID:         req.GetTargetUserId(),
		SeatNo:               req.GetSeatNo(),
		TargetDisplayProfile: userDisplayProfileFromProto(req.GetTargetDisplayProfile()),
	}

	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 cmd.TargetUserID <= 0 {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
		}
		if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
			return mutationResult{}, nil, err
		}
		if err := canSelfOrManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
			return mutationResult{}, nil, err
		}

		fromSeat, exists := current.SeatByUser(cmd.TargetUserID)
		if !exists {
			// 未上麦用户没有可换出的源麦位。
			return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not on seat")
		}

		toIndex := current.SeatIndex(cmd.SeatNo)
		if toIndex < 0 {
			// 目标麦位必须存在。
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "seat does not exist")
		}

		if current.MicSeats[toIndex].Locked {
			// 锁定麦位不能换入。
			return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "seat is locked")
		}

		if current.MicSeats[toIndex].UserID != 0 {
			// 目标麦位被占用时拒绝换位，避免覆盖另一个用户。
			return mutationResult{}, nil, xerr.New(xerr.Conflict, "seat is occupied")
		}

		// 清空源麦位和占用目标麦位在同一 Cell 任务内完成，客户端不会看到中间态。
		fromIndex := current.SeatIndex(fromSeat.SeatNo)
		movedSeat := current.MicSeats[fromIndex]
		current.MicSeats[fromIndex].UserID = 0
		current.MicSeats[fromIndex].PublishState = ""
		current.MicSeats[fromIndex].MicSessionID = ""
		current.MicSeats[fromIndex].PublishDeadlineMS = 0
		current.MicSeats[fromIndex].MicSessionRoomVersion = 0
		current.MicSeats[fromIndex].LastPublishEventTimeMS = 0
		current.MicSeats[fromIndex].MicHeartbeatAtMS = 0
		current.MicSeats[fromIndex].MicMuted = false
		current.MicSeats[toIndex].UserID = cmd.TargetUserID
		current.MicSeats[toIndex].PublishState = movedSeat.PublishState
		current.MicSeats[toIndex].MicSessionID = movedSeat.MicSessionID
		current.MicSeats[toIndex].PublishDeadlineMS = movedSeat.PublishDeadlineMS
		current.MicSeats[toIndex].MicSessionRoomVersion = movedSeat.MicSessionRoomVersion
		current.MicSeats[toIndex].LastPublishEventTimeMS = movedSeat.LastPublishEventTimeMS
		current.MicSeats[toIndex].MicHeartbeatAtMS = movedSeat.MicHeartbeatAtMS
		current.MicSeats[toIndex].MicMuted = movedSeat.MicMuted
		current.Version++
		seatStatus := current.MicSeats[toIndex].SeatStatus()
		targetGiftValue := roomUserGiftValue(current, cmd.TargetUserID)

		micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
			ActorUserId:       cmd.ActorUserID(),
			TargetUserId:      cmd.TargetUserID,
			FromSeat:          fromSeat.SeatNo,
			ToSeat:            cmd.SeatNo,
			Action:            "change",
			MicSessionId:      movedSeat.MicSessionID,
			PublishState:      movedSeat.PublishState,
			PublishDeadlineMs: movedSeat.PublishDeadlineMS,
			SeatStatus:        seatStatus,
			TargetGiftValue:   targetGiftValue,
		}, cmd.TargetDisplayProfile)
		micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, micChanged)
		if err != nil {
			return mutationResult{}, nil, err
		}

		return mutationResult{
			snapshot: current.ToProto(),
			syncEvent: &tencentim.RoomEvent{
				EventID:      micEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_mic_change",
				ActorUserID:  tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				TargetUserID: tencentim.FormatOptionalUserID(cmd.TargetUserID),
				SeatNo:       cmd.SeatNo,
				RoomVersion:  current.Version,
				Attributes:   roomMicChangedIMAttributes(micChanged),
			},
			directIMRecords: []outbox.Record{micEvent},
		}, nil, nil
	})
	if err != nil {
		return nil, err
	}

	return &roomv1.ChangeMicSeatResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		Room:   result.snapshot,
	}, nil
}

// ConfirmMicPublishing 把 pending_publish 麦位推进到 publishing。
func (s *Service) ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmMicPublishingRequest) (*roomv1.ConfirmMicPublishingResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	cmd := command.ConfirmMicPublishing{
		Base:                 baseFromMeta(req.GetMeta()),
		TargetUserID:         req.GetTargetUserId(),
		MicSessionID:         req.GetMicSessionId(),
		RoomVersion:          req.GetRoomVersion(),
		EventTimeMS:          req.GetEventTimeMs(),
		Source:               req.GetSource(),
		TargetDisplayProfile: userDisplayProfileFromProto(req.GetTargetDisplayProfile()),
	}
	if cmd.TargetUserID == 0 {
		cmd.TargetUserID = cmd.ActorUserID()
	}

	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 cmd.TargetUserID <= 0 || cmd.MicSessionID == "" || cmd.RoomVersion <= 0 || cmd.EventTimeMS <= 0 {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "mic publish confirmation is incomplete")
		}
		if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
			return mutationResult{}, nil, err
		}
		if cmd.ActorUserID() != cmd.TargetUserID {
			if err := canManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
				return mutationResult{}, nil, err
			}
		}
		if current.MuteUsers[cmd.TargetUserID] {
			// MicUp 后到 RTC 发流确认之间仍可能发生房间禁言；确认入口必须再次校验，不能复活已撤销的发言权限。
			return mutationResult{}, nil, xerr.New(xerr.RoomUserMuted, "user is muted")
		}

		seat, exists := current.SeatByUser(cmd.TargetUserID)
		if !exists {
			// RTC webhook 可能晚于用户下麦到达；旧事件只忽略，不返回错误清掉新状态。
			return mutationResult{snapshot: current.ToProto()}, nil, nil
		}
		receivedAtMS := micPublishConfirmReceivedAtMS(seat, cmd, s.micPublishTimeout, now)
		if !micPublishConfirmationMatchesCurrentSession(seat, cmd, receivedAtMS) {
			return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
		}
		if seat.PublishState == state.MicPublishPublishing {
			return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
		}
		if seat.PublishState != state.MicPublishPending {
			return mutationResult{}, nil, xerr.New(xerr.Conflict, "mic is not waiting for publish confirmation")
		}

		acceptedCmd := cmd
		acceptedCmd.AcceptedEventTimeMS = receivedAtMS
		commandPayload, err := command.Serialize(acceptedCmd)
		if err != nil {
			return mutationResult{}, nil, err
		}

		index := current.SeatIndex(seat.SeatNo)
		current.MicSeats[index].PublishState = state.MicPublishPublishing
		current.MicSeats[index].LastPublishEventTimeMS = receivedAtMS
		// 确认发流本身代表当前会话刚被服务端接受，心跳起点使用同一个 accepted time，保证 command log 回放一致。
		current.MicSeats[index].MicHeartbeatAtMS = receivedAtMS
		if micPublishConfirmSourceIsMuted(cmd.Source) {
			current.MicSeats[index].MicMuted = true
		}
		current.Version++
		seatStatus := current.MicSeats[index].SeatStatus()
		targetGiftValue := roomUserGiftValue(current, cmd.TargetUserID)

		micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
			ActorUserId:        cmd.ActorUserID(),
			TargetUserId:       cmd.TargetUserID,
			FromSeat:           seat.SeatNo,
			ToSeat:             seat.SeatNo,
			Action:             "publish_confirmed",
			MicSessionId:       cmd.MicSessionID,
			PublishState:       state.MicPublishPublishing,
			PublishDeadlineMs:  seat.PublishDeadlineMS,
			PublishEventTimeMs: receivedAtMS,
			MicMuted:           current.MicSeats[index].MicMuted,
			SeatStatus:         seatStatus,
			TargetGiftValue:    targetGiftValue,
		}, cmd.TargetDisplayProfile)
		micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, micChanged)
		if err != nil {
			return mutationResult{}, nil, err
		}
		attributes := roomMicChangedIMAttributes(micChanged)
		if strings.TrimSpace(cmd.Source) != "" {
			attributes["source"] = strings.TrimSpace(cmd.Source)
		}

		return mutationResult{
			snapshot:          current.ToProto(),
			seatNo:            seat.SeatNo,
			micSessionID:      cmd.MicSessionID,
			publishDeadlineMS: seat.PublishDeadlineMS,
			micHeartbeatAtMS:  current.MicSeats[index].MicHeartbeatAtMS,
			commandPayload:    commandPayload,
			syncEvent: &tencentim.RoomEvent{
				EventID:      micEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_mic_publish_confirmed",
				ActorUserID:  tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				TargetUserID: tencentim.FormatOptionalUserID(cmd.TargetUserID),
				SeatNo:       seat.SeatNo,
				RoomVersion:  current.Version,
				Attributes:   attributes,
			},
		}, []outbox.Record{micEvent}, nil
	})
	if err != nil {
		return nil, err
	}

	return &roomv1.ConfirmMicPublishingResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		SeatNo: result.seatNo,
		Room:   result.snapshot,
	}, nil
}

func micPublishConfirmationMatchesCurrentSession(seat state.MicSeat, cmd command.ConfirmMicPublishing, arrivedAtMS int64) bool {
	if seat.MicSessionID != cmd.MicSessionID || cmd.RoomVersion < seat.MicSessionRoomVersion {
		// session_id 和创建 session 的房间版本共同防止快速下麦/重上麦后旧确认误推进新会话。
		return false
	}
	if cmd.EventTimeMS <= seat.LastPublishEventTimeMS {
		// event_time_ms 只负责同一会话内的事件顺序，不能拿它判断客户端请求是否真的超时。
		return false
	}
	if seat.PublishDeadlineMS <= 0 || arrivedAtMS >= seat.PublishDeadlineMS {
		// deadline 是服务端创建的等待窗口：客户端时钟可能快于服务端，只有服务端到达时间越界才算真实超时。
		return false
	}

	return true
}

func micPublishConfirmReceivedAtMS(seat state.MicSeat, cmd command.ConfirmMicPublishing, timeout time.Duration, now time.Time) int64 {
	receivedAtMS := cmd.SentAtMS
	if receivedAtMS <= 0 {
		return now.UnixMilli()
	}
	if createdAtMS := rtcMicSessionCreatedAtMS(seat, timeout); createdAtMS > 0 && receivedAtMS < createdAtMS {
		// sent_at_ms 正常来自 gateway 服务端时间；若测试夹具或异常入口给了早于会话创建的时间，
		// 不能把它当成本次确认时间，否则恢复后 LastPublishEventTimeMS 会落到会话创建前。
		return now.UnixMilli()
	}
	return receivedAtMS
}

func confirmMicPublishingAcceptedEventTimeMS(cmd *command.ConfirmMicPublishing) int64 {
	if cmd == nil {
		return 0
	}
	if cmd.AcceptedEventTimeMS > 0 {
		return cmd.AcceptedEventTimeMS
	}
	return cmd.EventTimeMS
}

// MicHeartbeat 刷新当前 publishing 麦位会话的服务端心跳时间。
func (s *Service) MicHeartbeat(ctx context.Context, req *roomv1.MicHeartbeatRequest) (*roomv1.MicHeartbeatResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	cmd := command.MicHeartbeat{
		Base:         baseFromMeta(req.GetMeta()),
		TargetUserID: req.GetTargetUserId(),
		MicSessionID: req.GetMicSessionId(),
	}
	if cmd.TargetUserID == 0 {
		cmd.TargetUserID = cmd.ActorUserID()
	}

	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 cmd.TargetUserID <= 0 || cmd.MicSessionID == "" {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "mic heartbeat is incomplete")
		}
		if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
			return mutationResult{}, nil, err
		}
		if cmd.ActorUserID() != cmd.TargetUserID {
			if err := canManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
				return mutationResult{}, nil, err
			}
		}

		seat, exists := current.SeatByUser(cmd.TargetUserID)
		if !exists {
			return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not on seat")
		}
		if seat.MicSessionID != cmd.MicSessionID {
			// 旧客户端或旧 RTC 会话的心跳不能续住新麦位会话。
			return mutationResult{}, nil, xerr.New(xerr.Conflict, "mic session mismatch")
		}
		if seat.PublishState != state.MicPublishPublishing {
			// pending_publish 还没有有效发流，只能先走 ConfirmMicPublishing，不能靠心跳开始计时。
			return mutationResult{}, nil, xerr.New(xerr.Conflict, "mic is not publishing")
		}

		heartbeatAtMS := now.UnixMilli()
		if existing, ok := current.OnlineUsers[cmd.TargetUserID]; ok {
			// 麦上心跳也证明用户仍处于房间页，顺手刷新通用 presence，避免客户端同时打两个心跳。
			existing.LastSeenAtMS = heartbeatAtMS
		}
		index := current.SeatIndex(seat.SeatNo)
		current.MicSeats[index].MicHeartbeatAtMS = heartbeatAtMS
		current.Version++

		return mutationResult{
			snapshot:         current.ToProto(),
			seatNo:           seat.SeatNo,
			micHeartbeatAtMS: heartbeatAtMS,
		}, nil, nil
	})
	if err != nil {
		return nil, err
	}
	if result.micHeartbeatAtMS == 0 {
		result.seatNo, result.micHeartbeatAtMS = micHeartbeatFromSnapshot(result.snapshot, cmd.TargetUserID, cmd.MicSessionID)
	}

	return &roomv1.MicHeartbeatResponse{
		Result:           commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		SeatNo:           result.seatNo,
		Room:             result.snapshot,
		MicHeartbeatAtMs: result.micHeartbeatAtMS,
	}, nil
}

// SetMicMute 修改麦位上的服务端可见静音态。
func (s *Service) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	cmd := command.SetMicMute{
		Base:                 baseFromMeta(req.GetMeta()),
		TargetUserID:         req.GetTargetUserId(),
		Muted:                req.GetMuted(),
		TargetDisplayProfile: userDisplayProfileFromProto(req.GetTargetDisplayProfile()),
	}
	if cmd.TargetUserID == 0 {
		cmd.TargetUserID = cmd.ActorUserID()
	}

	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 cmd.TargetUserID <= 0 {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
		}
		if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
			return mutationResult{}, nil, err
		}
		if err := canSelfOrManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
			return mutationResult{}, nil, err
		}

		seat, exists := current.SeatByUser(cmd.TargetUserID)
		if !exists {
			return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not on seat")
		}
		if !cmd.Muted && current.MuteUsers[cmd.TargetUserID] {
			// 房间禁言优先级高于用户自己的麦克风开关；解除房间禁言前禁止目标或管理员把 RTC 麦位重新打开。
			return mutationResult{}, nil, xerr.New(xerr.RoomUserMuted, "user is muted")
		}
		if seat.MicMuted == cmd.Muted {
			return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
		}

		index := current.SeatIndex(seat.SeatNo)
		current.MicSeats[index].MicMuted = cmd.Muted
		current.Version++
		seatStatus := current.MicSeats[index].SeatStatus()
		targetGiftValue := roomUserGiftValue(current, cmd.TargetUserID)

		action := "mic_unmute"
		if cmd.Muted {
			action = "mic_mute"
		}
		micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
			ActorUserId:       cmd.ActorUserID(),
			TargetUserId:      cmd.TargetUserID,
			FromSeat:          seat.SeatNo,
			ToSeat:            seat.SeatNo,
			Action:            action,
			MicSessionId:      seat.MicSessionID,
			PublishState:      seat.PublishState,
			PublishDeadlineMs: seat.PublishDeadlineMS,
			MicMuted:          cmd.Muted,
			SeatStatus:        seatStatus,
			TargetGiftValue:   targetGiftValue,
		}, cmd.TargetDisplayProfile)
		micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, micChanged)
		if err != nil {
			return mutationResult{}, nil, err
		}

		return mutationResult{
			snapshot: current.ToProto(),
			seatNo:   seat.SeatNo,
			syncEvent: &tencentim.RoomEvent{
				EventID:      micEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_mic_mute_changed",
				ActorUserID:  tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				TargetUserID: tencentim.FormatOptionalUserID(cmd.TargetUserID),
				SeatNo:       seat.SeatNo,
				RoomVersion:  current.Version,
				Attributes:   roomMicChangedIMAttributes(micChanged),
			},
		}, []outbox.Record{micEvent}, nil
	})
	if err != nil {
		return nil, err
	}

	return &roomv1.SetMicMuteResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		SeatNo: result.seatNo,
		Room:   result.snapshot,
	}, nil
}

func micSessionIDForCommand(roomID string, commandID string) string {
	sum := sha256.Sum256([]byte(roomID + "\x00" + commandID))
	return "mic_" + hex.EncodeToString(sum[:16])
}

func micPublishConfirmSourceIsMuted(source string) bool {
	switch strings.ToLower(strings.TrimSpace(source)) {
	case "client_muted", "client_start_muted":
		return true
	default:
		return false
	}
}

func micHeartbeatFromSnapshot(snapshot *roomv1.RoomSnapshot, userID int64, micSessionID string) (int32, int64) {
	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() == userID && seat.GetMicSessionId() == micSessionID {
			return seat.GetSeatNo(), seat.GetMicHeartbeatAtMs()
		}
	}
	return 0, 0
}

func protoSeatByUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.SeatState {
	if snapshot == nil {
		return nil
	}

	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() == userID {
			return seat
		}
	}

	return nil
}

func roomUserGiftValue(current *state.RoomState, userID int64) int64 {
	if current == nil || userID <= 0 {
		return 0
	}
	if userState := current.OnlineUsers[userID]; userState != nil {
		// 麦位事件只透出在线用户态中的收礼热度，避免把麦位号当成热度归属。
		return userState.GiftValue
	}
	return 0
}
