// Package grpc 将 room-service 领域服务适配成 protobuf gRPC 接口。
package grpc

import (
	"context"
	"strings"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	roomservice "hyapp/services/room-service/internal/room/service"
	"hyapp/services/room-service/internal/router"
)

// Server 把 room-service 领域实现挂到 gRPC 接口上。
type Server struct {
	// UnimplementedRoomCommandServiceServer 提供未实现 RPC 的标准 gRPC 返回。
	roomv1.UnimplementedRoomCommandServiceServer
	// UnimplementedRoomGuardServiceServer 提供守卫服务未实现 RPC 的标准 gRPC 返回。
	roomv1.UnimplementedRoomGuardServiceServer
	// UnimplementedRoomQueryServiceServer 提供查询服务未实现 RPC 的标准 gRPC 返回。
	roomv1.UnimplementedRoomQueryServiceServer

	// svc 是领域服务入口，gRPC 层不保存任何房间状态。
	svc *roomservice.Service
	// ownerForwarder 在请求命中非 owner 节点时把房间命令转到当前 owner 实例。
	ownerForwarder *ownerForwarder
}

// Option 调整 room-service gRPC 边界行为。
type Option func(*Server)

// WithOwnerForwarding 启用 room_id -> owner node -> instance addr 转发。
func WithOwnerForwarding(nodeID string, directory router.Directory, registry router.NodeRegistry, timeout time.Duration) Option {
	return func(s *Server) {
		s.ownerForwarder = newOwnerForwarder(nodeID, directory, registry, timeout)
	}
}

// NewServer 创建一个可注册到 gRPC 的 room server。
func NewServer(svc *roomservice.Service, opts ...Option) *Server {
	server := &Server{svc: svc}
	for _, opt := range opts {
		if opt != nil {
			opt(server)
		}
	}
	return server
}

// mapServiceResult 把领域错误固定转换为 gRPC status + ErrorInfo.reason。
// gateway 只认识传输层 gRPC 错误；如果这里直接透传 *xerr.Error，grpc-go 会降级成 Unknown，
// gateway 就无法恢复 INVALID_ARGUMENT/NOT_FOUND 等业务 reason，最终只能错误地返回 INTERNAL_ERROR。
func mapServiceResult[T any](result T, err error) (T, error) {
	if err != nil {
		var zero T
		return zero, xerr.ToGRPCError(err)
	}

	return result, nil
}

// CreateRoom 代理到领域服务。
func (s *Server) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest) (*roomv1.CreateRoomResponse, error) {
	// 创建房间会获取 Redis lease、写 MySQL meta/command/outbox/snapshot，并安装本地 Cell。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.CreateRoomResponse, error) {
		return client.CreateRoom(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.CreateRoom(ctx, req))
}

// UpdateRoomProfile 代理到领域服务。
func (s *Server) UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (*roomv1.UpdateRoomProfileResponse, error) {
	// 房间资料和座位数变更必须经 Room Cell 串行提交，gRPC 层不直接修改快照。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.UpdateRoomProfileResponse, error) {
		return client.UpdateRoomProfile(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.UpdateRoomProfile(ctx, req))
}

func (s *Server) SaveRoomBackground(ctx context.Context, req *roomv1.SaveRoomBackgroundRequest) (*roomv1.SaveRoomBackgroundResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.SaveRoomBackground(ctx, req))
}

func (s *Server) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBackgroundRequest) (*roomv1.SetRoomBackgroundResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SetRoomBackgroundResponse, error) {
		return client.SetRoomBackground(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.SetRoomBackground(ctx, req))
}

func (s *Server) ApplyRoomDecoration(ctx context.Context, req *roomv1.ApplyRoomDecorationRequest) (*roomv1.ApplyRoomDecorationResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.ApplyRoomDecorationResponse, error) {
		return client.ApplyRoomDecoration(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.ApplyRoomDecoration(ctx, req))
}

func (s *Server) SendRoomImage(ctx context.Context, req *roomv1.SendRoomImageRequest) (*roomv1.SendRoomImageResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SendRoomImageResponse, error) {
		return client.SendRoomImage(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.SendRoomImage(ctx, req))
}

func (s *Server) AuthorizeRoomMediaUpload(ctx context.Context, req *roomv1.AuthorizeRoomMediaUploadRequest) (*roomv1.AuthorizeRoomMediaUploadResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AuthorizeRoomMediaUpload(ctx, req))
}

func (s *Server) CompleteRoomMediaUpload(ctx context.Context, req *roomv1.CompleteRoomMediaUploadRequest) (*roomv1.CompleteRoomMediaUploadResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.CompleteRoomMediaUpload(ctx, req))
}

// JoinRoom 代理到领域服务。
func (s *Server) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
	// JoinRoom 只维护 room-service presence，真实消息连接由腾讯云 IM SDK 处理。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.JoinRoomResponse, error) {
		return client.JoinRoom(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.JoinRoom(ctx, req))
}

// RoomHeartbeat 代理到领域服务。
func (s *Server) RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRequest) (*roomv1.RoomHeartbeatResponse, error) {
	// 心跳只刷新已有业务 presence，不能隐式创建进房状态。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.RoomHeartbeatResponse, error) {
		return client.RoomHeartbeat(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.RoomHeartbeat(ctx, req))
}

// LeaveRoom 代理到领域服务。
func (s *Server) LeaveRoom(ctx context.Context, req *roomv1.LeaveRoomRequest) (*roomv1.LeaveRoomResponse, error) {
	// LeaveRoom 不直接操作腾讯云 IM 连接，只修改房间业务态。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.LeaveRoomResponse, error) {
		return client.LeaveRoom(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.LeaveRoom(ctx, req))
}

// CloseRoom 代理到领域服务。
func (s *Server) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (*roomv1.CloseRoomResponse, error) {
	// CloseRoom 统一关闭房间生命周期、清理 presence/麦位并写出系统事件。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.CloseRoomResponse, error) {
		return client.CloseRoom(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.CloseRoom(ctx, req))
}

func (s *Server) AdminUpdateRoom(ctx context.Context, req *roomv1.AdminUpdateRoomRequest) (*roomv1.AdminUpdateRoomResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.AdminUpdateRoomResponse, error) {
		return client.AdminUpdateRoom(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.AdminUpdateRoom(ctx, req))
}

func (s *Server) AdminBanRoom(ctx context.Context, req *roomv1.AdminBanRoomRequest) (*roomv1.AdminBanRoomResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.AdminBanRoomResponse, error) {
		return client.AdminBanRoom(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.AdminBanRoom(ctx, req))
}

func (s *Server) AdminUnbanRoom(ctx context.Context, req *roomv1.AdminUnbanRoomRequest) (*roomv1.AdminUnbanRoomResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.AdminUnbanRoomResponse, error) {
		return client.AdminUnbanRoom(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.AdminUnbanRoom(ctx, req))
}

func (s *Server) AdminDeleteRoom(ctx context.Context, req *roomv1.AdminDeleteRoomRequest) (*roomv1.AdminDeleteRoomResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.AdminDeleteRoomResponse, error) {
		return client.AdminDeleteRoom(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.AdminDeleteRoom(ctx, req))
}

func (s *Server) AdminUpdateRoomRocketConfig(ctx context.Context, req *roomv1.AdminUpdateRoomRocketConfigRequest) (*roomv1.AdminUpdateRoomRocketConfigResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminUpdateRoomRocketConfig(ctx, req))
}

func (s *Server) AdminUpdateRoomSeatConfig(ctx context.Context, req *roomv1.AdminUpdateRoomSeatConfigRequest) (*roomv1.AdminUpdateRoomSeatConfigResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminUpdateRoomSeatConfig(ctx, req))
}

func (s *Server) AdminUpdateHumanRoomRobotConfig(ctx context.Context, req *roomv1.AdminUpdateHumanRoomRobotConfigRequest) (*roomv1.AdminUpdateHumanRoomRobotConfigResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminUpdateHumanRoomRobotConfig(ctx, req))
}

func (s *Server) AdminCreateRoomPin(ctx context.Context, req *roomv1.AdminCreateRoomPinRequest) (*roomv1.AdminCreateRoomPinResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminCreateRoomPin(ctx, req))
}

func (s *Server) AdminCancelRoomPin(ctx context.Context, req *roomv1.AdminCancelRoomPinRequest) (*roomv1.AdminCancelRoomPinResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminCancelRoomPin(ctx, req))
}

func (s *Server) AdminCreateRobotRoom(ctx context.Context, req *roomv1.AdminCreateRobotRoomRequest) (*roomv1.AdminCreateRobotRoomResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminCreateRobotRoom(ctx, req))
}

func (s *Server) AdminSetRobotRoomStatus(ctx context.Context, req *roomv1.AdminSetRobotRoomStatusRequest) (*roomv1.AdminSetRobotRoomStatusResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminSetRobotRoomStatus(ctx, req))
}

func (s *Server) AdminRenameOwnerCountryCode(ctx context.Context, req *roomv1.AdminRenameOwnerCountryCodeRequest) (*roomv1.AdminRenameOwnerCountryCodeResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminRenameOwnerCountryCode(ctx, req))
}

// MicUp 代理到领域服务。
func (s *Server) MicUp(ctx context.Context, req *roomv1.MicUpRequest) (*roomv1.MicUpResponse, error) {
	// 麦位修改由 Room Cell 串行执行，gRPC 层不做状态判断。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.MicUpResponse, error) {
		return client.MicUp(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.MicUp(ctx, req))
}

// MicDown 代理到领域服务。
func (s *Server) MicDown(ctx context.Context, req *roomv1.MicDownRequest) (*roomv1.MicDownResponse, error) {
	// 下麦结果会进入 command log 和 outbox，失败不产生部分状态。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.MicDownResponse, error) {
		return client.MicDown(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.MicDown(ctx, req))
}

// ChangeMicSeat 代理到领域服务。
func (s *Server) ChangeMicSeat(ctx context.Context, req *roomv1.ChangeMicSeatRequest) (*roomv1.ChangeMicSeatResponse, error) {
	// 换位在领域层原子修改源麦位和目标麦位。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.ChangeMicSeatResponse, error) {
		return client.ChangeMicSeat(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.ChangeMicSeat(ctx, req))
}

// ConfirmMicPublishing 代理到领域服务。
func (s *Server) ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmMicPublishingRequest) (*roomv1.ConfirmMicPublishingResponse, error) {
	// 发流确认必须进入 Room Cell 串行状态机，避免旧 RTC 事件覆盖新 mic_session。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.ConfirmMicPublishingResponse, error) {
		return client.ConfirmMicPublishing(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.ConfirmMicPublishing(ctx, req))
}

// MicHeartbeat 代理到领域服务。
func (s *Server) MicHeartbeat(ctx context.Context, req *roomv1.MicHeartbeatRequest) (*roomv1.MicHeartbeatResponse, error) {
	// 心跳仍由 Room Cell 串行提交，保证 presence、麦位快照和 command log 同版本。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.MicHeartbeatResponse, error) {
		return client.MicHeartbeat(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.MicHeartbeat(ctx, req))
}

// SetMicMute 代理到领域服务。
func (s *Server) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
	// 服务端可见静音态必须经 Room Cell 提交，确保其他用户 UI 和系统消息一致。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SetMicMuteResponse, error) {
		return client.SetMicMute(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.SetMicMute(ctx, req))
}

// ApplyRTCEvent 代理到领域服务。
func (s *Server) ApplyRTCEvent(ctx context.Context, req *roomv1.ApplyRTCEventRequest) (*roomv1.ApplyRTCEventResponse, error) {
	// 腾讯 RTC 回调已在 gateway 验签；房间状态变化仍必须由 Room Cell 串行提交。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.ApplyRTCEventResponse, error) {
		return client.ApplyRTCEvent(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.ApplyRTCEvent(ctx, req))
}

// ResolveRoomAppCode 仅解析已验签回调的房间租户，不读取或修改 Room Cell 状态。
func (s *Server) ResolveRoomAppCode(ctx context.Context, req *roomv1.ResolveRoomAppCodeRequest) (*roomv1.ResolveRoomAppCodeResponse, error) {
	return mapServiceResult(s.svc.ResolveRoomAppCode(ctx, req))
}

// SetMicSeatLock 代理到领域服务。
func (s *Server) SetMicSeatLock(ctx context.Context, req *roomv1.SetMicSeatLockRequest) (*roomv1.SetMicSeatLockResponse, error) {
	// 锁麦权限和目标麦位状态由 Room Cell 判定。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SetMicSeatLockResponse, error) {
		return client.SetMicSeatLock(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.SetMicSeatLock(ctx, req))
}

// SetChatEnabled 代理到领域服务。
func (s *Server) SetChatEnabled(ctx context.Context, req *roomv1.SetChatEnabledRequest) (*roomv1.SetChatEnabledResponse, error) {
	// 公屏开关是 room-service 发言守卫的权威状态。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SetChatEnabledResponse, error) {
		return client.SetChatEnabled(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.SetChatEnabled(ctx, req))
}

// SetRoomPassword 代理到领域服务。
func (s *Server) SetRoomPassword(ctx context.Context, req *roomv1.SetRoomPasswordRequest) (*roomv1.SetRoomPasswordResponse, error) {
	// 房间密码影响 JoinRoom 入口，必须由当前 Room Cell owner 串行提交。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SetRoomPasswordResponse, error) {
		return client.SetRoomPassword(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.SetRoomPassword(ctx, req))
}

// SetRoomAdmin 代理到领域服务。
func (s *Server) SetRoomAdmin(ctx context.Context, req *roomv1.SetRoomAdminRequest) (*roomv1.SetRoomAdminResponse, error) {
	// 管理员集合只能由 room-service 权限矩阵修改。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SetRoomAdminResponse, error) {
		return client.SetRoomAdmin(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.SetRoomAdmin(ctx, req))
}

// MuteUser 代理到领域服务。
func (s *Server) MuteUser(ctx context.Context, req *roomv1.MuteUserRequest) (*roomv1.MuteUserResponse, error) {
	// 禁言状态由 Room Cell 持有；腾讯 IM/RTC 仅消费 outbox 异步投影，查询接口保留给其它受控入口。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.MuteUserResponse, error) {
		return client.MuteUser(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.MuteUser(ctx, req))
}

// KickUser 代理到领域服务。
func (s *Server) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*roomv1.KickUserResponse, error) {
	// 踢人会修改 presence、ban 集合和麦位占用。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.KickUserResponse, error) {
		return client.KickUser(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.KickUser(ctx, req))
}

// UnbanUser 代理到领域服务。
func (s *Server) UnbanUser(ctx context.Context, req *roomv1.UnbanUserRequest) (*roomv1.UnbanUserResponse, error) {
	// 解封只解除 ban，不恢复 presence、管理员身份或麦位。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.UnbanUserResponse, error) {
		return client.UnbanUser(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.UnbanUser(ctx, req))
}

// SystemEvictUser 代理到领域服务。
func (s *Server) SystemEvictUser(ctx context.Context, req *roomv1.SystemEvictUserRequest) (*roomv1.SystemEvictUserResponse, error) {
	// 系统驱逐由平台封禁或风控触发，仍必须经 Room Cell 串行提交。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SystemEvictUserResponse, error) {
		return client.SystemEvictUser(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.SystemEvictUser(ctx, req))
}

// SendGift 代理到领域服务。
func (s *Server) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*roomv1.SendGiftResponse, error) {
	// SendGift 先同步扣费，成功后才更新房间热度和本地榜。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SendGiftResponse, error) {
		return client.SendGift(callCtx, req)
	}); forwarded {
		return resp, err
	}
	if strings.EqualFold(strings.TrimSpace(req.GetDisplayMode()), "batch") {
		// protobuf 仍保持一个 RPC，领域层显式分派 batch 入口，避免普通 SendGift 被 display_mode 偷偷改变展示/IM 策略。
		return mapServiceResult(s.svc.SendGiftBatch(ctx, req))
	}
	return mapServiceResult(s.svc.SendGift(ctx, req))
}

// ApplyRoomRPSGift 接收 game-service 已完成结算的败者礼物，并路由到房间 owner 节点的 Room Cell。
func (s *Server) ApplyRoomRPSGift(ctx context.Context, req *roomv1.ApplyRoomRPSGiftRequest) (*roomv1.ApplyRoomRPSGiftResponse, error) {
	if req == nil || req.GetMeta() == nil {
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "room rps settled gift meta is required"))
	}
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.ApplyRoomRPSGiftResponse, error) {
		return client.ApplyRoomRPSGift(callCtx, req)
	}); forwarded || err != nil {
		return resp, err
	}
	return mapServiceResult(s.svc.ApplyRoomRPSGift(ctx, req))
}

// FollowRoom 建立用户对房间的关注关系。
func (s *Server) FollowRoom(ctx context.Context, req *roomv1.FollowRoomRequest) (*roomv1.FollowRoomResponse, error) {
	// 房间关注是用户-房间关系事实，不改变 Room Cell 核心状态，因此不需要 owner 转发。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.FollowRoom(ctx, req))
}

// UnfollowRoom 取消用户对房间的关注关系。
func (s *Server) UnfollowRoom(ctx context.Context, req *roomv1.UnfollowRoomRequest) (*roomv1.UnfollowRoomResponse, error) {
	// 取消关注只收敛关系表状态；历史房间是否仍有 Cell 不影响幂等取消。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.UnfollowRoom(ctx, req))
}

// CheckSpeakPermission 代理到领域服务。
func (s *Server) CheckSpeakPermission(ctx context.Context, req *roomv1.CheckSpeakPermissionRequest) (*roomv1.CheckSpeakPermissionResponse, error) {
	// 外部 IM 发公屏前调用该守卫，避免复制禁言和房间状态规则。
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	if resp, forwarded, err := forwardGuard(s, ctx, req.GetAppCode(), req.GetRoomId(), func(callCtx context.Context, client roomv1.RoomGuardServiceClient) (*roomv1.CheckSpeakPermissionResponse, error) {
		return client.CheckSpeakPermission(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.CheckSpeakPermission(ctx, req))
}

// VerifyRoomPresence 代理到领域服务。
func (s *Server) VerifyRoomPresence(ctx context.Context, req *roomv1.VerifyRoomPresenceRequest) (*roomv1.VerifyRoomPresenceResponse, error) {
	// 外部 IM 进群前调用该守卫，防止未进房或被踢用户收消息。
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	if resp, forwarded, err := forwardGuard(s, ctx, req.GetAppCode(), req.GetRoomId(), func(callCtx context.Context, client roomv1.RoomGuardServiceClient) (*roomv1.VerifyRoomPresenceResponse, error) {
		return client.VerifyRoomPresence(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.VerifyRoomPresence(ctx, req))
}

// ListRooms 代理到 room-service 列表读模型。
func (s *Server) AdminListRooms(ctx context.Context, req *roomv1.AdminListRoomsRequest) (*roomv1.AdminListRoomsResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminListRooms(ctx, req))
}

func (s *Server) AdminGetRoom(ctx context.Context, req *roomv1.AdminGetRoomRequest) (*roomv1.AdminGetRoomResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminGetRoom(ctx, req))
}

func (s *Server) AdminGetRoomRocketConfig(ctx context.Context, req *roomv1.AdminGetRoomRocketConfigRequest) (*roomv1.AdminGetRoomRocketConfigResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminGetRoomRocketConfig(ctx, req))
}

func (s *Server) AdminGetRoomSeatConfig(ctx context.Context, req *roomv1.AdminGetRoomSeatConfigRequest) (*roomv1.AdminGetRoomSeatConfigResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminGetRoomSeatConfig(ctx, req))
}

func (s *Server) AdminGetHumanRoomRobotConfig(ctx context.Context, req *roomv1.AdminGetHumanRoomRobotConfigRequest) (*roomv1.AdminGetHumanRoomRobotConfigResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminGetHumanRoomRobotConfig(ctx, req))
}

func (s *Server) AdminListRoomPins(ctx context.Context, req *roomv1.AdminListRoomPinsRequest) (*roomv1.AdminListRoomPinsResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminListRoomPins(ctx, req))
}

func (s *Server) AdminListRobotRooms(ctx context.Context, req *roomv1.AdminListRobotRoomsRequest) (*roomv1.AdminListRobotRoomsResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminListRobotRooms(ctx, req))
}

func (s *Server) AdminFilterAvailableRoomRobots(ctx context.Context, req *roomv1.AdminFilterAvailableRoomRobotsRequest) (*roomv1.AdminFilterAvailableRoomRobotsResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.AdminFilterAvailableRoomRobots(ctx, req))
}

// ListRooms 代理到 room-service 列表读模型。
func (s *Server) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) {
	// 列表查询只读 MySQL 投影，不扫描 Room Cell 内存集合。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.ListRooms(ctx, req))
}

// ListRoomsByOwners 代理到 owner 批量房间卡片读模型。
func (s *Server) ListRoomsByOwners(ctx context.Context, req *roomv1.ListRoomsByOwnersRequest) (*roomv1.ListRoomsResponse, error) {
	// owner 集合由上游业务归属解析后注入，room-service 只按房间 owner 投影批量读取卡片。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.ListRoomsByOwners(ctx, req))
}

// ListRoomFeeds 代理到 Mine 页用户房间流读模型。
func (s *Server) ListRoomFeeds(ctx context.Context, req *roomv1.ListRoomFeedsRequest) (*roomv1.ListRoomsResponse, error) {
	// feed 查询只读用户到房间索引，并 join 房间卡片投影，不扫描 Room Cell 内存集合。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.ListRoomFeeds(ctx, req))
}

// ListRoomGiftLeaderboard 代理到 Redis zset 房间金币榜读模型。
func (s *Server) ListRoomGiftLeaderboard(ctx context.Context, req *roomv1.ListRoomGiftLeaderboardRequest) (*roomv1.ListRoomGiftLeaderboardResponse, error) {
	// 房间榜只读 room-service 维护的金币 zset，并用 room_list_entries 批量补卡片。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.ListRoomGiftLeaderboard(ctx, req))
}

// ListRoomContributionRank 代理到 Redis zset 单房间用户贡献周期榜读模型。
func (s *Server) ListRoomContributionRank(ctx context.Context, req *roomv1.ListRoomContributionRankRequest) (*roomv1.ListRoomContributionRankResponse, error) {
	// 单房间贡献榜只读 SendGift 成功后投影的周期 zset，不扫描 Room Cell、command log 或 outbox。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.ListRoomContributionRank(ctx, req))
}

// GetMyRoom 代理到 Mine 顶部“我的房间”权威查询。
func (s *Server) GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error) {
	// 我的房间查询直接读取 rooms 元数据，并用快照补齐实时卡片字段，不依赖发现页投影。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.GetMyRoom(ctx, req))
}

// GetCurrentRoom 代理到用户当前房间恢复探测读模型。
func (s *Server) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) {
	// 恢复探测不修改 Room Cell，只在读模型命中后用快照做二次校验。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.GetCurrentRoom(ctx, req))
}

// BatchGetUserVoiceRoomPresences 代理到个人资料页上麦状态批量查询。
func (s *Server) BatchGetUserVoiceRoomPresences(ctx context.Context, req *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) {
	// 查询先用用户级 presence 定位候选房间，再由 Room Cell 快照收敛状态；gRPC 层不复制权限判断。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.BatchGetUserVoiceRoomPresences(ctx, req))
}

// GetRoomSnapshot 代理到房间页完整快照只读查询。
func (s *Server) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
	// 快照查询不刷新 presence，不替代 heartbeat，也不能让未进房用户读取完整房间态。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardQuery(s, ctx, req.GetMeta().GetAppCode(), req.GetRoomId(), func(callCtx context.Context, client roomv1.RoomQueryServiceClient) (*roomv1.GetRoomSnapshotResponse, error) {
		return client.GetRoomSnapshot(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.GetRoomSnapshot(ctx, req))
}

func (s *Server) ListRoomBackgrounds(ctx context.Context, req *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) {
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardQuery(s, ctx, req.GetMeta().GetAppCode(), req.GetRoomId(), func(callCtx context.Context, client roomv1.RoomQueryServiceClient) (*roomv1.ListRoomBackgroundsResponse, error) {
		return client.ListRoomBackgrounds(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.ListRoomBackgrounds(ctx, req))
}

// GetRoomRocket 代理到房间火箭只读查询。
func (s *Server) GetRoomRocket(ctx context.Context, req *roomv1.GetRoomRocketRequest) (*roomv1.GetRoomRocketResponse, error) {
	// 火箭查询不刷新 presence；当前进度仍以 Room Cell 快照和 UTC 日边界为准。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardQuery(s, ctx, req.GetMeta().GetAppCode(), req.GetRoomId(), func(callCtx context.Context, client roomv1.RoomQueryServiceClient) (*roomv1.GetRoomRocketResponse, error) {
		return client.GetRoomRocket(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.GetRoomRocket(ctx, req))
}

// ListRoomOnlineUsers 代理到房间在线用户分页读模型。
func (s *Server) ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error) {
	// 在线人数弹窗只读 room_user_presence 投影，不拉完整 RoomSnapshot 做分页。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	return mapServiceResult(s.svc.ListRoomOnlineUsers(ctx, req))
}

// ListRoomBannedUsers 代理到房间黑名单分页读模型。
func (s *Server) ListRoomBannedUsers(ctx context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
	// 黑名单来自 Room Cell，必须按 room owner 路由读取最新治理状态。
	ctx = contextWithMetaApp(ctx, req.GetMeta())
	if resp, forwarded, err := forwardQuery(s, ctx, req.GetMeta().GetAppCode(), req.GetRoomId(), func(callCtx context.Context, client roomv1.RoomQueryServiceClient) (*roomv1.ListRoomBannedUsersResponse, error) {
		return client.ListRoomBannedUsers(callCtx, req)
	}); forwarded {
		return resp, err
	}
	return mapServiceResult(s.svc.ListRoomBannedUsers(ctx, req))
}

func contextWithMetaApp(ctx context.Context, meta *roomv1.RequestMeta) context.Context {
	if meta == nil {
		return appcode.WithContext(ctx, "")
	}
	return appcode.WithContext(ctx, meta.GetAppCode())
}

func forwardCommand[T any](s *Server, ctx context.Context, meta *roomv1.RequestMeta, call func(context.Context, roomv1.RoomCommandServiceClient) (T, error)) (T, bool, error) {
	var zero T
	if s == nil || s.ownerForwarder == nil || meta == nil {
		return zero, false, nil
	}
	return forwardCommandToOwner(s.ownerForwarder, ctx, meta.GetAppCode(), meta.GetRoomId(), call)
}

func forwardGuard[T any](s *Server, ctx context.Context, appCode string, roomID string, call func(context.Context, roomv1.RoomGuardServiceClient) (T, error)) (T, bool, error) {
	var zero T
	if s == nil || s.ownerForwarder == nil {
		return zero, false, nil
	}
	return forwardGuardToOwner(s.ownerForwarder, ctx, appCode, roomID, call)
}

func forwardQuery[T any](s *Server, ctx context.Context, appCode string, roomID string, call func(context.Context, roomv1.RoomQueryServiceClient) (T, error)) (T, bool, error) {
	var zero T
	if s == nil || s.ownerForwarder == nil {
		return zero, false, nil
	}
	return forwardQueryToOwner(s.ownerForwarder, ctx, appCode, roomID, call)
}
