package tencentim

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

const (
	// DefaultEndpoint 是腾讯云 IM 国内 REST 默认入口；海外部署可在配置中覆盖为对应地域域名。
	DefaultEndpoint = "console.tim.qq.com"
	// DefaultGroupType 使用 ChatRoom，保留消息历史能力；AVChatRoom 不适合需要补偿和漫游的房间消息。
	DefaultGroupType = "ChatRoom"
	// BroadcastGroupType 使用 AVChatRoom（直播群）：ChatRoom 受套餐成员上限约束（标准版 2000 人，
	// 已在线上触发 10038 拒绝加群），AVChatRoom 成员无上限、进出免审批，播报横幅也不需要漫游补偿。
	BroadcastGroupType = "AVChatRoom"
	// createGroupCommand 是建群 REST 命令；房间群、全局播报群、区域播报群都复用这一个能力。
	createGroupCommand = "v4/group_open_http_svc/create_group"
	// sendGroupMsgCommand 是服务端发群消息 REST 命令；播报只走服务端管理员账号，不开放客户端直发。
	sendGroupMsgCommand = "v4/group_open_http_svc/send_group_msg"
	// sendC2CMsgCommand 是服务端给单个用户发自定义消息的 REST 命令；钱包余额变更这类私有事件只能走单聊面。
	sendC2CMsgCommand = "v4/openim/sendmsg"
	// accountImportCommand 把内部 user_id 导入腾讯 IM 账号体系；C2C 发送和 SDK 登录依赖该账号存在。
	accountImportCommand = "v4/im_open_login_svc/account_import"
	// destroyGroupCommand 用于真实 IM smoke test 或明确运维动作的临时群清理，业务代码不应自动解散播报群。
	destroyGroupCommand = "v4/group_open_http_svc/destroy_group"
	// deleteGroupMemberCommand 用于把已离房用户从腾讯群里移除，防止 IM 群成员残留扩大消息面。
	deleteGroupMemberCommand = "v4/group_open_http_svc/delete_group_member"
	// forbidSendMsgCommand 只修改指定群和指定成员的腾讯 IM 禁言状态，不进入任何消息发送前回调链路。
	forbidSendMsgCommand = "v4/group_open_http_svc/forbid_send_msg"
	// kickUserCommand 让指定账号现有 IM 登录态失效；被封禁用户需要立即从腾讯 IM SDK 下线。
	kickUserCommand = "v4/im_open_login_svc/kick"
	// PermanentGroupMuteSeconds 是腾讯 IM 约定的永久禁言秒数；解除禁言使用 0。
	PermanentGroupMuteSeconds uint32 = 1<<32 - 1
)

// RESTConfig 保存服务端调用腾讯云 IM REST API 所需配置。
type RESTConfig struct {
	// SDKAppID 是腾讯云 IM 应用 ID；回调校验和 UserSig 签发都必须使用同一个值。
	SDKAppID int64
	// SecretKey 只用于服务端签 admin UserSig，不能返回给客户端或写入错误日志。
	SecretKey string
	// AdminIdentifier 是腾讯云控制台配置的管理员账号，播报群消息用它证明“服务端发送”。
	AdminIdentifier string
	// AdminUserSigTTL 控制 REST 调用签名有效期；过短会增加签发频率，过长会扩大泄露影响面。
	AdminUserSigTTL time.Duration
	// Endpoint 是 REST 地域入口，例如 adminapisgp.im.qcloud.com；不要把地域选择写死在业务里。
	Endpoint string
	// GroupType 是房间和播报群共用的腾讯群类型；当前保持 ChatRoom 以保留历史能力。
	GroupType string
	// HTTPClient 允许测试注入 fake transport，也让上层统一设置超时。
	HTTPClient *http.Client
}

// GroupSpec 是项目内部的建群请求，屏蔽腾讯 REST 字段命名差异。
type GroupSpec struct {
	GroupID         string
	Name            string
	OwnerAccount    string
	Type            string
	ApplyJoinOption string
}

// CustomGroupMessage 表示一条服务端发出的 TIMCustomElem 群消息。
// PayloadJSON 同时写入 Data 和 CloudCustomData，客户端和排障侧都能按 event_id 做幂等和追踪。
type CustomGroupMessage struct {
	GroupID     string
	EventID     string
	Desc        string
	Ext         string
	FromAccount string
	PayloadJSON json.RawMessage
}

// CustomUserMessage 表示一条服务端发给单个用户的 TIMCustomElem。
// PayloadJSON 同时写入 Data 和 CloudCustomData；客户端必须按 event_id 去重。
type CustomUserMessage struct {
	ToAccount string
	EventID   string
	Desc      string
	Ext       string
	// FromAccount 为空时使用管理员账号；用户主动行为要显式传用户 identifier，避免消息归属变成系统通知。
	FromAccount string
	// SyncOtherMachine 透传腾讯 C2C 同步策略；0 保持历史默认 2，不同步发送方其他终端。
	SyncOtherMachine int
	// DeduplicateByEventID 只用于允许 at-least-once 重投的业务事实；开启后 REST 层用 event_id
	// 稳定生成腾讯 120 秒去重键。普通聊天保持平台默认序列，避免改变同秒消息排序。
	DeduplicateByEventID bool
	PayloadJSON          json.RawMessage
}

// AccountProfile 表达腾讯 IM 账号导入所需的最小用户资料。
type AccountProfile struct {
	UserID  int64
	Nick    string
	FaceURL string
}

// RoomEvent 是 room-service 发给腾讯 IM 的房间系统消息负载。
type RoomEvent struct {
	GroupID   string `json:"-"`
	EventID   string `json:"event_id"`
	RoomID    string `json:"room_id"`
	EventType string `json:"event_type"`
	// ActorUserID 和 TargetUserID 是客户端协议字段，必须保持十进制字符串；
	// JavaScript Number 无法安全承载 Snowflake-like int64，不能在 IM JSON 边界输出数字。
	ActorUserID  string `json:"actor_user_id,omitempty"`
	TargetUserID string `json:"target_user_id,omitempty"`
	SeatNo       int32  `json:"seat_no,omitempty"`
	GiftValue    int64  `json:"gift_value,omitempty"`
	RoomHeat     int64  `json:"room_heat,omitempty"`
	RoomVersion  int64  `json:"room_version,omitempty"`
	Text         string `json:"text,omitempty"`
	// EntryVehicle 是用户进房瞬间的座驾快照；客户端收到 room_user_joined 后直接播放，不再反查 wallet。
	EntryVehicle *RoomEntryVehicleSnapshot `json:"entry_vehicle,omitempty"`
	Attributes   map[string]string         `json:"attributes,omitempty"`
}

func (event RoomEvent) MarshalJSON() ([]byte, error) {
	// 直接构造最终负载，不能先反序列化到 map[string]any；encoding/json 会把其中
	// 的整数转成 float64，使任何超过 2^53-1 的长整数字段在后端二次序列化时失真。
	merged := map[string]any{
		"event_id":   event.EventID,
		"room_id":    event.RoomID,
		"event_type": event.EventType,
	}
	if actorUserID := strings.TrimSpace(event.ActorUserID); actorUserID != "" {
		merged["actor_user_id"] = actorUserID
	}
	if targetUserID := strings.TrimSpace(event.TargetUserID); targetUserID != "" {
		merged["target_user_id"] = targetUserID
	}
	if event.SeatNo != 0 {
		merged["seat_no"] = event.SeatNo
	}
	if event.GiftValue != 0 {
		merged["gift_value"] = event.GiftValue
	}
	if event.RoomHeat != 0 {
		merged["room_heat"] = event.RoomHeat
	}
	if event.RoomVersion != 0 {
		merged["room_version"] = event.RoomVersion
	}
	if event.Text != "" {
		merged["text"] = event.Text
	}
	if event.EntryVehicle != nil {
		merged["entry_vehicle"] = event.EntryVehicle
	}
	if len(event.Attributes) > 0 {
		merged["attributes"] = event.Attributes
	}
	for key, value := range event.Attributes {
		if strings.TrimSpace(key) == "" {
			continue
		}
		if _, exists := merged[key]; exists {
			// 顶层协议字段由 RoomEvent 结构体负责，attributes 只能补充展示字段，不能覆盖事件身份。
			continue
		}
		merged[key] = value
	}
	return json.Marshal(merged)
}

// RoomEntryVehicleSnapshot 是房间系统消息里可直接渲染的座驾素材快照。
// 它故意不包含 wallet 内部状态，只保留客户端展示和过期兜底需要的字段。
type RoomEntryVehicleSnapshot struct {
	ResourceID    int64  `json:"resource_id"`
	ResourceCode  string `json:"resource_code,omitempty"`
	Name          string `json:"name,omitempty"`
	AssetURL      string `json:"asset_url,omitempty"`
	PreviewURL    string `json:"preview_url,omitempty"`
	AnimationURL  string `json:"animation_url,omitempty"`
	MetadataJSON  string `json:"metadata_json,omitempty"`
	EntitlementID string `json:"entitlement_id,omitempty"`
	ExpiresAtMS   int64  `json:"expires_at_ms,omitempty"`
}

// RESTClient 封装腾讯云 IM REST API，把 UserSig、endpoint query 和错误码处理收敛在一个边界。
type RESTClient struct {
	// cfg 初始化后不再修改，因此同一个 client 可以被多个 worker 并发复用。
	cfg RESTConfig
	// httpClient 复用 keep-alive，避免高频播报时每条消息都重新建连接。
	httpClient *http.Client
}

// NewRESTClient 校验必需配置并创建 REST client。
func NewRESTClient(cfg RESTConfig) (*RESTClient, error) {
	if cfg.SDKAppID <= 0 || cfg.SecretKey == "" || strings.TrimSpace(cfg.AdminIdentifier) == "" {
		// REST API 依赖管理员账号和 admin UserSig；缺任何一个都不能伪造成功。
		return nil, fmt.Errorf("tencent im rest config is incomplete")
	}
	if cfg.AdminUserSigTTL <= 0 {
		cfg.AdminUserSigTTL = 24 * time.Hour
	}
	if strings.TrimSpace(cfg.Endpoint) == "" {
		cfg.Endpoint = DefaultEndpoint
	}
	if strings.TrimSpace(cfg.GroupType) == "" {
		cfg.GroupType = DefaultGroupType
	}
	if cfg.HTTPClient == nil {
		cfg.HTTPClient = &http.Client{Timeout: 5 * time.Second}
	}

	return &RESTClient{
		cfg:        cfg,
		httpClient: cfg.HTTPClient,
	}, nil
}

// EnsureRoomGroup 为房间创建腾讯云 IM 群。
// ownerUserID 暂不映射为腾讯群主，房间状态 owner 仍是 Room Cell，腾讯群只做投递通道。
func (c *RESTClient) EnsureRoomGroup(ctx context.Context, roomID string, ownerUserID int64) error {
	roomID = strings.TrimSpace(roomID)
	if roomID == "" {
		return fmt.Errorf("room_id is required")
	}
	_ = ownerUserID

	return c.EnsureGroup(ctx, GroupSpec{
		GroupID:         roomID,
		Name:            roomID,
		Type:            c.cfg.GroupType,
		ApplyJoinOption: "FreeAccess",
	})
}

// EnsureGroup 创建服务端拥有的腾讯云 IM 群，并把“自定义 GroupID 已存在”视为成功。
// 这样启动 reconciler、CreateRoom 重试或发布补偿重复执行时，都不会因为已建群而中断主流程。
func (c *RESTClient) EnsureGroup(ctx context.Context, spec GroupSpec) error {
	spec.GroupID = strings.TrimSpace(spec.GroupID)
	spec.Name = strings.TrimSpace(spec.Name)
	spec.Type = strings.TrimSpace(spec.Type)
	spec.ApplyJoinOption = strings.TrimSpace(spec.ApplyJoinOption)
	spec.OwnerAccount = strings.TrimSpace(spec.OwnerAccount)
	if spec.GroupID == "" {
		return fmt.Errorf("group_id is required")
	}
	if spec.Name == "" {
		spec.Name = spec.GroupID
	}
	if spec.Type == "" {
		spec.Type = c.cfg.GroupType
	}
	if spec.Type == BroadcastGroupType {
		// AVChatRoom 不支持 ApplyJoinOption，带上会被腾讯以非法参数拒绝。
		spec.ApplyJoinOption = ""
	} else if spec.ApplyJoinOption == "" {
		spec.ApplyJoinOption = "FreeAccess"
	}

	request := createGroupRequest{
		Type:            spec.Type,
		GroupID:         spec.GroupID,
		Name:            spec.Name,
		OwnerAccount:    spec.OwnerAccount,
		ApplyJoinOption: spec.ApplyJoinOption,
	}

	var response restResponse
	if err := c.post(ctx, createGroupCommand, request, &response); err != nil {
		return err
	}
	if response.ErrorCode == 10021 || response.ErrorCode == 10025 {
		// 腾讯云不同接口版本可能返回 10021/10025 表示群已存在；建群语义要求幂等。
		return nil
	}

	return response.err()
}

// PublishRoomEvent 把房间系统事件转换为腾讯云群自定义消息。
func (c *RESTClient) PublishRoomEvent(ctx context.Context, event RoomEvent) error {
	if strings.TrimSpace(event.RoomID) == "" || strings.TrimSpace(event.EventID) == "" {
		// event_id 是客户端去重、日志追踪和补偿投递判断的共同锚点。
		return fmt.Errorf("room event is incomplete")
	}
	groupID := strings.TrimSpace(event.GroupID)
	if groupID == "" {
		groupID = event.RoomID
	}

	eventPayload, err := json.Marshal(event)
	if err != nil {
		return err
	}

	return c.PublishGroupCustomMessage(ctx, CustomGroupMessage{
		GroupID:     groupID,
		EventID:     event.EventID,
		Desc:        event.EventType,
		Ext:         "room_system_message",
		PayloadJSON: eventPayload,
	})
}

// PublishGroupCustomMessage 向任意服务端拥有的腾讯云 IM 群发送 TIMCustomElem。
// 该方法不判断业务类型，调用方必须已经通过 room/broadcast service 完成权限、幂等和持久化决策。
func (c *RESTClient) PublishGroupCustomMessage(ctx context.Context, message CustomGroupMessage) error {
	message.GroupID = strings.TrimSpace(message.GroupID)
	message.EventID = strings.TrimSpace(message.EventID)
	message.Desc = strings.TrimSpace(message.Desc)
	message.Ext = strings.TrimSpace(message.Ext)
	message.FromAccount = strings.TrimSpace(message.FromAccount)
	payload := bytes.TrimSpace(message.PayloadJSON)
	if message.GroupID == "" || message.EventID == "" || len(payload) == 0 {
		return fmt.Errorf("group custom message is incomplete")
	}

	request := sendGroupMsgRequest{
		GroupID: message.GroupID,
		// 腾讯云群消息会在短窗口内按 Random + 完整消息内容去重。outbox 的 event_id
		// 对同一业务事实稳定，因此从它派生 Random 可收敛“腾讯已成功、本地尚未标记”后的补偿重试；
		// 客户端仍应按 payload.event_id 去重，以覆盖超出腾讯去重窗口后的极端重放。
		Random:          stableMessageRandom(message.EventID),
		CloudCustomData: string(payload),
		FromAccount:     message.FromAccount,
		MsgBody: []messageElement{
			{
				MsgType: "TIMCustomElem",
				MsgContent: customMsgContent{
					Data:  string(payload),
					Desc:  message.Desc,
					Ext:   message.Ext,
					Sound: "",
				},
			},
		},
	}

	var response restResponse
	if err := c.post(ctx, sendGroupMsgCommand, request, &response); err != nil {
		return err
	}

	return response.err()
}

func stableMessageRandom(eventID string) uint32 {
	sum := sha256.Sum256([]byte(strings.TrimSpace(eventID)))
	return binary.BigEndian.Uint32(sum[:4])
}

func stableMessageSequence(eventID string) uint32 {
	sum := sha256.Sum256([]byte(strings.TrimSpace(eventID)))
	sequence := binary.BigEndian.Uint32(sum[4:8])
	if sequence == 0 {
		// MsgSeq=0 省略或由平台生成会破坏重试去重键；极小概率哈希为零时固定提升到 1。
		return 1
	}
	return sequence
}

// PublishUserCustomMessage 向单个腾讯云 IM identifier 发送 TIMCustomElem。
// 该方法只负责投递协议和腾讯的短窗口去重；持久业务回执、隐私字段和重试策略仍由调用方保证。
func (c *RESTClient) PublishUserCustomMessage(ctx context.Context, message CustomUserMessage) error {
	message.ToAccount = strings.TrimSpace(message.ToAccount)
	message.EventID = strings.TrimSpace(message.EventID)
	message.Desc = strings.TrimSpace(message.Desc)
	message.Ext = strings.TrimSpace(message.Ext)
	message.FromAccount = strings.TrimSpace(message.FromAccount)
	if message.FromAccount == "" {
		// C2C 服务端消息默认从管理员账号发出，避免调用方每个业务模块复制同一配置。
		message.FromAccount = c.cfg.AdminIdentifier
	}
	syncOtherMachine := message.SyncOtherMachine
	if syncOtherMachine == 0 {
		// 历史 notice 消息只投递给目标用户；不显式传值时继续保持这个行为，避免改变钱包/CP 通知的漫游面。
		syncOtherMachine = 2
	}
	payload := bytes.TrimSpace(message.PayloadJSON)
	if message.ToAccount == "" || message.EventID == "" || len(payload) == 0 {
		return fmt.Errorf("user custom message is incomplete")
	}

	messageSequence := uint32(0)
	messageRandom := randomUint32()
	if message.DeduplicateByEventID {
		// 腾讯云以发送人、MsgSeq、MsgRandom 和消息体 CRC 组成 120 秒去重键。
		// event_id 是项目内逻辑消息幂等键，稳定派生两项可收敛并发首发和响应丢失后的即时重试。
		messageSequence = stableMessageSequence(message.EventID)
		messageRandom = stableMessageRandom(message.EventID)
	}
	request := sendC2CMsgRequest{
		SyncOtherMachine: syncOtherMachine,
		ToAccount:        message.ToAccount,
		MsgSeq:           messageSequence,
		MsgRandom:        messageRandom,
		CloudCustomData:  string(payload),
		FromAccount:      message.FromAccount,
		MsgBody: []messageElement{
			{
				MsgType: "TIMCustomElem",
				MsgContent: customMsgContent{
					Data:  string(payload),
					Desc:  message.Desc,
					Ext:   message.Ext,
					Sound: "",
				},
			},
		},
	}

	var response restResponse
	if err := c.post(ctx, sendC2CMsgCommand, request, &response); err != nil {
		return err
	}

	return response.err()
}

// ImportAccount 确保内部 user_id 已存在于腾讯 IM 账号体系。
// 腾讯接口本身没有本地事务语义，因此调用方应在用户事务提交后调用，并允许后续 UserSig 入口补偿。
func (c *RESTClient) ImportAccount(ctx context.Context, profile AccountProfile) error {
	if profile.UserID <= 0 {
		return fmt.Errorf("im account import is incomplete")
	}

	request := accountImportRequest{
		UserID:  FormatUserID(profile.UserID),
		Nick:    strings.TrimSpace(profile.Nick),
		FaceURL: strings.TrimSpace(profile.FaceURL),
	}
	var response restResponse
	if err := c.post(ctx, accountImportCommand, request, &response); err != nil {
		return err
	}
	if response.ErrorCode == 0 || isAccountAlreadyImported(response) {
		return nil
	}

	return response.err()
}

// DestroyGroup 解散一个腾讯云 IM 群。
// 生产播报群不应该在业务流里自动调用该方法；它主要用于真实 IM 测试后的临时群清理或显式运维操作。
func (c *RESTClient) DestroyGroup(ctx context.Context, groupID string) error {
	groupID = strings.TrimSpace(groupID)
	if groupID == "" {
		return fmt.Errorf("group_id is required")
	}

	request := destroyGroupRequest{GroupID: groupID}
	var response restResponse
	if err := c.post(ctx, destroyGroupCommand, request, &response); err != nil {
		return err
	}

	return response.err()
}

// DeleteGroupMember 从指定腾讯云 IM 群移除一个用户。
// 它只改变“这个用户在这个群里的成员关系”，不会删除群，也不会影响其他成员。
func (c *RESTClient) DeleteGroupMember(ctx context.Context, groupID string, userID int64) error {
	groupID = strings.TrimSpace(groupID)
	if groupID == "" || userID <= 0 {
		return fmt.Errorf("group member removal is incomplete")
	}

	request := deleteGroupMemberRequest{
		GroupID:            groupID,
		MemberToDelAccount: []string{FormatUserID(userID)},
		Silence:            1,
	}

	var response restResponse
	if err := c.post(ctx, deleteGroupMemberCommand, request, &response); err != nil {
		return err
	}

	return response.err()
}

// DeleteRoomGroupMember 从房间 IM 群移除用户；这是 presence 清理的外部副作用，不改变房间权威状态。
func (c *RESTClient) DeleteRoomGroupMember(ctx context.Context, roomID string, userID int64) error {
	return c.DeleteGroupMember(ctx, roomID, userID)
}

// SetGroupMemberMuted 通过腾讯 IM 原生群成员禁言能力投影 Room Cell 状态。
// 该接口只在禁言事实变化时调用，不参与普通群消息发送，因此不会给共享 SDKAppID 的其它项目增加发送时延。
func (c *RESTClient) SetGroupMemberMuted(ctx context.Context, groupID string, userID int64, muted bool) error {
	groupID = strings.TrimSpace(groupID)
	if groupID == "" || userID <= 0 {
		return fmt.Errorf("group member mute is incomplete")
	}

	muteTime := uint32(0)
	if muted {
		muteTime = PermanentGroupMuteSeconds
	}
	request := forbidSendMsgRequest{
		GroupID:        groupID,
		MemberAccounts: []string{FormatUserID(userID)},
		MuteTime:       muteTime,
	}
	var response restResponse
	if err := c.post(ctx, forbidSendMsgCommand, request, &response); err != nil {
		return err
	}
	return response.err()
}

// KickUser 失效指定用户的腾讯云 IM 登录态。
// 该接口不会禁止未来用新 UserSig 登录，因此调用方必须先在业务状态和 token denylist 上完成封禁。
func (c *RESTClient) KickUser(ctx context.Context, userID int64) error {
	if userID <= 0 {
		return fmt.Errorf("kick user is incomplete")
	}

	request := kickUserRequest{UserID: FormatUserID(userID)}
	var response restResponse
	if err := c.post(ctx, kickUserCommand, request, &response); err != nil {
		return err
	}

	return response.err()
}

// FormatUserID 把内部不可变 user_id 映射成腾讯云账号字符串。
func FormatUserID(userID int64) string {
	// 十进制 user_id 跨服务稳定，也避免把可变 display_user_id 暴露给 IM 账号体系。
	return strconv.FormatInt(userID, 10)
}

// FormatOptionalUserID 把可选内部 ID 转成客户端协议字符串；零值保持为空，延续 JSON omitempty 语义。
func FormatOptionalUserID(userID int64) string {
	if userID <= 0 {
		return ""
	}
	return FormatUserID(userID)
}

// FormatUserIDsJSON 把用户 ID 集合编码成 JSON 字符串数组，避免客户端二次解析数组时重新落入 Number 精度边界。
func FormatUserIDsJSON(userIDs []int64) string {
	formatted := make([]string, 0, len(userIDs))
	for _, userID := range userIDs {
		if value := FormatOptionalUserID(userID); value != "" {
			formatted = append(formatted, strconv.Quote(value))
		}
	}
	return "[" + strings.Join(formatted, ",") + "]"
}

func (c *RESTClient) post(ctx context.Context, command string, payload any, out any) error {
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint(command), bytes.NewReader(body))
	if err != nil {
		return err
	}
	request.Header.Set("Content-Type", "application/json")

	response, err := c.httpClient.Do(request)
	if err != nil {
		var transportErr *url.Error
		if errors.As(err, &transportErr) {
			// net/http 的 url.Error.Error 会拼入完整请求 URL，而腾讯 IM UserSig 位于
			// query string。只向上保留底层网络原因，既维持 errors.Is 的超时/取消
			// 语义，又避免日志和 notice last_error 持久化可用签名。
			return fmt.Errorf("tencent im request failed: %w", transportErr.Err)
		}
		return fmt.Errorf("tencent im request failed: %w", err)
	}
	defer response.Body.Close()

	responseBody, err := io.ReadAll(response.Body)
	if err != nil {
		return err
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		// 腾讯 REST 正常业务失败通常是 HTTP 200 + ErrorCode；非 2xx 视为网络/网关层失败。
		return fmt.Errorf("tencent im http status %d: %s", response.StatusCode, string(responseBody))
	}
	if err := json.Unmarshal(responseBody, out); err != nil {
		return err
	}

	return nil
}

func (c *RESTClient) endpoint(command string) string {
	adminSig, _ := GenerateUserSig(UserSigConfig{
		SDKAppID:  c.cfg.SDKAppID,
		SecretKey: c.cfg.SecretKey,
		TTL:       c.cfg.AdminUserSigTTL,
	}, c.cfg.AdminIdentifier, time.Now().UTC())

	values := url.Values{}
	values.Set("sdkappid", strconv.FormatInt(c.cfg.SDKAppID, 10))
	values.Set("identifier", c.cfg.AdminIdentifier)
	values.Set("usersig", adminSig.UserSig)
	values.Set("random", strconv.FormatUint(uint64(randomUint32()), 10))
	values.Set("contenttype", "json")

	return "https://" + strings.TrimSuffix(c.cfg.Endpoint, "/") + "/" + strings.TrimPrefix(command, "/") + "?" + values.Encode()
}

func randomUint32() uint32 {
	var payload [4]byte
	if _, err := rand.Read(payload[:]); err != nil {
		// REST random 只需要足够的去重熵；加密随机失败时用纳秒时间兜底。
		return uint32(time.Now().UnixNano())
	}

	return binary.BigEndian.Uint32(payload[:])
}

type restResponse struct {
	ActionStatus string `json:"ActionStatus"`
	ErrorInfo    string `json:"ErrorInfo"`
	ErrorCode    int    `json:"ErrorCode"`
}

// RESTError 保留腾讯云 IM 业务错误码，让上层能区分缺群、参数错误和限流。
type RESTError struct {
	Code int
	Info string
}

// Error 实现 error，沿用既有日志格式，避免线上检索规则失效。
func (e RESTError) Error() string {
	return fmt.Sprintf("tencent im rest failed: code=%d info=%s", e.Code, e.Info)
}

// IsRESTErrorCode 判断错误链中是否包含指定腾讯云 IM 业务错误码。
func IsRESTErrorCode(err error, codes ...int) bool {
	var restErr RESTError
	if !errors.As(err, &restErr) {
		return false
	}
	for _, code := range codes {
		if restErr.Code == code {
			return true
		}
	}
	return false
}

func (r restResponse) err() error {
	if r.ErrorCode == 0 && strings.EqualFold(r.ActionStatus, "OK") {
		return nil
	}
	if r.ErrorCode == 0 && r.ActionStatus == "" {
		// 部分测试只构造 ErrorCode；腾讯语义里 0 即成功，因此允许空 ActionStatus。
		return nil
	}

	return RESTError{Code: r.ErrorCode, Info: r.ErrorInfo}
}

func isAccountAlreadyImported(response restResponse) bool {
	// 腾讯不同版本和地域对重复导入的错误码/文案不完全一致；账号已存在是幂等成功。
	if response.ErrorCode == 70107 {
		return true
	}
	info := strings.ToLower(strings.TrimSpace(response.ErrorInfo))
	return strings.Contains(info, "already") || strings.Contains(info, "exist")
}

type createGroupRequest struct {
	OwnerAccount    string `json:"Owner_Account,omitempty"`
	Type            string `json:"Type"`
	GroupID         string `json:"GroupId"`
	Name            string `json:"Name"`
	ApplyJoinOption string `json:"ApplyJoinOption,omitempty"`
}

type sendGroupMsgRequest struct {
	GroupID         string           `json:"GroupId"`
	Random          uint32           `json:"Random"`
	MsgBody         []messageElement `json:"MsgBody"`
	CloudCustomData string           `json:"CloudCustomData,omitempty"`
	FromAccount     string           `json:"From_Account,omitempty"`
}

type sendC2CMsgRequest struct {
	SyncOtherMachine int              `json:"SyncOtherMachine"`
	FromAccount      string           `json:"From_Account,omitempty"`
	ToAccount        string           `json:"To_Account"`
	MsgLifeTime      int              `json:"MsgLifeTime,omitempty"`
	MsgSeq           uint32           `json:"MsgSeq,omitempty"`
	MsgRandom        uint32           `json:"MsgRandom"`
	CloudCustomData  string           `json:"CloudCustomData,omitempty"`
	MsgBody          []messageElement `json:"MsgBody"`
}

type accountImportRequest struct {
	UserID  string `json:"UserID"`
	Nick    string `json:"Nick,omitempty"`
	FaceURL string `json:"FaceUrl,omitempty"`
}

type destroyGroupRequest struct {
	GroupID string `json:"GroupId"`
}

type deleteGroupMemberRequest struct {
	GroupID            string   `json:"GroupId"`
	MemberToDelAccount []string `json:"MemberToDel_Account"`
	Silence            int      `json:"Silence,omitempty"`
	Reason             string   `json:"Reason,omitempty"`
}

type forbidSendMsgRequest struct {
	GroupID        string   `json:"GroupId"`
	MemberAccounts []string `json:"Members_Account"`
	MuteTime       uint32   `json:"MuteTime"`
}

type kickUserRequest struct {
	UserID string `json:"UserID"`
}

type messageElement struct {
	MsgType    string           `json:"MsgType"`
	MsgContent customMsgContent `json:"MsgContent"`
}

type customMsgContent struct {
	Data  string `json:"Data"`
	Desc  string `json:"Desc,omitempty"`
	Ext   string `json:"Ext,omitempty"`
	Sound string `json:"Sound,omitempty"`
}
