// Package teamspeak 提供 gomobile 友好的 TeamSpeak 客户端 API。
//
// gomobile 限制：
//   - 不支持 []string、[]*T 等复杂切片类型导出
//   - 不支持 Go 的 error 类型，改用 string 返回错误
//   - 回调必须通过 interface 定义
//   - 复杂数据通过 JSON 字符串传递
package teamspeak

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"sync"
	"time"

	ts "github.com/honeybbq/teamspeak-go"
	"github.com/honeybbq/teamspeak-go/crypto"
)

// queuedEvent 封装待处理的事件，用于 JNI 回调串行化
type queuedEvent struct {
	evtType string
	data    any
}

// eventQueue owns one callback consumer. A new connection receives a fresh
// instance so a stopped consumer can never resume against a later session.
const maxQueuedPCMEvents = 12

type eventQueue struct {
	mu       sync.Mutex
	cond     *sync.Cond
	events   []queuedEvent
	callback EventCallback
	stopped  bool
	pcmCount int
	stopOnce sync.Once
}

func newEventQueue(callback EventCallback) *eventQueue {
	q := &eventQueue{callback: callback}
	q.cond = sync.NewCond(&q.mu)
	return q
}

// ─── 内部数据类型（不导出给 gomobile） ─────────────────────

// serverInfoJSON 用于 JSON 序列化的服务器信息
type serverInfoJSON struct {
	Name                string `json:"name"`
	WelcomeMessage      string `json:"welcomeMessage"`
	MaxClients          int    `json:"maxClients"`
	ClientsOnline       int    `json:"clientsOnline"`
	ChannelsOnline      int    `json:"channelsOnline"`
	Uptime              string `json:"uptime"`
	Version             string `json:"version"`
	Platform            string `json:"platform"`
	Created             string `json:"created"`
	IconID              string `json:"iconId"`
	DefaultServerGroup  int    `json:"defaultServerGroup"`
	DefaultChannelGroup int    `json:"defaultChannelGroup"`
}

// channelDetailedInfoJSON 用于 JSON 序列化的单频道详情（来自 channelinfo cid=X）
type channelDetailedInfoJSON struct {
	ID               string `json:"id"`
	ParentID         string `json:"parentId"`
	Name             string `json:"name"`
	Topic            string `json:"topic"`
	Description      string `json:"description"`
	Codec            int    `json:"codec"`
	CodecQuality     int    `json:"codecQuality"`
	MaxClients       int    `json:"maxClients"`
	MaxFamilyClients int    `json:"maxFamilyClients"`
	NeededTalkPower  int    `json:"neededTalkPower"`
	IconID           string `json:"iconId"`
	IsPermanent      bool   `json:"isPermanent"`
	IsSemiPermanent  bool   `json:"isSemiPermanent"`
	IsDefault        bool   `json:"isDefault"`
	IsPassword       bool   `json:"isPassword"`
	Order            string `json:"order"`
	BannerGfxURL     string `json:"bannerGfxUrl"`
}

// clientDetailedInfoJSON 用于 JSON 序列化的单客户端详情（来自 clientinfo clid=X）
type clientDetailedInfoJSON struct {
	ID               string   `json:"id"`
	Nickname         string   `json:"nickname"`
	UID              string   `json:"uid"`
	ChannelID        string   `json:"channelId"`
	Type             int      `json:"type"`
	ServerGroups     []string `json:"serverGroups"`
	Away             bool     `json:"away"`
	AwayMessage      string   `json:"awayMessage"`
	InputMuted       bool     `json:"inputMuted"`
	OutputMuted      bool     `json:"outputMuted"`
	Platform         string   `json:"platform"`
	Version          string   `json:"version"`
	IP               string   `json:"ip"`
	Created          string   `json:"created"`
	LastConnected    string   `json:"lastConnected"`
	TotalConnections int      `json:"totalConnections"`
	Description      string   `json:"description"`
	IconID           string   `json:"iconId"`
}

// dbClientJSON 用于 JSON 序列化的数据库客户端
type dbClientJSON struct {
	DBID             string `json:"dbid"`
	UID              string `json:"uid"`
	Nickname         string `json:"nickname"`
	Created          string `json:"created"`
	LastConnected    string `json:"lastConnected"`
	TotalConnections int    `json:"totalConnections"`
	Description      string `json:"description"`
}

// banEntryJSON 用于 JSON 序列化的 ban 条目
type banEntryJSON struct {
	BanID       string `json:"banId"`
	IP          string `json:"ip"`
	Name        string `json:"name"`
	UID         string `json:"uid"`
	Created     string `json:"created"`
	InvokerName string `json:"invokerName"`
	InvokerUID  string `json:"invokerUid"`
	Reason      string `json:"reason"`
	Enforcement bool   `json:"enforcement"`
}

// fileEntryJSON 用于 JSON 序列化的文件条目
type fileEntryJSON struct {
	Name     string `json:"name"`
	Size     string `json:"size"`
	DateTime string `json:"dateTime"`
	IsFile   bool   `json:"isFile"`
}

// tokenEntryJSON 用于 JSON 序列化的 token 条目
type tokenEntryJSON struct {
	Token       string `json:"token"`
	TokenType   int    `json:"tokenType"`
	TokenID1    string `json:"tokenId1"`
	TokenID2    string `json:"tokenId2"`
	Created     string `json:"created"`
	Description string `json:"description"`
}

// complaintEntryJSON 用于 JSON 序列化的投诉条目
type complaintEntryJSON struct {
	FromDBID  string `json:"fromDbid"`
	ToDBID    string `json:"toDbid"`
	Message   string `json:"message"`
	Timestamp string `json:"timestamp"`
}

// channelJSON 用于 JSON 序列化的频道信息
type channelJSON struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	ParentID    string `json:"parentId"`
	Description string `json:"description"`
	IsPassword  bool   `json:"isPassword"`
}

// channelDetailedJSON 用于 JSON 序列化的详细频道信息（含 flags、voice、limits、icon）
type channelDetailedJSON struct {
	ID                          string `json:"id"`
	Name                        string `json:"name"`
	ParentID                    string `json:"parentId"`
	Topic                       string `json:"topic"`
	Order                       string `json:"order"`
	Codec                       int    `json:"codec"`
	CodecQuality                int    `json:"codecQuality"`
	NeededTalkPower             int    `json:"neededTalkPower"`
	MaxClients                  int    `json:"maxClients"`
	MaxFamilyClients            int    `json:"maxFamilyClients"`
	IsMaxClientsUnlimited       bool   `json:"isMaxClientsUnlimited"`
	IsMaxFamilyClientsUnlimited bool   `json:"isMaxFamilyClientsUnlimited"`
	IsPermanent                 bool   `json:"isPermanent"`
	IsSemiPermanent             bool   `json:"isSemiPermanent"`
	IsDefault                   bool   `json:"isDefault"`
	IsPassword                  bool   `json:"isPassword"`
	IsOrdered                   bool   `json:"isOrdered"`
	IconID                      string `json:"iconId"`
	NeededModifyPower           int    `json:"neededModifyPower"`
}

// clientJSON 用于 JSON 序列化的客户端信息
type clientJSON struct {
	ID           int      `json:"id"`
	Nickname     string   `json:"nickname"`
	UID          string   `json:"uid"`
	ChannelID    string   `json:"channelId"`
	ServerGroups []string `json:"serverGroups"`
	IsSelf       bool     `json:"isSelf"`
}

// ─── 数据类型（gomobile 可导出） ────────────────────────────

// Channel 频道信息（gomobile 导出）
type Channel struct {
	ID          string
	Name        string
	ParentID    string
	Description string
}

// Client 客户端信息（gomobile 导出）
type Client struct {
	ID        int
	Nickname  string
	UID       string
	ChannelID string
	IsSelf    bool
}

// TextMsg 文字消息（gomobile 导出）
type TextMsg struct {
	InvokerName string
	InvokerUID  string
	Message     string
	InvokerID   int
	TargetMode  int
	TargetID    string
}

// PokeEvent Poke 事件（gomobile 导出）
type PokeEvent struct {
	InvokerID   int
	InvokerName string
	InvokerUID  string
	Message     string
}

// ─── 事件回调接口 ─────────────────────────────────────────

// EventCallback 事件回调接口，Android 端实现此接口接收事件
type EventCallback interface {
	OnConnected()
	OnDisconnected(message string)
	OnTextMessage(msg *TextMsg)
	OnClientEnter(client *Client)
	OnClientLeave(id int, reasonMsg string)
	OnClientMoved(id int, targetChannelID string)
	OnKicked(reason string)
	OnVoiceData(clientID int, data []byte, codec int, sequence int, isWhisper bool)
	// OnMixedVoicePCM receives a 20 ms, 48 kHz interleaved stereo PCM16 little-endian frame.
	OnMixedVoicePCM(pcm []byte)
	OnClientSpeaking(clientID int, speaking bool)
	OnPoked(event *PokeEvent)
}

// ─── TSClient 主类 ────────────────────────────────────────

// TSClient TeamSpeak 客户端封装
type TSClient struct {
	mu        sync.Mutex
	client    *ts.Client
	callback  EventCallback
	connected bool
	host      string // 服务器地址（用于文件传输 TCP 连接）
	selfUID   string // 当前连接的 TeamSpeak UID，用于识别同 identity 的残留 session

	// 事件队列：保证所有 JNI 回调在同一个协程中顺序执行
	evtQueueMu sync.Mutex // protects eventQueue replacement
	evtQueue   *eventQueue

	// receiveFactory is platform-owned and intentionally private so gomobile
	// never exposes the native decoder implementation. Android supplies a
	// libopus-backed factory; unsupported host builds leave it nil.
	receiveFactory voiceDecoderFactory
	receive        *receiveAudio
}

// NewClient 创建新的 TeamSpeak 客户端
func NewClient() *TSClient {
	factory := newPlatformVoiceDecoderFactory()
	log.Printf("[TSVoice] platform decoder factory=%T available=%t", factory, factory != nil)
	return &TSClient{receiveFactory: factory}
}

// Connect 连接到 TeamSpeak 服务器
// 返回空字符串表示成功，非空为错误信息
func (c *TSClient) Connect(host, nickname, password, defaultChannel, defaultChannelPassword string, cb EventCallback) string {
	// Ensure repeated attempts on this wrapper cannot leave a prior transport alive.
	c.Disconnect()
	// A prior failed or disconnected session may have left its consumer waiting.
	// Stop it before replacing the callback and starting this session's queue.
	c.stopEventQueue()

	c.mu.Lock()
	c.callback = cb
	c.connected = false
	c.host = host
	c.mu.Unlock()
	c.initEventQueue()
	c.ensureReceiveFactory()
	c.StartReceiveAudio()

	identity, err := crypto.GenerateIdentity(8)
	if err != nil {
		return fmt.Sprintf("生成身份失败: %v", err)
	}

	// 计算 TeamSpeak UID 并存储，用于僵尸会话过滤（isStaleSelfSession）。
	c.mu.Lock()
	c.selfUID = crypto.GetUidFromPublicKey(identity.PublicKeyBase64())
	c.mu.Unlock()

	opts := []ts.ClientOption{}
	if password != "" {
		opts = append(opts, ts.WithServerPassword(password))
	}
	if defaultChannel != "" {
		opts = append(opts, ts.WithDefaultChannel(defaultChannel))
	}
	if defaultChannelPassword != "" {
		opts = append(opts, ts.WithDefaultChannelPassword(defaultChannelPassword))
	}

	client := ts.NewClient(identity, host, nickname, opts...)

	c.mu.Lock()
	c.client = client
	c.mu.Unlock()

	// 注册事件
	c.registerEvents(client)

	if err := client.Connect(); err != nil {
		c.Disconnect()
		return fmt.Sprintf("连接失败: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	if err := client.WaitConnected(ctx); err != nil {
		c.Disconnect()
		return fmt.Sprintf("连接失败: %v", err)
	}

	return ""
}

// Disconnect 断开连接
func (c *TSClient) Disconnect() {
	c.StopReceiveAudio()
	// 停止事件消费协程
	c.stopEventQueue()

	c.mu.Lock()
	client := c.client
	c.client = nil
	c.connected = false
	c.mu.Unlock()

	if client != nil {
		if err := client.Disconnect(); err != nil {
			log.Printf("[TSBridge] disconnect error: %v", err)
		}
	}
}

// IsConnected 是否已连接
func (c *TSClient) IsConnected() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.connected && c.client != nil
}

// GetChannelsJSON 获取频道列表（JSON 格式）
// gomobile 不支持返回 []*Channel，所以用 JSON 字符串传递
func (c *TSClient) GetChannelsJSON() string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		log.Printf("[TSBridge] GetChannelsJSON: client is nil")
		return "[]"
	}

	log.Printf("[TSBridge] GetChannelsJSON: calling ListChannelsDetailed...")
	channels, err := client.ListChannelsDetailed()
	if err != nil {
		log.Printf("[TSBridge] ListChannelsDetailed error: %v", err)
		return "[]"
	}
	log.Printf("[TSBridge] ListChannelsDetailed returned %d channels", len(channels))

	result := make([]channelJSON, len(channels))
	for i, ch := range channels {
		parentID := "0"
		if ch.ParentID != 0 {
			parentID = fmt.Sprintf("%d", ch.ParentID)
		}
		result[i] = channelJSON{
			ID:         fmt.Sprintf("%d", ch.ID),
			Name:       ch.Name,
			ParentID:   parentID,
			IsPassword: ch.IsPassword,
		}
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "[]"
	}
	return string(data)
}

// GetChannelsDetailedJSON 获取详细频道列表（JSON 格式）
// 通过 channellist -topic -flags -voice -limits -icon 获取更丰富的频道属性，
// 包括：密码标志、编解码器、人数限制、永久/半永久标志、图标 ID 等。
// gomobile 不支持返回 []*ChannelInfoDetailed，所以用 JSON 字符串传递。
func (c *TSClient) GetChannelsDetailedJSON() string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "[]"
	}

	channels, err := client.ListChannelsDetailed()
	if err != nil {
		log.Printf("[TSBridge] ListChannelsDetailed error: %v", err)
		return "[]"
	}

	result := make([]channelDetailedJSON, len(channels))
	for i, ch := range channels {
		parentID := "0"
		if ch.ParentID != 0 {
			parentID = fmt.Sprintf("%d", ch.ParentID)
		}
		result[i] = channelDetailedJSON{
			ID:                          fmt.Sprintf("%d", ch.ID),
			Name:                        ch.Name,
			ParentID:                    parentID,
			Topic:                       ch.Topic,
			Order:                       fmt.Sprintf("%d", ch.Order),
			Codec:                       ch.Codec,
			CodecQuality:                ch.CodecQuality,
			NeededTalkPower:             ch.NeededTalkPower,
			MaxClients:                  ch.MaxClients,
			MaxFamilyClients:            ch.MaxFamilyClients,
			IsMaxClientsUnlimited:       ch.IsMaxClientsUnlimited,
			IsMaxFamilyClientsUnlimited: ch.IsMaxFamilyClientsUnlimited,
			IsPermanent:                 ch.IsPermanent,
			IsSemiPermanent:             ch.IsSemiPermanent,
			IsDefault:                   ch.IsDefault,
			IsPassword:                  ch.IsPassword,
			IsOrdered:                   ch.Order > 0,
			IconID:                      fmt.Sprintf("%d", ch.IconID),
			NeededModifyPower:           ch.NeededModifyPower,
		}
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "[]"
	}
	return string(data)
}

// GetClientsJSON 获取在线客户端列表（JSON 格式）
// gomobile 不支持返回 []*Client，所以用 JSON 字符串传递
func (c *TSClient) GetClientsJSON() string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "[]"
	}

	clients, err := client.ListClients()
	if err != nil {
		log.Printf("[TSBridge] ListClients error: %v", err)
		return "[]"
	}

	myID := client.ClientID()
	result := make([]clientJSON, len(clients))
	for i, cl := range clients {
		result[i] = clientJSON{
			ID:           int(cl.ID),
			Nickname:     cl.Nickname,
			UID:          cl.UID,
			ChannelID:    fmt.Sprintf("%d", cl.ChannelID),
			ServerGroups: cl.ServerGroups,
			IsSelf:       cl.ID == myID,
		}
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "[]"
	}
	return string(data)
}

// ─── 服务器信息 ────────────────────────────────────────────

// GetServerInfoJSON 获取服务器信息（JSON 格式）
// gomobile 不支持返回 *ServerInfo，所以用 JSON 字符串传递
func (c *TSClient) GetServerInfoJSON() string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "{}"
	}

	info, err := client.GetServerInfo()
	if err != nil {
		log.Printf("[TSBridge] GetServerInfo error: %v", err)
		return "{}"
	}

	result := serverInfoJSON{
		Name:                info.Name,
		WelcomeMessage:      info.WelcomeMessage,
		MaxClients:          info.MaxClients,
		ClientsOnline:       info.ClientsOnline,
		ChannelsOnline:      info.ChannelsOnline,
		Uptime:              fmt.Sprintf("%d", info.Uptime),
		Version:             info.Version,
		Platform:            info.Platform,
		Created:             fmt.Sprintf("%d", info.Created),
		IconID:              fmt.Sprintf("%d", info.IconID),
		DefaultServerGroup:  info.DefaultServerGroup,
		DefaultChannelGroup: info.DefaultChannelGroup,
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "{}"
	}
	return string(data)
}

// ─── 频道详情 ──────────────────────────────────────────────

// GetChannelDetailInfoJSON 获取单个频道的详细信息（JSON 格式）
// 通过 channelinfo cid=X 获取完整的频道属性（含 description）。
// 返回空字符串表示错误。
func (c *TSClient) GetChannelDetailInfoJSON(channelIDStr string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "{}"
	}

	var channelID uint64
	if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
		return "{}"
	}

	info, err := client.GetChannelInfo(channelID)
	if err != nil {
		log.Printf("[TSBridge] GetChannelInfo error: %v", err)
		return "{}"
	}

	result := channelDetailedInfoJSON{
		ID:               fmt.Sprintf("%d", info.ID),
		ParentID:         fmt.Sprintf("%d", info.ParentID),
		Name:             info.Name,
		Topic:            info.Topic,
		Description:      info.Description,
		Codec:            info.Codec,
		CodecQuality:     info.CodecQuality,
		MaxClients:       info.MaxClients,
		MaxFamilyClients: info.MaxFamilyClients,
		NeededTalkPower:  info.NeededTalkPower,
		IconID:           fmt.Sprintf("%d", info.IconID),
		IsPermanent:      info.IsPermanent,
		IsSemiPermanent:  info.IsSemiPermanent,
		IsDefault:        info.IsDefault,
		IsPassword:       info.IsPassword,
		Order:            fmt.Sprintf("%d", info.Order),
		BannerGfxURL:     info.BannerGfxURL,
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "{}"
	}
	return string(data)
}

// ─── 客户端详情 ────────────────────────────────────────────

// GetClientDetailInfoJSON 获取单个客户端的详细信息（JSON 格式）
// 通过 clientinfo clid=X 获取完整的客户端属性。
func (c *TSClient) GetClientDetailInfoJSON(clid int) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "{}"
	}

	info, err := client.GetClientDetailInfo(uint16(clid))
	if err != nil {
		log.Printf("[TSBridge] GetClientDetailInfo error: %v", err)
		return "{}"
	}

	result := clientDetailedInfoJSON{
		ID:               fmt.Sprintf("%d", info.ID),
		Nickname:         info.Nickname,
		UID:              info.UID,
		ChannelID:        fmt.Sprintf("%d", info.ChannelID),
		Type:             info.Type,
		ServerGroups:     info.ServerGroups,
		Away:             info.Away,
		AwayMessage:      info.AwayMessage,
		InputMuted:       info.InputMuted,
		OutputMuted:      info.OutputMuted,
		Platform:         info.Platform,
		Version:          info.Version,
		IP:               info.IP,
		Created:          fmt.Sprintf("%d", info.Created),
		LastConnected:    fmt.Sprintf("%d", info.LastConnected),
		TotalConnections: info.TotalConnections,
		Description:      info.Description,
		IconID:           fmt.Sprintf("%d", info.IconID),
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "{}"
	}
	return string(data)
}

// ─── 数据库客户端列表 ──────────────────────────────────────

// ListDBClientsJSON 获取服务器数据库中的客户端列表（JSON 格式）
// start: 起始位置, duration: 返回数量（0=全部）
func (c *TSClient) ListDBClientsJSON(start, duration int) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "[]"
	}

	clients, err := client.ListDBClients(start, duration)
	if err != nil {
		log.Printf("[TSBridge] ListDBClients error: %v", err)
		return "[]"
	}

	result := make([]dbClientJSON, len(clients))
	for i, cl := range clients {
		result[i] = dbClientJSON{
			DBID:             fmt.Sprintf("%d", cl.DBID),
			UID:              cl.UID,
			Nickname:         cl.Nickname,
			Created:          fmt.Sprintf("%d", cl.Created),
			LastConnected:    fmt.Sprintf("%d", cl.LastConnected),
			TotalConnections: cl.TotalConnections,
			Description:      cl.Description,
		}
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "[]"
	}
	return string(data)
}

// ─── Ban 管理 ─────────────────────────────────────────────

// ListBansJSON 获取 ban 列表（JSON 格式）
func (c *TSClient) ListBansJSON() string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "[]"
	}

	bans, err := client.ListBans()
	if err != nil {
		log.Printf("[TSBridge] ListBans error: %v", err)
		return "[]"
	}

	result := make([]banEntryJSON, len(bans))
	for i, b := range bans {
		result[i] = banEntryJSON{
			BanID:       fmt.Sprintf("%d", b.BanID),
			IP:          b.IP,
			Name:        b.Name,
			UID:         b.UID,
			Created:     fmt.Sprintf("%d", b.Created),
			InvokerName: b.InvokerName,
			InvokerUID:  b.InvokerUID,
			Reason:      b.Reason,
			Enforcement: b.Enforcement,
		}
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "[]"
	}
	return string(data)
}

// AddBan 添加 ban 条目。ip/name/uid 至少指定一个。timeSeconds=0 为永久。
// 返回空字符串表示成功。
func (c *TSClient) AddBan(ip, name, uid string, timeSeconds int, reason string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	if err := client.AddBan(ip, name, uid, timeSeconds, reason); err != nil {
		return fmt.Sprintf("添加失败: %v", err)
	}
	return ""
}

// DeleteBan 删除 ban 条目。返回空字符串表示成功。
func (c *TSClient) DeleteBan(banIDStr string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	var banID int64
	if _, err := fmt.Sscanf(banIDStr, "%d", &banID); err != nil {
		return fmt.Sprintf("无效的 ban ID: %v", err)
	}

	if err := client.DeleteBan(banID); err != nil {
		return fmt.Sprintf("删除失败: %v", err)
	}
	return ""
}

// ─── Token 管理 ───────────────────────────────────────────

// ListTokensJSON 获取 token 列表（JSON 格式）
func (c *TSClient) ListTokensJSON() string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "[]"
	}

	tokens, err := client.ListTokens()
	if err != nil {
		log.Printf("[TSBridge] ListTokens error: %v", err)
		return "[]"
	}

	result := make([]tokenEntryJSON, len(tokens))
	for i, t := range tokens {
		result[i] = tokenEntryJSON{
			Token:       t.Token,
			TokenType:   t.TokenType,
			TokenID1:    fmt.Sprintf("%d", t.TokenID1),
			TokenID2:    fmt.Sprintf("%d", t.TokenID2),
			Created:     fmt.Sprintf("%d", t.Created),
			Description: t.Description,
		}
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "[]"
	}
	return string(data)
}

// UseToken 使用一个 privilege key。返回空字符串表示成功。
func (c *TSClient) UseToken(token string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	if err := client.UseToken(token); err != nil {
		return fmt.Sprintf("使用失败: %v", err)
	}
	return ""
}

// ─── 文件列表 ─────────────────────────────────────────────

// ListFilesJSON 获取频道目录下的文件列表（JSON 格式）
// path: 虚拟路径，根目录为 "/"
func (c *TSClient) ListFilesJSON(channelIDStr, path string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "[]"
	}

	var channelID uint64
	if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
		return "[]"
	}

	files, err := client.ListFiles(channelID, path)
	if err != nil {
		log.Printf("[TSBridge] ListFiles error: %v", err)
		return "[]"
	}

	result := make([]fileEntryJSON, len(files))
	for i, f := range files {
		result[i] = fileEntryJSON{
			Name:     f.Name,
			Size:     fmt.Sprintf("%d", f.Size),
			DateTime: fmt.Sprintf("%d", f.DateTime),
			IsFile:   f.IsFile,
		}
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "[]"
	}
	return string(data)
}

// ─── 投诉管理 ─────────────────────────────────────────────

// ListComplaintsJSON 获取投诉列表（JSON 格式）
// targetDBIDStr: 目标客户端 DBID，"0" 表示返回全部投诉
func (c *TSClient) ListComplaintsJSON(targetDBIDStr string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "[]"
	}

	var targetDBID uint64
	if targetDBIDStr != "" && targetDBIDStr != "0" {
		if _, err := fmt.Sscanf(targetDBIDStr, "%d", &targetDBID); err != nil {
			return "[]"
		}
	}

	complaints, err := client.ListComplaints(targetDBID)
	if err != nil {
		log.Printf("[TSBridge] ListComplaints error: %v", err)
		return "[]"
	}

	result := make([]complaintEntryJSON, len(complaints))
	for i, c := range complaints {
		result[i] = complaintEntryJSON{
			FromDBID:  fmt.Sprintf("%d", c.FromDBID),
			ToDBID:    fmt.Sprintf("%d", c.ToDBID),
			Message:   c.Message,
			Timestamp: fmt.Sprintf("%d", c.Timestamp),
		}
	}

	data, err := json.Marshal(result)
	if err != nil {
		log.Printf("[TSBridge] JSON marshal error: %v", err)
		return "[]"
	}
	return string(data)
}

// AddComplaint 提交投诉。返回空字符串表示成功。
func (c *TSClient) AddComplaint(targetDBIDStr, message string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	var targetDBID uint64
	if _, err := fmt.Sscanf(targetDBIDStr, "%d", &targetDBID); err != nil {
		return fmt.Sprintf("无效的 DBID: %v", err)
	}

	if err := client.AddComplaint(targetDBID, message); err != nil {
		return fmt.Sprintf("提交失败: %v", err)
	}
	return ""
}

// ─── 频道管理（创建/编辑/删除） ───────────────────────────

// CreateChannelJSON 创建新频道。propertiesJSON 为 JSON 对象，
// 包含可选的频道属性，如 channel_topic、channel_flag_permanent 等。
// 返回新频道的 ID 字符串，空字符串表示失败。
func (c *TSClient) CreateChannelJSON(name, propertiesJSON string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return ""
	}

	var props map[string]string
	if propertiesJSON != "" && propertiesJSON != "{}" {
		if err := json.Unmarshal([]byte(propertiesJSON), &props); err != nil {
			log.Printf("[TSBridge] CreateChannel properties parse error: %v", err)
			return ""
		}
	}

	cid, err := client.CreateChannel(name, props)
	if err != nil {
		log.Printf("[TSBridge] CreateChannel error: %v", err)
		return ""
	}
	return fmt.Sprintf("%d", cid)
}

// EditChannelJSON 编辑频道属性。propertiesJSON 为 JSON 对象。
// 返回空字符串表示成功，非空为错误。
func (c *TSClient) EditChannelJSON(channelIDStr, propertiesJSON string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	var channelID uint64
	if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
		return fmt.Sprintf("无效的频道 ID: %v", err)
	}

	var props map[string]string
	if err := json.Unmarshal([]byte(propertiesJSON), &props); err != nil {
		return fmt.Sprintf("属性解析失败: %v", err)
	}

	if err := client.EditChannel(channelID, props); err != nil {
		return fmt.Sprintf("编辑失败: %v", err)
	}
	return ""
}

// DeleteChannel 删除频道。force=true 强制删除（含子频道）。
// 返回空字符串表示成功。
func (c *TSClient) DeleteChannel(channelIDStr string, force bool) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	var channelID uint64
	if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
		return fmt.Sprintf("无效的频道 ID: %v", err)
	}

	if err := client.DeleteChannel(channelID, force); err != nil {
		return fmt.Sprintf("删除失败: %v", err)
	}
	return ""
}

// UpdateSelfJSON 更新当前客户端属性。propertiesJSON 为 JSON 对象，
// 如 {"client_away": "1", "client_away_message": "AFK"}。
// 返回空字符串表示成功。
func (c *TSClient) UpdateSelfJSON(propertiesJSON string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	var props map[string]string
	if err := json.Unmarshal([]byte(propertiesJSON), &props); err != nil {
		return fmt.Sprintf("属性解析失败: %v", err)
	}

	if err := client.UpdateSelf(props); err != nil {
		return fmt.Sprintf("更新失败: %v", err)
	}
	return ""
}

// KickClient 踢出客户端。reasonID: 4=频道踢出, 5=服务器踢出。
// 返回空字符串表示成功。
func (c *TSClient) KickClient(clid int, reasonID int, reasonMsg string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	if err := client.KickClient(uint16(clid), reasonID, reasonMsg); err != nil {
		return fmt.Sprintf("踢出失败: %v", err)
	}
	return ""
}

// GetClientID 获取当前客户端 ID
func (c *TSClient) GetClientID() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.client == nil {
		return 0
	}
	return int(c.client.ClientID())
}

// GetChannelID 获取当前频道 ID
func (c *TSClient) GetChannelID() string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "0"
	}

	// 通过 GetClientInfo 获取当前频道 ID
	info, err := client.GetClientInfo(client.ClientID())
	if err != nil {
		return "0"
	}
	if cid, ok := info["cid"]; ok {
		return cid
	}
	return "0"
}

// SendChannelMessage 发送频道文字消息
// 返回空字符串表示成功
func (c *TSClient) SendChannelMessage(channelIDStr, message string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	var channelID uint64
	if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
		return fmt.Sprintf("无效的频道 ID: %v", err)
	}

	if err := client.SendTextMessage(2, channelID, message); err != nil {
		return fmt.Sprintf("发送失败: %v", err)
	}
	return ""
}

// MoveToChannel 移动到指定频道
// 返回空字符串表示成功
func (c *TSClient) MoveToChannel(channelIDStr, password string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	var channelID uint64
	if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
		return fmt.Sprintf("无效的频道 ID: %v", err)
	}

	if err := client.ClientMove(client.ClientID(), channelID, password); err != nil {
		return fmt.Sprintf("移动失败: %v", err)
	}
	return ""
}

// SendTextMessage 发送文字消息
// targetMode: 1=私聊, 2=频道, 3=服务器
// 返回空字符串表示成功
func (c *TSClient) SendTextMessage(targetMode int, targetIDStr string, message string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	var targetID uint64
	if _, err := fmt.Sscanf(targetIDStr, "%d", &targetID); err != nil {
		return fmt.Sprintf("无效的目标 ID: %v", err)
	}

	if err := client.SendTextMessage(targetMode, targetID, message); err != nil {
		return fmt.Sprintf("发送失败: %v", err)
	}
	return ""
}

// Poke 向指定客户端发送 Poke 消息
// 返回空字符串表示成功
func (c *TSClient) Poke(clidStr string, message string) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	var clid uint64
	if _, err := fmt.Sscanf(clidStr, "%d", &clid); err != nil {
		return fmt.Sprintf("无效的客户端 ID: %v", err)
	}

	if err := client.Poke(uint16(clid), message); err != nil {
		return fmt.Sprintf("Poke 失败: %v", err)
	}
	return ""
}

// setReceiveDecoderFactory installs the decoder provider used by
// StartReceiveAudio. It remains internal because VoiceDecoderFactory is a Go
// implementation detail and must not be exported through gomobile.
func (c *TSClient) ensureReceiveFactory() {
	c.mu.Lock()
	if c.receiveFactory == nil {
		c.receiveFactory = newPlatformVoiceDecoderFactory()
		log.Printf("[TSVoice] receive factory initialized lazily type=%T available=%t", c.receiveFactory, c.receiveFactory != nil)
	}
	c.mu.Unlock()
}

func (c *TSClient) setReceiveDecoderFactory(factory voiceDecoderFactory) {
	c.mu.Lock()
	c.receiveFactory = factory
	if c.receive == nil && factory != nil {
		c.receive = newReceiveAudio(factory, c.queueMixedPCM, c.queueSpeaking)
	}
	c.mu.Unlock()
}

// StartReceiveAudio starts the Go-owned receive pipeline when this platform
// provides a decoder factory. A fresh worker is created after a prior stop, so
// speaker toggles and reconnects can safely restart receiving.
func (c *TSClient) StartReceiveAudio() {
	c.ensureReceiveFactory()
	c.mu.Lock()
	if c.receive == nil && c.receiveFactory != nil {
		c.receive = newReceiveAudio(c.receiveFactory, c.queueMixedPCM, c.queueSpeaking)
	}
	r := c.receive
	factoryAvailable := c.receiveFactory != nil
	c.mu.Unlock()
	log.Printf("[TSVoice] StartReceiveAudio worker=%t factory=%t", r != nil, factoryAvailable)
	if r != nil {
		r.start()
	}
}

// StopReceiveAudio stops the receive worker and waits for it to release all
// decoders. Repeated calls are harmless.
func (c *TSClient) StopReceiveAudio() {
	c.mu.Lock()
	r := c.receive
	c.receive = nil
	c.mu.Unlock()
	if r != nil {
		r.stopAndWait()
	}
}

// SetRemoteClientMuted excludes a remote client from the final mix while it
// continues receiving, decoding, and reporting speaking state.
func (c *TSClient) SetRemoteClientMuted(clientID int, muted bool) {
	c.receiveCommand(audioCommand{kind: "mute", clientID: clientID, muted: muted})
}
func (c *TSClient) RemoveRemoteAudioClient(clientID int) {
	c.receiveCommand(audioCommand{kind: "remove", clientID: clientID})
}
func (c *TSClient) ClearRemoteAudioClients() { c.receiveCommand(audioCommand{kind: "clear"}) }
func (c *TSClient) receiveCommand(cmd audioCommand) {
	c.mu.Lock()
	r := c.receive
	c.mu.Unlock()
	if r != nil {
		r.command(cmd)
	}
}

// SendVoice 发送 Opus 语音帧
// codec: 4=Opus Voice, 5=Opus Music
// 返回空字符串表示成功
func (c *TSClient) SendVoice(data []byte, codec int64) string {
	c.mu.Lock()
	client := c.client
	c.mu.Unlock()

	if client == nil {
		return "未连接"
	}

	if err := client.SendVoice(data, byte(codec)); err != nil {
		return fmt.Sprintf("发送失败: %v", err)
	}
	return ""
}

// initEventQueue initializes a fresh consumer for this connection session.
func (c *TSClient) initEventQueue() {
	c.mu.Lock()
	callback := c.callback
	c.mu.Unlock()
	q := newEventQueue(callback)
	c.evtQueueMu.Lock()
	c.evtQueue = q
	c.evtQueueMu.Unlock()
	c.startEventLoop(q)
}

// stopEventQueue stops and joins the current consumer. It is safe to call
// repeatedly and prevents stale sessions from dispatching into a new callback.
func (c *TSClient) stopEventQueue() {
	c.evtQueueMu.Lock()
	q := c.evtQueue
	c.evtQueue = nil
	c.evtQueueMu.Unlock()
	if q == nil {
		return
	}
	q.stopOnce.Do(func() {
		q.mu.Lock()
		q.stopped = true
		q.cond.Broadcast()
		q.mu.Unlock()
	})
}

// startEventLoop starts the callback consumer for q.
func (c *TSClient) startEventLoop(q *eventQueue) {
	go func() {
		for {
			q.mu.Lock()
			for len(q.events) == 0 && !q.stopped {
				q.cond.Wait()
			}
			if q.stopped {
				q.mu.Unlock()
				return
			}
			events := q.events
			q.events = nil
			q.pcmCount = 0
			q.mu.Unlock()

			for _, evt := range events {
				c.processEvent(q.callback, evt)
			}
		}
	}()
}

// queueEvent queues an event only while its session's consumer remains active.
func (c *TSClient) queueEvent(evt queuedEvent) {
	c.evtQueueMu.Lock()
	q := c.evtQueue
	c.evtQueueMu.Unlock()
	if q == nil {
		return
	}
	q.mu.Lock()
	if !q.stopped {
		q.events = append(q.events, evt)
		q.cond.Signal()
	}
	q.mu.Unlock()
}

// processEvent 处理单个事件（在消费协程中顺序执行）
func (c *TSClient) processEvent(cb EventCallback, evt queuedEvent) {
	if cb == nil {
		return
	}

	switch evt.evtType {
	case "connected":
		log.Printf("[TSEvent] connected")
		cb.OnConnected()
	case "disconnected":
		msg := evt.data.(string)
		log.Printf("[TSEvent] disconnected: %q", msg)
		cb.OnDisconnected(msg)
	case "textmessage":
		m := evt.data.(*TextMsg)
		log.Printf("[TSEvent] textmessage from=%q uid=%q mode=%d target=%s msg=%q", m.InvokerName, m.InvokerUID, m.TargetMode, m.TargetID, m.Message)
		cb.OnTextMessage(m)
	case "cliententer":
		cl := evt.data.(*Client)
		log.Printf("[TSEvent] cliententer id=%d nick=%q uid=%q channel=%s", cl.ID, cl.Nickname, cl.UID, cl.ChannelID)
		cb.OnClientEnter(cl)
	case "clientleave":
		d := evt.data.(struct {
			ID        int
			ReasonMsg string
		})
		log.Printf("[TSEvent] clientleave id=%d reason=%q", d.ID, d.ReasonMsg)
		cb.OnClientLeave(d.ID, d.ReasonMsg)
	case "clientmoved":
		d := evt.data.(struct {
			ID              int
			TargetChannelID string
		})
		log.Printf("[TSEvent] clientmoved id=%d targetChannel=%s", d.ID, d.TargetChannelID)
		cb.OnClientMoved(d.ID, d.TargetChannelID)
	case "kicked":
		reason := evt.data.(string)
		log.Printf("[TSEvent] kicked reason=%q", reason)
		cb.OnKicked(reason)
	case "voicedata":
		d := evt.data.(struct {
			ClientID  int
			Data      []byte
			Codec     int
			Sequence  int
			IsWhisper bool
		})
		cb.OnVoiceData(d.ClientID, d.Data, d.Codec, d.Sequence, d.IsWhisper)
	case "mixedvoicepcm":
		cb.OnMixedVoicePCM(evt.data.([]byte))
	case "clientspeaking":
		d := evt.data.(struct {
			ClientID int
			Speaking bool
		})
		cb.OnClientSpeaking(d.ClientID, d.Speaking)
	case "poked":
		p := evt.data.(*PokeEvent)
		log.Printf("[TSEvent] poked from=%q (id=%d uid=%q) msg=%q", p.InvokerName, p.InvokerID, p.InvokerUID, p.Message)
		cb.OnPoked(p)
	}
}

func (c *TSClient) queueMixedPCM(pcm []byte) {
	c.evtQueueMu.Lock()
	q := c.evtQueue
	c.evtQueueMu.Unlock()
	if q == nil {
		return
	}
	q.mu.Lock()
	if !q.stopped {
		if q.pcmCount >= maxQueuedPCMEvents {
			for i, evt := range q.events {
				if evt.evtType == "mixedvoicepcm" {
					copy(q.events[i:], q.events[i+1:])
					q.events = q.events[:len(q.events)-1]
					q.pcmCount--
					break
				}
			}
		}
		q.events = append(q.events, queuedEvent{evtType: "mixedvoicepcm", data: pcm})
		q.pcmCount++
		q.cond.Signal()
	}
	q.mu.Unlock()
}
func (c *TSClient) queueSpeaking(clientID int, speaking bool) {
	c.queueEvent(queuedEvent{evtType: "clientspeaking", data: struct {
		ClientID int
		Speaking bool
	}{clientID, speaking}})
}

// registerEvents 注册事件处理
// 所有事件通过队列传递，在消费协程中顺序调用 JNI 回调
func (c *TSClient) isCurrentClient(client *ts.Client) bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.client == client
}

// isStaleSelfSession checks if a remote client event belongs to a stale session
// using the same identity as the current connection. This happens when the previous
// process crashed without sending clientdisconnect, leaving a "zombie" session on
// the server that the new connection must isolate from.
func (c *TSClient) isStaleSelfSession(uid string, clid int) bool {
	c.mu.Lock()
	selfUID := c.selfUID
	selfClid := 0
	if c.client != nil {
		selfClid = int(c.client.ClientID())
	}
	c.mu.Unlock()

	if selfUID == "" || uid == "" || uid != selfUID {
		return false
	}
	return clid != selfClid
}

func (c *TSClient) queueClientEvent(client *ts.Client, evt queuedEvent) {
	if c.isCurrentClient(client) {
		c.queueEvent(evt)
	}
}

func (c *TSClient) registerEvents(client *ts.Client) {
	client.OnConnected(func() {
		c.mu.Lock()
		if c.client != client {
			c.mu.Unlock()
			return
		}
		c.connected = true
		c.mu.Unlock()
		c.queueEvent(queuedEvent{evtType: "connected"})
	})

	client.OnDisconnected(func(err error) {
		c.StopReceiveAudio()
		c.mu.Lock()
		if c.client != client {
			c.mu.Unlock()
			return
		}
		c.connected = false
		c.client = nil
		c.mu.Unlock()
		msg := ""
		if err != nil {
			msg = err.Error()
		}
		c.queueEvent(queuedEvent{evtType: "disconnected", data: msg})
	})

	client.OnTextMessage(func(msg ts.TextMessage) {
		c.queueClientEvent(client, queuedEvent{
			evtType: "textmessage",
			data: &TextMsg{
				InvokerName: msg.InvokerName,
				InvokerUID:  msg.InvokerUID,
				Message:     msg.Message,
				InvokerID:   int(msg.InvokerID),
				TargetMode:  msg.TargetMode,
				TargetID:    fmt.Sprintf("%d", msg.TargetID),
			},
		})
	})

	client.OnClientEnter(func(info ts.ClientInfo) {
		if c.isStaleSelfSession(info.UID, int(info.ID)) {
			log.Printf("[TSBridge] ignoring stale self session enter clid=%d uid=%s", info.ID, info.UID)
			return
		}
		c.queueClientEvent(client, queuedEvent{
			evtType: "cliententer",
			data: &Client{
				ID:        int(info.ID),
				Nickname:  info.Nickname,
				UID:       info.UID,
				ChannelID: fmt.Sprintf("%d", info.ChannelID),
				IsSelf:    false,
			},
		})
	})

	client.OnClientLeave(func(data ts.ClientLeftViewEvent) {
		// 不过滤僵尸的 leave 事件：GetClientsJSON 初始同步可能已包含僵尸，
		// 需要 leave 事件来更新 UI 将其移除。同时确保音频解码器被清理。
		if c.isStaleSelfSession(data.UID, int(data.ID)) {
			log.Printf("[TSBridge] stale self session leave clid=%d uid=%s (allowing for UI cleanup)", data.ID, data.UID)
		}
		c.RemoveRemoteAudioClient(int(data.ID))
		c.queueClientEvent(client, queuedEvent{
			evtType: "clientleave",
			data: struct {
				ID        int
				ReasonMsg string
			}{
				ID:        int(data.ID),
				ReasonMsg: data.ReasonMsg,
			},
		})
	})

	client.OnClientMoved(func(data ts.ClientMovedEvent) {
		c.queueClientEvent(client, queuedEvent{
			evtType: "clientmoved",
			data: struct {
				ID              int
				TargetChannelID string
			}{
				ID:              int(data.ID),
				TargetChannelID: fmt.Sprintf("%d", data.TargetChannelID),
			},
		})
	})

	client.OnKicked(func(reason string) {
		c.StopReceiveAudio()
		c.mu.Lock()
		if c.client != client {
			c.mu.Unlock()
			return
		}
		c.connected = false
		c.client = nil
		c.mu.Unlock()
		c.queueEvent(queuedEvent{evtType: "kicked", data: reason})
	})

	client.OnVoiceData(func(evt ts.VoiceDataEvent) {
		c.mu.Lock()
		r := c.receive
		c.mu.Unlock()
		if r != nil {
			r.enqueue(int(evt.ClientID), int(evt.Codec), evt.Sequence, evt.Data)
			return
		}
		// Keep the existing raw bridge active until an Android libopus decoder
		// factory has been supplied and the Go receiver explicitly started.
		if !c.isCurrentClient(client) {
			return
		}
		c.queueEvent(queuedEvent{
			evtType: "voicedata",
			data: struct {
				ClientID  int
				Data      []byte
				Codec     int
				Sequence  int
				IsWhisper bool
			}{
				ClientID:  int(evt.ClientID),
				Data:      evt.Data,
				Codec:     int(evt.Codec),
				Sequence:  int(evt.Sequence),
				IsWhisper: evt.IsWhisper,
			},
		})
	})

	client.OnPoked(func(evt ts.PokeEvent) {
		c.queueClientEvent(client, queuedEvent{
			evtType: "poked",
			data: &PokeEvent{
				InvokerID:   int(evt.InvokerID),
				InvokerName: evt.InvokerName,
				InvokerUID:  evt.InvokerUID,
				Message:     evt.Message,
			},
		})
	})
}
