package tencentim

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

const (
	// DefaultEndpoint 是腾讯云 IM 国内 REST 默认入口；海外部署可在配置中覆盖为对应地域域名。
	DefaultEndpoint = "console.tim.qq.com"
	// DefaultGroupType 使用 ChatRoom，保留消息历史能力；AVChatRoom 不适合需要补偿和漫游的房间消息。
	DefaultGroupType = "ChatRoom"
	// createGroupCommand 是建群 REST 命令；房间群、全局播报群、区域播报群都复用这一个能力。
	createGroupCommand = "v4/group_open_http_svc/create_group"
	// sendGroupMsgCommand 是服务端发群消息 REST 命令；播报只走服务端管理员账号，不开放客户端直发。
	sendGroupMsgCommand = "v4/group_open_http_svc/send_group_msg"
	// destroyGroupCommand 用于真实 IM smoke test 或明确运维动作的临时群清理，业务代码不应自动解散播报群。
	destroyGroupCommand = "v4/group_open_http_svc/destroy_group"
	// deleteGroupMemberCommand 用于把已离房用户从腾讯群里移除，防止 IM 群成员残留扩大消息面。
	deleteGroupMemberCommand = "v4/group_open_http_svc/delete_group_member"
	// kickUserCommand 让指定账号现有 IM 登录态失效；被封禁用户需要立即从腾讯 IM SDK 下线。
	kickUserCommand = "v4/im_open_login_svc/kick"
)

// 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
}

// RoomEvent 是 room-service 发给腾讯 IM 的房间系统消息负载。
type RoomEvent struct {
	EventID      string            `json:"event_id"`
	RoomID       string            `json:"room_id"`
	EventType    string            `json:"event_type"`
	ActorUserID  int64             `json:"actor_user_id,omitempty"`
	TargetUserID int64             `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"`
	Attributes   map[string]string `json:"attributes,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.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")
	}

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

	return c.PublishGroupCustomMessage(ctx, CustomGroupMessage{
		GroupID:     event.RoomID,
		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:          randomUint32(),
		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()
}

// 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)
}

// 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)
}

func (c *RESTClient) post(ctx context.Context, command string, payload any, out *restResponse) 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 {
		return 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"`
}

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 fmt.Errorf("tencent im rest failed: code=%d info=%s", r.ErrorCode, r.ErrorInfo)
}

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 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 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"`
}
