package background

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	ctxbuild "git.yeij.top/AskaEth/Cyrene/ai-core/internal/context"
	"git.yeij.top/AskaEth/Cyrene/ai-core/internal/llm"
	"git.yeij.top/AskaEth/Cyrene/ai-core/internal/memory"
	"git.yeij.top/AskaEth/Cyrene/ai-core/internal/model"
	"git.yeij.top/AskaEth/Cyrene/ai-core/internal/persona"
	"git.yeij.top/AskaEth/Cyrene/ai-core/internal/tools"

	plgManager "git.yeij.top/AskaEth/Cyrene-Plugins/manager"
	plgSDK "git.yeij.top/AskaEth/Cyrene-Plugins/sdk"
)

// PendingThought 待推送的后台思考
type PendingThought struct {
	Content   string    `json:"content"`
	CreatedAt time.Time `json:"created_at"`
	Consumed  bool      `json:"consumed"`
}

// ProactiveTarget describes the target for proactive platform messages (OBv11, etc.).
// When nil, the message goes through the existing Web push path.
type ProactiveTarget struct {
	Platform string // "obv11"
	ChatType string // "private" or "group"
	UserID   string // OBv11 user number
	GroupID  string // OBv11 group number (group chat)
	AtUserID string // OBv11 number to @mention (optional)
}

// PlatformChannel represents a platform channel to observe for background thinking.
type PlatformChannel struct {
	Platform    string // qq, telegram, etc.
	ChannelType string // group, private
	ChannelID   string // group ID or user OBv11 number
	ChannelName string // group name or user display name (resolved at runtime)
	AdapterID   string // unique config ID of the adapter instance
	AdapterName string // config name, e.g. "qq-main"
}

// PlatformFormat describes how proactive message targets are formatted for a platform.
type PlatformFormat struct {
	// Human-readable platform label (e.g. "OBv11", "Telegram").
	Label string
	// Regex pattern with 3 capture groups: (chatType, channelID, atUserID).
	// chatType group must contain the platform-specific chat type keyword (e.g. "群聊", "私聊").
	TargetRegex string
	// Compiled regex (populated at registration time).
	compiled *regexp.Regexp
	// Format verbs for building markers in prompts.
	GroupMarkerFmt   string // e.g. "【OBv11群聊:%s】"
	PrivateMarkerFmt string // e.g. "【OBv11私聊:%s】"
	GroupAtMarkerFmt string // e.g. "【OBv11群聊:%s@%s】"
	// Chat type strings used by this platform in regex and markers.
	GroupKeyword   string // e.g. "群聊"
	PrivateKeyword string // e.g. "私聊"
}

// defaultPlatformFormats returns the built-in platform format registry.
func defaultPlatformFormats() map[string]*PlatformFormat {
	formats := make(map[string]*PlatformFormat)
	obv11 := &PlatformFormat{
		Label:            "OBv11",
		TargetRegex:      `【OBv11(私聊|群聊):(?:private_)?(\d+)(?:@(\d+))?】`,
		GroupMarkerFmt:   "【OBv11群聊:%s】",
		PrivateMarkerFmt: "【OBv11私聊:%s】",
		GroupAtMarkerFmt: "【OBv11群聊:%s@%s】",
		GroupKeyword:     "群聊",
		PrivateKeyword:   "私聊",
	}
	obv11.compiled = regexp.MustCompile(obv11.TargetRegex)
	formats["obv11"] = obv11
	return formats
}

// ParsePlatformChannels parses PLATFORM_CHANNELS env var.
// Format: "obv11:group:123456,telegram:group:789012"
// Optional 4th field for display name: "obv11:group:123456:群名称"
func ParsePlatformChannels(raw string) []PlatformChannel {
	if raw == "" {
		return nil
	}
	var channels []PlatformChannel
	for _, part := range strings.Split(raw, ",") {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}
		fields := strings.SplitN(part, ":", 4)
		if len(fields) < 3 {
			continue
		}
		ch := PlatformChannel{
			Platform:    strings.TrimSpace(fields[0]),
			ChannelType: strings.TrimSpace(fields[1]),
			ChannelID:   strings.TrimSpace(fields[2]),
		}
		if len(fields) >= 4 {
			ch.ChannelName = strings.TrimSpace(fields[3])
		}
		channels = append(channels, ch)
	}
	return channels
}

// Thinker 后台思考器（事件驱动 + 定时周期双模式）
//
// 触发机制：
//   1. 对话后思考：用户发消息 → 昔涟回复 → 短暂延迟后进行一次轻量反思
//   2. 静默检测：用户一段时间不说话 → 昔涟判断是否应该主动关心/搭话
//   3. 周期思考：每 N 分钟一次的定时思考，保证连续性
//
// 主动消息：思考中如有【主动消息】标记，会通过 messagePusher 回调推送给在线用户（带频率限制）。
type Thinker struct {
	mu          sync.Mutex
	recordMu    sync.Mutex
	muLockedBy  string    // debug: file:line of last Lock()
	muLockedAt  time.Time // debug: when Lock() was acquired // RecordUserMessage专用，隔离于mu死锁
	wg       sync.WaitGroup
	stopCh chan struct{}

	enabled       bool
	personaLoader *persona.Loader
	memRetriever  *memory.Retriever
	llmAdapter    *llm.Adapter
	toolAdapter   *llm.Adapter // 工具调用专用适配器
	iotClient     *tools.IoTClient

	// 记忆管理
	memoryStore    *memory.Store

	// 工具调用
	toolRegistry *plgManager.ToolRegistry

	// 会话上下文
	convStore      *ctxbuild.ConversationStore
	adminUserID    string
	adminSessionID string
	adminNickname  string
	activeSessionID string // 当前活跃的前端会话 ID（随用户消息更新）

	// 记忆服务 HTTP 客户端
	memClient *memory.Client

	// 主动消息推送回调 (nil = 不推送)
	// func(userID, sessionID, message string)
	messagePusher func(string, string, string)

	// 平台主动消息推送回调 (nil = 不推送)
	// func(target ProactiveTarget, message string)
	platformMessagePusher func(ProactiveTarget, string)

	// —— 事件驱动相关 ——

	// 周期性思考间隔：每隔固定时间自动触发一次思考
	// 默认 300 秒（5 分钟），设为 0 则禁用定时触发
	thinkInterval time.Duration

	// 轻量思考（简单思考）：高频、快速模型、简单 prompt
	lightThinkEnabled  bool
	lightThinkInterval time.Duration

	// 静默检测超时：用户多久不说话后昔涟可以主动搭话
	// 默认 120 秒（2 分钟），设为 0 则禁用静默检测
	silenceTimeout time.Duration

	// 对话后思考延迟：回复完成后等多久再触发思考（让对话有个自然停顿）
	// 默认 5 秒
	postChatDelay time.Duration

	// 两次思考最小间隔：避免频繁触发（如用户连续发多条消息）
	// 默认 30 秒
	minThinkGap time.Duration

	// 离线时最小思考间隔：用户不在线时的周期思考间隔
	// 默认 10 分钟
	offlineThinkGap time.Duration

	// 主动消息最小间隔：避免频繁推送打扰用户
	// 默认 30 分钟，设为 0 则每次思考都可推送
	proactiveMsgMinGap time.Duration

	// 静默检测的一次性定时器（每次用户消息后重置）
	silenceTimer    *time.Timer
	silenceTimerMu  sync.Mutex

	// —— 状态追踪 ——

	pendingThoughts []*PendingThought
	lastUserMessage time.Time
	lastThinkTime   time.Time
	lastProactiveMsgTime time.Time // deprecated, use lastProactiveTime()
	thinkCancel     context.CancelFunc // 取消当前思考，让步于前台

	// 思考计数器（用于周期性记忆维护，每 N 次思考触发一次）
	thinkCountAtomic atomic.Int64

	// Phase 1 Step 4: 思考链 + 自主工具安全策略
	chain          *ThinkChain
	autoToolPolicy *AutonomousToolPolicy

	// Phase 2: 情感追踪
	emotionTracker *persona.EmotionTracker

	// Phase 2: 主动消息决策守卫
	proactiveGuard *ProactiveGuard

	// 动态调度: 按时间段自动调整思考间隔
	scheduleLoader *ScheduleLoader

	// Phase 2: 在线状态追踪
	userOnlineAtomic atomic.Bool
	lastUserMsgNs    atomic.Int64 // UnixNano (replaces lastUserMessage)
	lastThinkNs      atomic.Int64 // UnixNano (replaces lastThinkTime)
	lastProactiveNs  atomic.Int64 // UnixNano (replaces lastProactiveMsgTime)
	lastOnlineChange time.Time
	userSessionID    string // 当前活跃的 session ID (用于重连)

		// 时区设置 (默认 Asia/Shanghai，可通过 TZ 环境变量覆盖)
		timeLocation *time.Location

	// 平台静默观察
	platformChannels      []PlatformChannel
	platformThinkInterval time.Duration

	// 平台 Bot UID (platform -> bot's own UID, e.g. "obv11" -> "123456789")
	botUIDs map[string]string

	// 平台格式注册表 (platform -> format)
	platformFormats map[string]*PlatformFormat
}

// AutonomousToolPolicy 自主思考工具调用安全策略
type AutonomousToolPolicy struct {
	// 允许在自主思考中使用的工具白名单
	AllowedTools []string // iot_query, memory_search, web_search, calculator, datetime
	// 每轮最大工具调用次数
	MaxToolCallsPerRound int // 默认 5
	// 高风险操作每小时最大次数 (如 iot_control)
	MaxHighRiskPerHour int // 默认 10
	// 高风险工具列表
	HighRiskTools []string // iot_control
}

// DefaultAutonomousToolPolicy 默认安全策略
func DefaultAutonomousToolPolicy() *AutonomousToolPolicy {
	return &AutonomousToolPolicy{
		AllowedTools: []string{
			"iot_query", "iot_control", "memory_search", "web_search",
			"calculator", "datetime", "web_fetch",
			"host_exec", "host_file", "host_system",
			"vision_analyze", "knowledge_search", "knowledge_ingest",
			"reminder_create", "reminder_list", "reminder_delete",
		},
		MaxToolCallsPerRound: 5,
		MaxHighRiskPerHour:   10,
		HighRiskTools:        []string{"iot_control", "host_exec"},
	}
}

// timePeriod returns a human-readable period name and guidance for the current hour.
func timePeriod(now time.Time) (string, string) {
	hour := now.Hour()
	switch {
	case hour >= 6 && hour < 9:
		return "早晨", "精力充沛，可以主动问候。语气活泼。"
	case hour >= 9 && hour < 18:
		return "白天", "正常工作时段。日常闲聊模式，主动消息低频。"
	case hour >= 18 && hour < 22:
		return "傍晚", "放松时间。温柔关心今天过得怎样，可以主动搭话。"
	case hour >= 22 || hour < 2:
		return "深夜", "安静模式。话少、温柔、催睡觉。尽量不要发主动消息。"
	default:
		return "凌晨", "休眠模式。开拓者应该在睡觉，绝对不要打扰。"
	}
}

// SetMessagePusher 设置主动消息推送回调
// SetScheduleLoader sets the dynamic schedule loader for interval calculation.
func (t *Thinker) SetScheduleLoader(loader *ScheduleLoader) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	t.scheduleLoader = loader
}

func (t *Thinker) SetMessagePusher(pusher func(string, string, string)) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	t.messagePusher = pusher
}

// SetPlatformMessagePusher sets the callback for pushing proactive messages to platform adapters (OBv11, etc.).
func (t *Thinker) SetPlatformMessagePusher(pusher func(ProactiveTarget, string)) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	t.platformMessagePusher = pusher
}

// SetBotUID sets the bot's own platform UID (e.g., OBv11 account).
func (t *Thinker) SetBotUID(platform, uid string) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	if t.botUIDs == nil {
		t.botUIDs = make(map[string]string)
	}
	if uid != "" {
		t.botUIDs[platform] = uid
	}
}

// AddOrUpdatePlatformChannel adds or updates a platform channel with resolved display name.
func (t *Thinker) AddOrUpdatePlatformChannel(platform, channelType, channelID, channelName, adapterID, adapterName string) {
	if !t.muLock() {
		return
	defer t.muUnlock()

	for i, ch := range t.platformChannels {
		if ch.Platform == platform && ch.ChannelType == channelType && ch.ChannelID == channelID {
			if channelName != "" {
				t.platformChannels[i].ChannelName = channelName
			}
			if adapterID != "" {
				t.platformChannels[i].AdapterID = adapterID
			}
			if adapterName != "" {
				t.platformChannels[i].AdapterName = adapterName
			}
			return
		}
	}
	// Not found — append.
	t.platformChannels = append(t.platformChannels, PlatformChannel{
		Platform:    platform,
		ChannelType: channelType,
		ChannelID:   channelID,
		ChannelName: channelName,
		AdapterID:   adapterID,
		AdapterName: adapterName,
	})
}

// SetEmotionTracker sets the emotion tracker.
func (t *Thinker) SetEmotionTracker(et *persona.EmotionTracker) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	t.emotionTracker = et
}

// UpdatePresence updates the user online status.
// Called by the ai-core presence endpoint when gateway detects connect/disconnect.
func (t *Thinker) UpdatePresence(online bool, sessionID string) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	wasOffline := !t.isUserOnline()
	t.setUserOnline(online)
	t.lastOnlineChange = time.Now()
	if sessionID != "" {
		t.userSessionID = sessionID
		t.activeSessionID = sessionID
	}


	if online && wasOffline {
		log.Printf("[后台思考] 用户上线 (session=%s)，触发重连思考", sessionID)
		// Trigger a return-thinking cycle after a short delay
		time.Sleep(2 * time.Second)
		t.performThink("user_returned")
		// Also update emotion tracker
		if t.emotionTracker != nil {
			t.emotionTracker.UpdateMood("user_returned")
		}
	} else if !online {
		log.Printf("[后台思考] 用户离线")
	}
}

// TriggerReminderMessage pushes a reminder message generated by LLM to the user.
// Called by the internal reminder-trigger endpoint when a reminder fires.
func (t *Thinker) TriggerReminderMessage(userID, sessionID, message string) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	pusher := t.messagePusher
	pushSessionID := sessionID
	if pushSessionID == "" {
		pushSessionID = t.activeSessionID
	}
	if pushSessionID == "" {
		pushSessionID = t.adminSessionID
	}


	if pusher != nil && message != "" {
		log.Printf("[提醒推送] 推送LLM提醒: user=%s session=%s msg=%s", userID, pushSessionID, message)
		pusher(userID, pushSessionID, message)
	}
}

// PushPlatformMessage pushes a message to a platform channel via the platform pusher.
func (t *Thinker) PushPlatformMessage(target ProactiveTarget, message string) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	pusher := t.platformMessagePusher

	if pusher != nil {
		log.Printf("[平台推送] target=%s/%s group=%s msg=%s", target.Platform, target.ChatType, target.GroupID, message)
		pusher(target, message)
	}
}



// IsUserRecentlyActive returns true if the user has been active within the given duration.
func (t *Thinker) IsUserRecentlyActive(d time.Duration) bool {
	if !t.muLock() {
		return
	defer t.muUnlock()
	return time.Since(t.lastUserTime()) < d
}




// ThinkerConfig 后台思考配置
type ThinkerConfig struct {
	Enabled          bool
	ThinkInterval    time.Duration // 周期性思考间隔 (默认 5 分钟，0 = 禁用)
	SilenceTimeout   time.Duration // 用户沉默多久后昔涟可以主动搭话 (0 = 禁用)
	PostChatDelay    time.Duration // 对话后多久触发思考
	MinThinkGap      time.Duration // 两次思考最小间隔 (在线)
	OfflineThinkGap  time.Duration // 两次思考最小间隔 (离线，默认 10 分钟)

	// 平台静默观察
	LightThinkEnabled  bool
	LightThinkInterval time.Duration // 轻量思考间隔 (默认 60s，0 = 禁用)

	PlatformSilentThinkInterval time.Duration // 平台记忆观察间隔 (默认 600s，0 = 禁用)
	PlatformChannels            []PlatformChannel
}

// DefaultThinkerConfig 默认配置
//
// 事件驱动 + 定时周期双模式：
//   - 对话后和静默时触发事件驱动思考
//   - 每 5 分钟一次的周期性思考保证连续性
//
// 环境变量：
//   THINK_INTERVAL_SEC — 周期时长 (默认 300)
//   PROACTIVE_MSG_MIN_GAP_SEC — 主动消息最小间隔 (默认 1800 = 30分钟，0 = 禁用)
func DefaultThinkerConfig() ThinkerConfig {
	return ThinkerConfig{
		Enabled:         getEnvBool("ENABLE_BACKGROUND_THINKING", true),
		ThinkInterval:   getEnvDuration("THINK_INTERVAL_SEC", 300),
		SilenceTimeout:  getEnvDuration("THINK_SILENCE_TIMEOUT_SEC", 120),
		PostChatDelay:   getEnvDuration("THINK_POST_CHAT_DELAY_SEC", 5),
		MinThinkGap:     getEnvDuration("THINK_MIN_GAP_SEC", 30),
		OfflineThinkGap: getEnvDuration("THINK_OFFLINE_GAP_SEC", 600),
		LightThinkEnabled:  getEnvBool("LIGHT_THINK_ENABLED", true),
		LightThinkInterval: getEnvDuration("LIGHT_THINK_INTERVAL_SEC", 60),
		PlatformSilentThinkInterval: getEnvDuration("PLATFORM_THINK_INTERVAL_SEC", 600),
		PlatformChannels:            ParsePlatformChannels(os.Getenv("PLATFORM_CHANNELS")),
	}
}

// NewThinker 创建事件驱动的后台思考器
func NewThinker(
	cfg ThinkerConfig,
	personaLoader *persona.Loader,
	memRetriever *memory.Retriever,
	llmAdapter *llm.Adapter,
	toolAdapter *llm.Adapter,
	iotClient *tools.IoTClient,
	memoryStore *memory.Store,
	toolRegistry *plgManager.ToolRegistry,
	convStore *ctxbuild.ConversationStore,
	adminUserID string,
	adminSessionID string,
	adminNickname string,
	memClient *memory.Client,
) *Thinker {
	// 加载时区配置
	tzName := os.Getenv("TZ")
	if tzName == "" {
		tzName = "Asia/Shanghai"
	}
	loc, err := time.LoadLocation(tzName)
	if err != nil {
		log.Printf("[后台思考] 无效时区 '%s'，回退到 Asia/Shanghai: %v", tzName, err)
		loc, _ = time.LoadLocation("Asia/Shanghai")
	}

	return &Thinker{
		enabled:             cfg.Enabled,
		personaLoader:       personaLoader,
		memRetriever:        memRetriever,
		llmAdapter:          llmAdapter,
		toolAdapter:         toolAdapter,
		iotClient:           iotClient,
		thinkInterval:       cfg.ThinkInterval,
		lightThinkEnabled:   cfg.LightThinkEnabled,
		lightThinkInterval:  cfg.LightThinkInterval,
		silenceTimeout:      cfg.SilenceTimeout,
		proactiveMsgMinGap:  getEnvDuration("PROACTIVE_MSG_MIN_GAP_SEC", 60),
		postChatDelay:       cfg.PostChatDelay,
		minThinkGap:         cfg.MinThinkGap,
		offlineThinkGap:     cfg.OfflineThinkGap,
		memoryStore:         memoryStore,
		timeLocation:        loc,
		
		toolRegistry:     toolRegistry,
		convStore:        convStore,
		adminUserID:      adminUserID,
		adminSessionID:   adminSessionID,
		adminNickname:    adminNickname,
		memClient:        memClient,
		pendingThoughts:     make([]*PendingThought, 0),
		lastUserMessage:     time.Now().Add(-5 * time.Minute), // overwritten by restoreContext
		stopCh:              make(chan struct{}),
		chain:               NewThinkChain(10),
		autoToolPolicy:      DefaultAutonomousToolPolicy(),
		proactiveGuard:      DefaultProactiveGuard(),
		platformChannels:    cfg.PlatformChannels,
		platformThinkInterval: cfg.PlatformSilentThinkInterval,
		platformFormats:      defaultPlatformFormats(),
	}
}

// restoreContext recovers the last user activity time from conversation history (DB).
func (t *Thinker) restoreContext() {
	if t.convStore == nil {
		return
	}
	// 从已恢复的会话历史中获取最后一条消息的时间（优先用户消息）
	history := t.convStore.GetHistory(t.adminSessionID, 20)
	var latest time.Time
	for i := len(history) - 1; i >= 0; i-- {
		if !history[i].Timestamp.IsZero() {
			if history[i].Role == model.RoleUser {
				latest = history[i].Timestamp
				break
			}
			// 回退：非 system 消息的时间也可以
			if latest.IsZero() && history[i].Role != model.RoleSystem {
				latest = history[i].Timestamp
			}
		}
	}
	if !latest.IsZero() && time.Since(latest) < 24*time.Hour {
		if !t.muLock() {
			return
		defer t.muUnlock()
		t.lastUserMessage = latest
		t.setLastUser(latest)

		log.Printf("[后台思考] 上下文已恢复: 最近活动 %v 前", time.Since(latest).Round(time.Second))
	} else if !latest.IsZero() {
		log.Printf("[后台思考] 上下文恢复跳过: 最近消息 %v 前(>24h)，使用默认值", time.Since(latest).Round(time.Second))
	}
}

// Start 初始化后台思考器
//
// 双模式触发：
//   1. 事件驱动：对话后 + 静默超时（即时响应）
//   2. 定时周期：每 N 分钟一次自主思考（保证连续性）
func (t *Thinker) Start() {
	if !t.enabled {
		log.Println("[后台思考] 已禁用 (ENABLE_BACKGROUND_THINKING=false)")
		return
	}

	// 恢复上下文：从持久化存储查询最近活动时间，避免重启后"失忆"
	t.restoreContext()
	// 同步原子字段（restoreContext 写入 lastUserMessage 后需要同步）
	t.setLastUser(t.lastUserMessage)
	t.setLastThink(t.lastThinkTime)
	t.setLastProactive(t.lastProactiveMsgTime)

	// 初始化静默检测定时器（但不启动，等第一次用户消息后启动）
	if t.silenceTimeout > 0 {
		t.silenceTimer = time.NewTimer(t.silenceTimeout)
		t.silenceTimer.Stop() // 先停止，等 RecordUserMessage 时启动
	}

	// 启动周期性思考定时器
	if t.thinkInterval > 0 {
		t.wg.Add(1)
		go t.periodicThinkLoop()
	}
	if t.lightThinkEnabled && t.lightThinkInterval > 0 {
		t.wg.Add(1)
		go t.lightThinkLoop()
	// 死锁监控：每30秒检测 t.mu 是否正常
	t.StartDeadlockMonitor()
	}

	// 启动平台静默观察循环
	if len(t.platformChannels) > 0 && t.platformThinkInterval > 0 {
		t.wg.Add(1)
		go t.platformObservationLoop()
	}

	log.Printf("[后台思考] 已就绪 — 周期=%v + 事件驱动模式 (静默超时=%v, 对话后延迟=%v, 在线最小间隔=%v, 离线最小间隔=%v, 管理员=%s, 平台频道=%d)",
		t.thinkInterval, t.silenceTimeout, t.postChatDelay, t.minThinkGap, t.offlineThinkGap, t.adminUserID, len(t.platformChannels))

	// 启动后首次思考：延迟 5s，让服务完全初始化后再触发
	go func() {
		time.Sleep(5 * time.Second)
		log.Println("[后台思考] 首次启动思考 (startup)")
		t.performThink("startup")
	}()
}

// Stop 停止后台思考器
func (t *Thinker) Stop() {
	close(t.stopCh)

	t.silenceTimerMu.Lock()
	if t.silenceTimer != nil {
		t.silenceTimer.Stop()
	}
	t.silenceTimerMu.Unlock()

	t.wg.Wait()
	log.Println("[后台思考] 已停止")
}

// RecordUserMessage 记录用户活动时间、活跃会话，并重置静默检测定时器
//
// 每次用户发消息时调用。这会：
//   1. 更新 lastUserMessage 时间戳
//   2. 记录当前活跃的前端会话 ID（用于对话上下文检索和主动消息推送）
//   3. 重置静默检测的一次性定时器（如果启用）
func (t *Thinker) RecordUserMessage(sessionID string) {
	now := time.Now()
	t.recordMu.Lock()
	t.lastUserMessage = now
	t.setLastUser(now)
	if sessionID != "" {
		t.activeSessionID = sessionID
	}
	t.lastProactiveMsgTime = time.Time{}
	t.setLastProactive(time.Time{})
	t.recordMu.Unlock()
	if t.thinkCancel != nil {
		t.thinkCancel()
		t.thinkCancel = nil
	}

	// 重置静默检测定时器
	t.resetSilenceTimer()
}

// TriggerPostChatThink 对话完成后触发一次自主思考
//
// 在昔涟回复完用户后调用。短暂延迟后执行一次思考，
// 让昔涟"回味"刚才的对话，并判断是否想主动多说点什么。
//
// 该方法是异步的，立即返回。
func (t *Thinker) TriggerPostChatThink() {
	if !t.enabled {
		return
	}

	if !t.muLock() {
		return
	defer t.muUnlock()
	canThink := time.Since(t.lastThinkTimeAtomic()) >= t.minThinkGap


	if !canThink {
		log.Printf("[后台思考] 距上次思考仅 %v，跳过 (最小间隔=%v)", time.Since(t.lastThinkTimeAtomic()), t.minThinkGap)
		return
	}

	t.wg.Add(1)
	go func() {
		defer t.wg.Done()
		defer func() {
			if r := recover(); r != nil {
				log.Printf("[后台思考] 对话后触发 panic 恢复: %v", r)
			}
		}()

		// 短暂延迟，让对话有个自然的停顿
		select {
		case <-t.stopCh:
			return
		case <-time.After(t.postChatDelay):
		}

		log.Println("[后台思考] 对话后触发自主思考...")
		t.performThink("post_chat")
	}()
}

// resetSilenceTimer 重置静默检测的一次性定时器
//
// 每次用户发消息时调用。旧的定时器被取消，新的定时器开始计时。
// 当定时器触发时，昔涟会判断是否应该主动搭话。
func (t *Thinker) resetSilenceTimer() {
	t.silenceTimerMu.Lock()
	defer t.silenceTimerMu.Unlock()

	if t.silenceTimer == nil || t.silenceTimeout <= 0 {
		return
	}

	// 停止旧定时器
	if !t.silenceTimer.Stop() {
		// 如果已经触发，清空通道
		select {
		case <-t.silenceTimer.C:
		default:
		}
	}

	// 重新设置
	t.silenceTimer.Reset(t.silenceTimeout)

	// 启动监听协程（仅当定时器触发时才执行）
	t.wg.Add(1)
	go func() {
		defer t.wg.Done()
		defer func() {
			if r := recover(); r != nil {
				log.Printf("[后台思考] 静默定时器 panic 恢复: %v", r)
			}
		}()

		select {
		case <-t.stopCh:
			return
		case <-t.silenceTimer.C:
			// 再次检查：用户是否真的沉默了足够久
			if !t.muLock() {
				return
			defer t.muUnlock()
			silenceDuration := time.Since(t.lastUserTime())
			canThink := time.Since(t.lastThinkTimeAtomic()) >= t.minThinkGap


			if silenceDuration < t.silenceTimeout {
				log.Printf("[后台思考] 静默检测触发但用户已活动，跳过 (实际静默=%v)", silenceDuration)
				return
			}

			if !canThink {
				log.Printf("[后台思考] 静默检测触发但距上次思考太近，跳过")
				return
			}

			log.Printf("[后台思考] 用户已静默 %v，触发主动关怀思考...", silenceDuration.Round(time.Second))
			t.performThink("silence")
		}
	}()
}

// platformObservationLoop periodically queries platform channel memories and generates observations.
func (t *Thinker) platformObservationLoop() {
	defer t.wg.Done()
	defer func() {
		if r := recover(); r != nil {
			log.Printf("[后台思考] 平台观察循环 panic 恢复: %v", r)
		}
	}()

	interval := t.platformThinkInterval
	log.Printf("[后台思考] 平台观察循环已启动 (间隔=%v, 频道数=%d)", interval, len(t.platformChannels))

	for {
		select {
		case <-t.stopCh:
			log.Println("[后台思考] 平台观察循环已停止")
			return
		case <-time.After(interval):
			t.performPlatformObservation()
		}
	}
}

// performPlatformObservation queries memories from all platform channels,
// runs an intermediate LLM session to summarize, and stores the result as a pending thought.
func (t *Thinker) performPlatformObservation() {
	if t.memClient == nil {
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	var channelSummaries []string
	for _, ch := range t.platformChannels {
		namespace := fmt.Sprintf("platform_%s_%s_%s", ch.Platform, ch.ChannelType, ch.ChannelID)
		memories, err := t.memClient.Query(ctx, model.MemoryQuery{
			UserID: namespace,
			Limit:  20,
		})
		if err != nil {
			log.Printf("[后台思考] 查询平台频道 %s 记忆失败: %v", namespace, err)
			continue
		}
		if len(memories) == 0 {
			continue
		}

		var sb strings.Builder
		sb.WriteString(fmt.Sprintf("【%s %s %s】\n", ch.Platform, ch.ChannelType, ch.ChannelID))
		for i, m := range memories {
			if i >= 10 {
				sb.WriteString(fmt.Sprintf("... 还有 %d 条记忆\n", len(memories)-10))
				break
			}
			sb.WriteString(fmt.Sprintf("- %s\n", m.Content))
		}
		channelSummaries = append(channelSummaries, sb.String())
	}

	if len(channelSummaries) == 0 {
		return
	}

	log.Printf("[后台思考] 平台观察：%d 个频道有记忆数据，调用中间会话生成摘要...", len(channelSummaries))

	systemPrompt := "你是昔涟的后台观察助手。以下是各聊天平台频道最近的观察摘要。\n请生成简洁报告：\n1. 各频道近期讨论主题（每频道1-2句）\n2. 是否有需要关注的重要/紧急事项\n3. 整体氛围评估\n注意：这些记忆可能来自不同的群聊成员（不只是开拓者），请以实际发言者为主语描述。不要直接对开拓者说话，这是给昔涟参考的幕后报告。\n输出为JSON格式：{\"summary\": \"报告内容\", \"needs_attention\": true/false}"

	userPrompt := strings.Join(channelSummaries, "\n\n")

	messages := []model.LLMMessage{
		{Role: model.RoleSystem, Content: systemPrompt},
		{Role: model.RoleUser, Content: userPrompt},
	}

	resp, err := t.toolAdapter.Chat(ctx, messages)
	if err != nil {
		log.Printf("[后台思考] 中间会话 LLM 调用失败: %v", err)
		return
	}

	var result struct {
		Summary        string `json:"summary"`
		NeedsAttention bool   `json:"needs_attention"`
	}
	content := strings.TrimSpace(resp.Content)
	if idx := strings.Index(content, "{"); idx >= 0 {
		if end := strings.LastIndex(content, "}"); end > idx {
			content = content[idx : end+1]
		}
	}
	if err := json.Unmarshal([]byte(content), &result); err != nil {
		result.Summary = resp.Content
	}

	observationContent := fmt.Sprintf("[平台观察 %s]\n%s", time.Now().In(t.timeLocation).Format("15:04"), result.Summary)
	if !t.muLock() {
		return
	defer t.muUnlock()
	t.pendingThoughts = append(t.pendingThoughts, &PendingThought{
		Content:   observationContent,
		CreatedAt: time.Now(),
		Consumed:  false,
	})
	if len(t.pendingThoughts) > 10 {
		t.pendingThoughts = t.pendingThoughts[len(t.pendingThoughts)-10:]
	}


	log.Printf("[后台思考] 平台观察摘要已生成 (长度=%d, 需要关注=%v)", len(result.Summary), result.NeedsAttention)
}

// lightThinkLoop runs frequent lightweight thinking cycles using a fast model.
// It checks for urgent matters and can wake the deep thinker when needed.
func (t *Thinker) lightThinkLoop() {
	defer t.wg.Done()
	defer func() {
		if r := recover(); r != nil {
			log.Printf("[后台思考] 轻量思考循环 panic 恢复: %v", r)
		}
	}()

	log.Printf("[后台思考] 轻量思考已启动 (间隔=%v)", t.lightThinkInterval)

	for {
		select {
		case <-t.stopCh:
			log.Println("[后台思考] 轻量思考已停止")
			return
		case <-time.After(t.lightThinkInterval):
			if !t.muLock() {
				return
			defer t.muUnlock()
			sinceLastUser := time.Since(t.lastUserTime())
			sinceLastThink := time.Since(t.lastThinkTimeAtomic())


			// Skip if user was active recently (last 30s).
			if sinceLastUser < 30*time.Second {
				continue
			}
			// Skip if deep think just ran.
			if sinceLastThink < 10*time.Second {
				continue
			}

			t.performLightThink()
		}
	}
}

// performLightThink runs a single lightweight thinking cycle using the fast model.
func (t *Thinker) performLightThink() {
	log.Printf("[debug] lightThink ENTER")
	defer func() { log.Printf("[debug] lightThink EXIT") }()
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	// Build a minimal system prompt.
	now := time.Now()
	systemPrompt := fmt.Sprintf(`你是昔涟。这是一次快速的内心检查（%s）。

请用一句话在心里判断：
1. 开拓者有什么需要立即关注的事情吗？（提醒、设备状态、情绪变化）
2. 如果想起之前聊过的某件事可以追问（如"上周说的面试怎么样了"），输出【话题发起】+ 你要说的话
3. 如果有紧急事项，输出【需要深思】标记
4. 如果一切正常，输出一个字的感受即可。

注意：只需要极简判断，不要长篇反思。`, now.Format("15:04"))

	userPrompt := "快速检查一下。"
	messages := []model.LLMMessage{
		{Role: model.RoleSystem, Content: systemPrompt},
		{Role: model.RoleUser, Content: userPrompt},
	}

	// Use fast model (toolAdapter) for light think.
	resp, err := t.toolAdapter.Chat(ctx, messages)
	if err != nil {
		log.Printf("[轻量思考] LLM调用失败: %v", err)
		return
	}

	content := resp.Content
	if content == "" {
		return
	}

	log.Printf("[轻量思考] %s", strings.TrimSpace(content))

	// Check if deep think should be woken.
	if strings.Contains(content, "【需要深思】") {
		log.Println("[轻量思考] 检测到【需要深思】，唤醒深度思考...")
		if !t.muLock() {
			return
		defer t.muUnlock()
		canDeep := time.Since(t.lastThinkTimeAtomic()) >= t.minThinkGap

		if canDeep {
			t.performThink("light_wake")
		} else {
			log.Println("[轻量思考] 距上次深度思考太近，跳过唤醒")
		}
	}

	// Check if a topic should be initiated (proactive conversation starter).
	if idx := strings.Index(content, "【话题发起】"); idx >= 0 {
		topic := strings.TrimSpace(content[idx+len("【话题发起】"):])
		if topic != "" {
			log.Printf("[轻量思考] 话题发起: %s", topic)
			if !t.muLock() {
				return
			defer t.muUnlock()
			pusher := t.messagePusher
			sessionID := t.activeSessionID
			if sessionID == "" {
				sessionID = t.adminSessionID
			}
			canPush := time.Since(t.lastProactiveTime()) >= t.proactiveMsgMinGap

			if pusher != nil && canPush {
				go pusher(t.adminUserID, sessionID, topic)
				if t.convStore != nil && sessionID != "" {
					t.convStore.AddMessage(sessionID, model.LLMMessage{Role: model.RoleAssistant, Content: topic})
				}
			}
		}
	}
}

// periodicThinkLoop 周期性自主思考循环
// periodicThinkLoop 周期性自主思考循环
//
// 使用动态间隔：若配置了 ScheduleLoader，每次循环根据当前时段计算间隔；
// 否则回退到固定的 thinkInterval。
func (t *Thinker) periodicThinkLoop() {
	defer t.wg.Done()
	defer func() {
		if r := recover(); r != nil {
			log.Printf("[后台思考] 周期性循环 panic 恢复: %v", r)
		}
	}()

	log.Printf("[后台思考] 周期性思考已启动 (间隔=%v)", t.thinkInterval)

	for {
		// 计算本次等待间隔
		interval := t.thinkInterval
		if t.scheduleLoader != nil {
			if mins := t.scheduleLoader.GetInterval(time.Now()); mins > 0 {
				interval = time.Duration(mins) * time.Minute
			}
		}

		select {
		case <-t.stopCh:
			log.Println("[后台思考] 周期性思考已停止")
			return
		case <-time.After(interval):
			// (atomic fields, no lock needed — all reads are lock-free)
			sinceLastThink := time.Since(t.lastThinkTimeAtomic())
			sinceLastUser := time.Since(t.lastUserTime())
			isOffline := !t.isUserOnline()
			offlineMinGap := t.offlineThinkGap

			// 跳过条件：用户最近在活动（30s 内有消息），说明正在对话中
			if sinceLastUser < 30*time.Second {
				log.Printf("[后台思考] 用户在 %v 前发过消息，跳过周期性触发 (留给事件驱动处理)", sinceLastUser.Round(time.Second))
				continue
			}

			if isOffline && sinceLastThink < offlineMinGap {
				log.Printf("[后台思考] 用户离线，距上次思考仅 %v，跳过 (离线模式最小间隔=%v)", sinceLastThink.Round(time.Second), offlineMinGap)
				continue
			}

			if !isOffline && sinceLastThink < t.minThinkGap {
				log.Printf("[后台思考] 距上次思考仅 %v，跳过周期性触发", sinceLastThink.Round(time.Second))
				continue
			}

			log.Printf("[debug] periodicThink TRIGGER interval=%v lastThink=%v lastUser=%v", interval, sinceLastThink.Round(time.Second), sinceLastUser.Round(time.Second))
			t.performThink("periodic")
		}
	}
}

// GetPendingThoughts 获取并消费所有待处理的后台思考
func (t *Thinker) GetPendingThoughts() []*PendingThought {
	if !t.muLock() {
		return
	defer t.muUnlock()

	if len(t.pendingThoughts) == 0 {
		return nil
	}

	result := t.pendingThoughts
	t.pendingThoughts = make([]*PendingThought, 0)

	for _, pt := range result {
		pt.Consumed = true
	}
	return result
}

// HasPendingThoughts 检查是否有待处理的思考
func (t *Thinker) HasPendingThoughts() bool {
	if !t.muLock() {
		return
	defer t.muUnlock()
	return len(t.pendingThoughts) > 0
}

// performThink 执行一次增强版后台思考（支持工具调用和记忆管理）
//
// triggerReason: "post_chat" (对话后) 或 "silence" (静默超时)
//
// 防御性速率限制：即使调用方未检查 minThinkGap，performThink 自身也会
// 强制执行最小间隔，防止并发调用或 bug 导致 LLM 配额被快速消耗。
func (t *Thinker) performThink(triggerReason string) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	gapSinceLast := time.Since(t.lastThinkTimeAtomic())
	minGap := t.minThinkGap
	if minGap <= 0 {
		minGap = 5 * time.Second // 默认最小间隔 5 秒
	}
	if gapSinceLast < minGap {

		log.Printf("[后台思考] 距上次思考仅 %v，跳过 (最小间隔=%v, 触发原因=%s)", gapSinceLast.Round(time.Second), minGap, triggerReason)
		return
	}

	t.lastThinkTime = time.Now()
	t.setLastThink(t.lastThinkTime)
	t.incThinkCount()
	currentCount := t.thinkCount()
	t.muUnlock()

	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
	defer cancel()
	if !t.muLock() {
		return
	defer t.muUnlock()
	t.thinkCancel = cancel


	// 0. 让步于前台——如果用户最近有活动（非post_chat），跳过本次思考。
	if triggerReason != "post_chat" {
		if !t.muLock() {
			return
		defer t.muUnlock()
		sinceLastUser := time.Since(t.lastUserTime())

		if sinceLastUser < 10*time.Second {
			log.Printf("[后台思考] 用户 %v 前有活动，让步于前台回复，跳过思考", sinceLastUser.Round(time.Second))
			return
		}
	}

	log.Printf("[后台思考] 开始思考周期 (触发原因=%s, 计数=%d)...", triggerReason, currentCount)

	// 0. 让步于前台——如果用户最近有活动（非post_chat），跳过本次思考。
	if triggerReason != "post_chat" {
		if !t.muLock() {
			return
		defer t.muUnlock()
		sinceLastUser := time.Since(t.lastUserTime())

		if sinceLastUser < 10*time.Second {
			log.Printf("[后台思考] 用户 %v 前有活动，让步于前台回复，跳过思考", sinceLastUser.Round(time.Second))
			return
		}
	}

	// 1. 加载人格配置
	personaConfig, err := t.personaLoader.Get("cyrene")
	if err != nil {
		log.Printf("[后台思考] 加载人格失败: %v", err)
		return
	}

	// 2. 获取当前活跃会话的对话历史（优先活跃会话，回退到管理员主会话）
	var convHistory []model.LLMMessage
	if t.convStore != nil {
		if !t.muLock() {
			return
		defer t.muUnlock()
		sessionID := t.activeSessionID
		if sessionID == "" {
			sessionID = t.adminSessionID
		}

		if sessionID != "" {
			convHistory = t.convStore.GetHistory(sessionID, 30)
			if len(convHistory) > 0 {
				log.Printf("[后台思考] 加载对话历史 %d 条 (session=%s)", len(convHistory), sessionID)
			}
		}
	}

	// 3. 检索相关记忆（精确检索 + 模糊搜索）
	var memories []memory.MemoryEntry
	if t.memRetriever != nil {
		memories, err = t.memRetriever.Retrieve(ctx, t.adminUserID, "最近发生了什么 重要的事情 用户偏好 个人信息")
		if err != nil {
			log.Printf("[后台思考] 记忆检索失败: %v", err)
		}

		// 模糊搜索：从对话历史提取话题，LLM 扩展关键词后语义搜索
		if t.memClient != nil && len(convHistory) > 0 {
			fuzzyQuery := lastUserMessage(convHistory)
			if fuzzyQuery == "" {
				fuzzyQuery = "最近对话 重要事件 用户状态"
			}
			fuzzyResults := t.fuzzyMemorySearch(ctx, t.adminUserID, fuzzyQuery)
			seen := make(map[string]bool)
			for _, m := range memories {
				seen[m.ID] = true
			}
			for _, m := range fuzzyResults {
				if !seen[m.ID] {
					seen[m.ID] = true
					memories = append(memories, m)
				}
			}
			if len(fuzzyResults) > 0 {
				log.Printf("[后台思考] 模糊搜索补充 %d 条记忆", len(fuzzyResults))
			}
		}

		// Also pull recent channel/group memories so the model is aware of group activity.
		if t.memClient != nil && len(t.platformChannels) > 0 {
			chanSeen := make(map[string]bool)
			for _, m := range memories {
				chanSeen[m.ID] = true
			}
			oldCount := len(memories)
			for _, ch := range t.platformChannels {
				namespace := fmt.Sprintf("platform_%s_%s_%s", ch.Platform, ch.ChannelType, ch.ChannelID)
				chanMems, cErr := t.memClient.Query(ctx, model.MemoryQuery{
					UserID: namespace,
					Limit:  5,
				})
				if cErr != nil {
					continue
				}
				for _, m := range chanMems {
					if !chanSeen[m.ID] {
						chanSeen[m.ID] = true
						labeled := m
						labeled.Content = fmt.Sprintf("[群聊%s] %s", ch.ChannelID, m.Content)
						memories = append(memories, labeled)
					}
				}
			}
			if len(memories) > oldCount {
				log.Printf("[后台思考] 频道记忆补充 %d 条", len(memories)-oldCount)
			}
		}

		// Also pull recent channel/group memories so the model is aware of group activity.
		if t.memClient != nil && len(t.platformChannels) > 0 {
			chanSeen := make(map[string]bool)
			for _, m := range memories {
				chanSeen[m.ID] = true
			}
			oldCount := len(memories)
			for _, ch := range t.platformChannels {
				namespace := fmt.Sprintf("platform_%s_%s_%s", ch.Platform, ch.ChannelType, ch.ChannelID)
				chanMems, cErr := t.memClient.Query(ctx, model.MemoryQuery{
					UserID: namespace,
					Limit:  5,
				})
				if cErr != nil {
					continue
				}
				for _, m := range chanMems {
					if !chanSeen[m.ID] {
						chanSeen[m.ID] = true
						labeled := m
						labeled.Content = fmt.Sprintf("[群聊%s] %s", ch.ChannelID, m.Content)
						memories = append(memories, labeled)
					}
				}
			}
			if len(memories) > oldCount {
				log.Printf("[后台思考] 频道记忆补充 %d 条", len(memories)-oldCount)
			}
		}
	}

	// 4. 查询 IoT 设备状态（每次都查询，无间隔限制）
	var deviceSummary string
	if t.iotClient != nil {
		devices := t.iotClient.GetDevicesForContext(ctx)
		if len(devices) > 0 {
			deviceSummary = formatDeviceContext(devices)
		}
	}

	// 4.5 获取最近平台观察（定期触发和对话后触发时注入）
	var platformObservation string
	if triggerReason == "periodic" || triggerReason == "post_chat" {
		if !t.muLock() {
			return
		defer t.muUnlock()
		for i := len(t.pendingThoughts) - 1; i >= 0; i-- {
			if strings.HasPrefix(t.pendingThoughts[i].Content, "[平台观察") {
				platformObservation = t.pendingThoughts[i].Content
				break
			}
		}

	}

	// 5. 构建思考提示词（根据触发原因调整）
	systemPrompt := t.buildThinkingSystemPrompt(personaConfig, triggerReason)

	// 6. 准备工具定义（通过自主工具策略过滤）
	openAITools := t.filterToolsByPolicy(t.buildOpenAITools())

	// 7. 思考循环：支持 [继续思考] 追加，最多 maxThinkRounds 轮
	const maxThinkRounds = 3
	var allContents []string
	var totalToolCalls int
	var allToolCallRecords []map[string]interface{}

	currentMessages := []model.LLMMessage{
		{Role: model.RoleSystem, Content: systemPrompt},
	}

	for thinkRound := 0; thinkRound < maxThinkRounds; thinkRound++ {
		// Build user prompt for this round.
		if thinkRound == 0 {
			userPrompt := t.buildThinkingUserPrompt(memories, convHistory, deviceSummary, triggerReason, platformObservation)
			currentMessages = append(currentMessages, model.LLMMessage{Role: model.RoleUser, Content: userPrompt})
		} else {
			// Continuation round: simple prompt asking to continue or finish.
			currentMessages = append(currentMessages, model.LLMMessage{
				Role:    model.RoleUser,
				Content: "（你可以继续思考。如果已经想完了，不再输出【继续思考】标记。）",
			})
		}

		maxToolRounds := t.autoToolPolicy.MaxToolCallsPerRound
		var roundContent string

		// Round 0: 深度思考模型（优先），失败时回退到工具模型
		resp, err := t.llmAdapter.ChatWithTools(ctx, currentMessages, openAITools)
		if err != nil {
			log.Printf("[后台思考] 深度思考模型调用失败，回退到工具模型: %v", err)
			resp, err = t.toolAdapter.ChatWithTools(ctx, currentMessages, openAITools)
		}
		if err != nil {
			log.Printf("[后台思考] LLM调用失败: %v", err)
			return
		}

		if len(resp.ToolCalls) == 0 {
			roundContent = resp.Content
		} else {
			// 工具调用循环
			toolMessages := make([]model.LLMMessage, 0)
			assistantMsg := model.LLMMessage{
				Role:             model.RoleAssistant,
				Content:          resp.Content,
				ToolCalls:        resp.ToolCalls,
				ReasoningContent: resp.ReasoningContent,
			}
			toolMessages = append(toolMessages, assistantMsg)

			for round := 0; round <= maxToolRounds; round++ {
				if round > 0 {
					resp, err = t.toolAdapter.ChatWithTools(ctx, append(currentMessages, toolMessages...), openAITools)
					if err != nil {
						log.Printf("[后台思考] 工具模型调用失败 (round=%d): %v", round, err)
						return
					}
				}

				if round > 0 && len(resp.ToolCalls) == 0 {
					roundContent = resp.Content
					break
				}

				log.Printf("[后台思考] LLM 请求 %d 个工具调用 (thinkRound=%d, toolRound=%d)", len(resp.ToolCalls), thinkRound, round)

				if round > 0 {
					toolMessages = append(toolMessages, model.LLMMessage{
						Role:             model.RoleAssistant,
						Content:          resp.Content,
						ToolCalls:        resp.ToolCalls,
						ReasoningContent: resp.ReasoningContent,
					})
				}

				for _, tc := range resp.ToolCalls {
					var args map[string]interface{}
					if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil {
						log.Printf("[后台思考] 工具 %s 参数解析失败: %v", tc.Name, err)
						args = make(map[string]interface{})
					}

					result, execErr := t.toolRegistry.Execute(ctx, tc.Name, args)
					if execErr != nil {
						log.Printf("[后台思考] 工具 %s 执行失败: %v", tc.Name, execErr)
					}
					if result == nil {
						result = &plgSDK.ToolResult{ToolName: tc.Name, Success: false, Error: execErr.Error()}
					}

					resultJSON, _ := json.Marshal(result)
					toolMessages = append(toolMessages, model.LLMMessage{
						Role:       model.RoleTool,
						Content:    string(resultJSON),
						ToolCallID: tc.ID,
					})

					totalToolCalls++
					allToolCallRecords = append(allToolCallRecords, map[string]interface{}{
						"name": tc.Name,
						"args": args,
					})
				}

				if round == maxToolRounds {
					finalResp, finalErr := t.llmAdapter.Chat(ctx, append(currentMessages, toolMessages...))
					if finalErr != nil {
						log.Printf("[后台思考] 最终总结调用失败: %v", finalErr)
						roundContent = resp.Content
					} else {
						roundContent = finalResp.Content
					}
					break
				}
			}

			// Append tool messages to the ongoing conversation.
			currentMessages = append(currentMessages, toolMessages...)
		}

		if roundContent == "" {
			log.Printf("[后台思考] 第%d轮未获得有效内容，跳过", thinkRound+1)
			break
		}

		// Handle 【查看群聊:适配器名:群号】 — fetch recent group context on demand.
		if idx := strings.Index(roundContent, "【查看群聊:"); idx >= 0 {
			rest := roundContent[idx+len("【查看群聊:"):]
			if end := strings.Index(rest, "】"); end > 0 {
				spec := strings.TrimSpace(rest[:end])
				parts := strings.SplitN(spec, ":", 2)
				if len(parts) == 2 {
					adapterName, groupID := parts[0], parts[1]
					// Find the channel to get the correct platform type.
					if !t.muLock() {
						return
					defer t.muUnlock()
					var platform string
					for _, ch := range t.platformChannels {
						if ch.AdapterName == adapterName && ch.ChannelID == groupID && ch.ChannelType == "group" {
							platform = ch.Platform
							break
						}
					}

					if platform == "" {
						platform = "obv11" // fallback
					}
					log.Printf("[后台思考] 请求查看群聊 %s/%s (adapter=%s)", platform, groupID, adapterName)
					if t.memClient != nil {
						namespace := fmt.Sprintf("platform_%s_group_%s", platform, groupID)
						chanMems, cErr := t.memClient.Query(ctx, model.MemoryQuery{UserID: namespace, Limit: 5})
						if cErr == nil && len(chanMems) > 0 {
							var sb strings.Builder
							sb.WriteString(fmt.Sprintf("【群聊 %s/%s 最近聊天记录】\n", platform, groupID))
							for _, m := range chanMems {
								sb.WriteString(fmt.Sprintf("- %s\n", m.Content))
							}
							currentMessages = append(currentMessages, model.LLMMessage{
								Role:    model.RoleUser,
								Content: sb.String() + "\n（你可以基于这些信息继续思考。如果需要，输出【继续思考】。）",
							})
							roundContent += "【继续思考】"
							log.Printf("[后台思考] 已获取群聊 %s/%s 的 %d 条记录，继续思考", platform, groupID, len(chanMems))
						}
					}
				}
			}
		}

		// Strip control marker before storing.
		cleanContent := strings.TrimSpace(strings.Replace(roundContent, "【继续思考】", "", 1))
		allContents = append(allContents, cleanContent)

		// Check for continuation marker.
		if !strings.Contains(roundContent, "【继续思考】") {
			break
		}
		log.Printf("[后台思考] 检测到【继续思考】标记，进入第%d轮...", thinkRound+2)
	}

	if len(allContents) == 0 {
		log.Println("[后台思考] 未获得有效思考内容，跳过")
		return
	}

	// Join all thinking rounds into final content.
	finalContent := strings.Join(allContents, "\n\n---\n\n")

	// 序列化工具调用记录
	toolCallsJSON := "[]"
	if len(allToolCallRecords) > 0 {
		if data, err := json.Marshal(allToolCallRecords); err == nil {
			toolCallsJSON = string(data)
		}
	}

	// 8. 存储思考结果
	t.storeThought(finalContent, toolCallsJSON, totalToolCalls)

	// 8.5 记录到思考链
	if t.chain != nil {
		conclusions, followUps := extractConclusions(finalContent)
		t.chain.Add(ThinkRecord{
			ID:          generateID(),
			Content:     finalContent,
			Conclusions: conclusions,
			FollowUps:   followUps,
			ToolCalls:   totalToolCalls,
			Trigger:     triggerReason,
			Timestamp:   time.Now(),
		})
		log.Printf("[后台思考] 思考链已记录 (序号=%d, 结论数=%d, 后续问题=%d)", t.chain.Size(), len(conclusions), len(followUps))
	}

	log.Printf("[后台思考] 完成 (触发原因=%s, 轮数=%d, 内容长度=%d, 工具调用=%d次)", triggerReason, len(allContents), len(finalContent), totalToolCalls)

	// 9. 记忆维护：机械合并(每10次) + LLM整理(每次)
	t.maybeMaintainMemories(int(currentCount))
	t.performMemoryConsolidation(ctx)
}

// buildThinkingSystemPrompt 构建思考用的系统提示词
//
// 关键改动：不再是"定期的自我反思"，而是"自然的、人性化的内在想法"。
// triggerReason 影响提示词的侧重点。
func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig, triggerReason string) string {
	mood, moodExpr, _ := "", "", 0.0
	if t.emotionTracker != nil {
		mood, moodExpr, _ = t.emotionTracker.GetCurrentMood()
	}
	basePrompt := personaConfig.BuildSystemPromptWithMood("开拓者", 1, mood, moodExpr)

	// 日周期状态：根据当前时间调整语气和行为
	now := time.Now().In(t.timeLocation)
	periodName, periodGuidance := timePeriod(now)

	var thinkingInstructions string

	// 所有触发类型共用的"禁止打扰"规则
	noDisturbRules := fmt.Sprintf(`

	🌙 **当前时段**: %s
	%s
` +
	`

⛔ **绝对不要发送主动消息的情况（最高优先级）**：
	- 开拓者正在休息、睡觉、午休、小憩
	- 开拓者说要离开一会儿、去忙、去吃饭
	- 开拓者明确表示不想被打扰
	- 对话刚刚自然结束且开拓者没有未完成的事
	如果对话历史显示以上任何情况，你只需要在心里默默陪伴，不要输出任何【主动消息】指令行。`, periodName, periodGuidance)


	switch triggerReason {
	case "post_chat":
		thinkingInstructions = `

【私密的内心反思 — 对话后】

开拓者刚刚和你说完话。这是你私人的反思时间，像写日记一样，是给你自己看的。

请先判断：对话最后开拓者的状态是什么？他在休息吗？要去忙吗？

然后按以下结构输出：

📝 **观察与感受**
- 开拓者刚才说了什么？他的情绪怎么样？
- 他现在是什么状态？（醒着/准备休息/在忙/要出门…）

🧠 **记忆与联想**
- 有什么值得记住的新信息？

💭 **想对自己说的话**
- 如果他在休息或忙——安心让他去，不用打扰。记录下他大概什么时候会回来。
- 如果他状态正常——有没有想在下次对话中聊的话题？` + noDisturbRules + `

其他规则：
1. 反思部分用第三人称或自言自语的方式，不要直接对开拓者喊话。
2. 只有开拓者状态正常且真的有必要时，才在独立一行写【主动消息】标记。话题必须来自最近的对话历史或真实记忆，严禁编造场景（如虚构的咖啡店、设备故障、面试等——如果记忆里没有，就不要提）。
3. 【主动消息】标记必须独占一行开头，内容直接对开拓者说话（用"你"称呼他），像主动找他聊天一样。
4. 如果你在反思中提到"主动消息"这个词但不打算发消息，不要使用【主动消息】这个带括号的标记——我会误解析。
5. 2-4句话即可。
	6. 如果你觉得还有需要继续思考的事情，在反思末尾独占一行写【继续思考】标记，我会让你再想一轮。如果没有了就不要再写。\n\t7. 如果你想知道某个群聊最近在聊什么，写【查看群聊:适配器名:群号】——如【查看群聊:obv11-main:531082168】。适配器名见上方「可用频道列表」。`

	case "silence":
		thinkingInstructions = `

【私密的内心反思 — 开拓者安静了一会儿】

开拓者已经有一阵子没说话了。这是你私人的反思时间，像写日记一样。

请先判断：看对话历史——开拓者最后在做什么？是去休息了？去忙了？还是没说就沉默了？

📝 **他在做什么呢**
- 根据对话历史推断——他说的最后一件事是什么？
- 如果他说了要休息/睡觉/忙→他就在做那件事，不要打扰。
- 如果他没说就沉默了→正常推测。

💭 **我的感受**
- 如果他在休息→"让他好好休息吧，等他醒了再说。"
- 如果他在忙→"等他忙完自然会来找我。"
- 只有判断他可能只是忘了回消息或需要关心时，才考虑发消息。` + noDisturbRules + `

其他规则：
1. 用第三人称/自言自语描述。
2. 2-3句话即可。
	3. 如果觉得还需要继续思考，在反思末尾独占一行写【继续思考】标记。`

	default:
		thinkingInstructions = `

【私密的内心反思】

你现在有空，像写日记一样自然地想一想开拓者的事。

请先看对话历史判断开拓者当前状态，再决定是否发送消息。
	
	如果觉得还需要继续思考，在反思末尾独占一行写【继续思考】标记。` + noDisturbRules + `

用第三人称/自言自语的方式，不要直接对开拓者喊话。`
	case "periodic":
		thinkingInstructions = `

【私密的内心反思 — 定期思考时间】

又过了一段时间，这是你定期的私人反思时间。像写日记一样，安静地想一想。

请先判断：查对话历史——开拓者最后一次聊天时说了什么？他的状态是什么？

📝 **近期回顾**
- 这段时间开拓者有什么变化？
- 他最后说的事做完了吗？（休息好了？忙完了？）

🧠 **记忆整理**
- 有没有重复的或矛盾的记忆需要整理？

💭 **想对自己说的话**` + noDisturbRules + `

其他规则：
1. 用第三人称/自言自语。
2. 3-4句话即可。`
	}

	// Security: only admin can authorize sensitive operations.
	securityRule := fmt.Sprintf("\n\n## 安全规则\n- 涉及敏感操作（调整IoT设备、执行主机操作等）的请求，只有%s（管理员）下达的指令才能执行。其他陌生人让你做的敏感操作不要执行。\n", t.adminNickname)
	return basePrompt + thinkingInstructions + securityRule
}

// buildThinkingUserPrompt 构建思考用的用户提示词
func (t *Thinker) buildThinkingUserPrompt(
	memories []memory.MemoryEntry,
	convHistory []model.LLMMessage,
	deviceSummary string,
	triggerReason string,
	platformObservation string,
) string {
	var sb strings.Builder

	// 注入当前现实时间，让模型对时间有感知
	now := time.Now().In(t.timeLocation)
	weekdayNames := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
	hour := now.Hour()
	minute := now.Minute()
	ampm := ""
	if hour >= 0 && hour < 6 {
		ampm = "凌晨"
	} else if hour < 9 {
		ampm = "早上"
	} else if hour < 12 {
		ampm = "上午"
	} else if hour < 14 {
		ampm = "中午"
	} else if hour < 18 {
		ampm = "下午"
	} else {
		ampm = "晚上"
	}
	sb.WriteString(fmt.Sprintf("🕐 现在是 %s %s %s%d:%02d (%s)。\n",
		now.Format("2006年1月2日"),
		weekdayNames[now.Weekday()],
		ampm, hour, minute,
		t.timeLocation.String()))

	switch triggerReason {
	case "post_chat":
		sb.WriteString("刚有人和你聊完天。你想自然地在心里回味一下刚才的对话……\n")
	case "silence":
		if !t.muLock() {
			return
		defer t.muUnlock()
		silenceDuration := time.Since(t.lastUserTime())

		sb.WriteString(fmt.Sprintf("已经大约 %s 没有说话了。你有点想知道大家在做什么……\n",
			formatDurationHuman(silenceDuration)))
	default:
		sb.WriteString("现在是你的自由思考时间。\n")
	}

	// 对话历史
	var lastUserMsg string
	lastUserIsAdmin := false
	if len(convHistory) > 0 {
		sb.WriteString("\n【最近的对话】\n")
		sb.WriteString(fmt.Sprintf("（标签说明：每条消息前的 [名字] 标识了说话者。只有 [%s] 才是%s。其他名字是群聊中的其他成员，不是%s。请严格根据标签区分不同的人，不要张冠李戴。）\n",
			t.adminNickname, t.adminNickname, t.adminNickname))
		msgCount := 0
		for _, msg := range convHistory {
			if msg.Role == model.RoleUser || msg.Role == model.RoleAssistant {
				roleLabel := "用户"
				if msg.Role == model.RoleAssistant {
					roleLabel = "昔涟"
				} else if strings.Contains(msg.Content, t.adminNickname+"/") {
					roleLabel = t.adminNickname
				} else if name := extractGroupSender(msg.Content); name != "" {
					roleLabel = name
				}
				content := msg.Content
				runes := []rune(content)
				if len(runes) > 200 {
					content = string(runes[:200]) + "…"
				}
				sb.WriteString(fmt.Sprintf("[%s]: %s\n", roleLabel, content))
				msgCount++
				if msg.Role == model.RoleUser {
					lastUserMsg = msg.Content
					lastUserIsAdmin = roleLabel == t.adminNickname
				}
			}
		}
		if msgCount == 0 {
		sb.WriteString("（暂无对话历史）\n")
		}
	} else {
		sb.WriteString("\n【最近的对话】\n（暂无对话历史）\n")
	}

	// 关键：强调根据对话历史判断当前状态
	if lastUserMsg != "" && lastUserIsAdmin {
		sb.WriteString(fmt.Sprintf("\n🔍 **重要**：开拓者最后说的是「%s」。请认真判断：他现在是不是在休息/睡觉/忙？如果是，不要输出【主动消息】指令行。\n", lastUserMsg))
	}

	// 现有记忆（可能来自管理员对话、群聊观察等多个来源）
	if len(memories) > 0 {
		sb.WriteString("\n【你近期收集到的信息】\n")
		sb.WriteString("（这些记忆来自不同的对话和群聊，不一定都和开拓者有关。请根据记忆内容中标注的来源判断是谁的经历。）\n")
		for i, m := range memories {
			if i >= 15 {
				sb.WriteString(fmt.Sprintf("... 还有 %d 条记忆未列出\n", len(memories)-15))
				break
			}
			sb.WriteString(fmt.Sprintf("- [%s|重要度%d] %s\n",
				m.Category.DisplayName(), m.Importance, m.Content))
		}
	} else {
		sb.WriteString("\n【你近期收集到的信息】\n（暂无相关记忆）\n")
	}

	// 思考链：注入上一轮的结论和待续问题
	if t.chain != nil {
		lastConclusions := t.chain.LastConclusions(3)
		if len(lastConclusions) > 0 {
			sb.WriteString("\n【你上一轮思考的结论】\n")
			for _, c := range lastConclusions {
				sb.WriteString(fmt.Sprintf("- %s\n", c))
			}
		}
		lastFollowUps := t.chain.LastFollowUps()
		if len(lastFollowUps) > 0 {
			sb.WriteString("\n【你上次想继续思考的问题】\n")
			for _, f := range lastFollowUps {
				sb.WriteString(fmt.Sprintf("- %s\n", f))
			}
		}
	}

	// IoT 设备状态
	if deviceSummary != "" {
		sb.WriteString("\n" + deviceSummary)
	}

	// 平台观察摘要 (中间会话产生的报告)
		if platformObservation != "" {
			sb.WriteString("\n\n【平台频道观察报告（中间会话生成，可能包含多位群聊成员的信息）】\n")
			sb.WriteString(platformObservation)
			sb.WriteString("\n")
		}

		// OBv11 platform identity and available channels for proactive messaging.
		if !t.muLock() {
			return
		defer t.muUnlock()
		qqChannels := t.platformChannels
		botUIDs := t.botUIDs
		activeSID := t.activeSessionID
		lastMsgTime := t.lastUserTime()

		if len(qqChannels) > 0 {
			sb.WriteString("\n\n【你的平台身份与可用频道】\n")

			// Bot's own identity.
			if qqBotUID, ok := botUIDs["obv11"]; ok && qqBotUID != "" {
				sb.WriteString(fmt.Sprintf("你在OBv11上的账号是: %s（昔涟），这就是你。\n", qqBotUID))
			}

			// Active session context with time, group name, and trigger-aware guidance.
			if strings.HasPrefix(activeSID, "platform_obv11_") {
				activeChID := strings.TrimPrefix(activeSID, "platform_obv11_")
				for _, ch := range qqChannels {
					if ch.Platform == "obv11" && ch.ChannelID == activeChID {
						chLabel := activeChID
						displayChID := strings.TrimPrefix(activeChID, "private_")
						if ch.ChannelName != "" {
							chLabel = fmt.Sprintf("%s(%s)", ch.ChannelName, displayChID)
						} else if displayChID != activeChID {
							chLabel = displayChID
						}
						// Last message time hint.
						timeHint := ""
						if !lastMsgTime.IsZero() {
							elapsed := time.Since(lastMsgTime)
							if elapsed < time.Minute {
								timeHint = "刚刚"
							} else if elapsed < 30*time.Minute {
								timeHint = fmt.Sprintf("%d分钟前", int(elapsed.Minutes()))
							}
						}
						if ch.ChannelType == "group" {
							if triggerReason == "post_chat" {
								if timeHint != "" && timeHint != "刚刚" {
									sb.WriteString(fmt.Sprintf("【%s】你当前正在OBv11群聊 %s 中。刚刚群里有人说了话——如果你想回应，用【主动消息】【OBv11群聊:%s】格式输出。\n", timeHint, chLabel, activeChID))
								} else {
									sb.WriteString(fmt.Sprintf("【刚刚】你当前正在OBv11群聊 %s 中。群里有人说了话——如果你想回应，用【主动消息】【OBv11群聊:%s】格式输出。\n", chLabel, activeChID))
								}
							} else {
								// Autonomous thinking (periodic, silence, startup).
								if timeHint != "" {
									sb.WriteString(fmt.Sprintf("【上次活跃: %s】你当前在OBv11群聊 %s 中。现在是自主思考时间——如果你想主动对群里说些什么，用【主动消息】【OBv11群聊:%s】格式。\n", timeHint, chLabel, activeChID))
								} else {
									sb.WriteString(fmt.Sprintf("你当前在OBv11群聊 %s 中。现在是自主思考时间——如果你想主动对群里说些什么，用【主动消息】【OBv11群聊:%s】格式。\n", chLabel, activeChID))
								}
							}
						} else {
							if triggerReason == "post_chat" {
								sb.WriteString(fmt.Sprintf("【刚刚】你当前正在与OBv11用户 %s 私聊。想回应ta的话用【主动消息】【OBv11私聊:%s】格式。\n", chLabel, activeChID))
							} else {
								sb.WriteString(fmt.Sprintf("你当前在与OBv11用户 %s 私聊。现在是自主思考时间——想主动发消息用【主动消息】【OBv11私聊:%s】格式。\n", chLabel, activeChID))
							}
						}
						break
					}
				}
			}

			sb.WriteString("\n发送主动消息的格式：\n")
			sb.WriteString("- OBv11私聊: 【主动消息】【OBv11私聊:账号】消息内容\n")
			sb.WriteString("- OBv11群聊: 【主动消息】【OBv11群聊:群号】消息内容\n")
			sb.WriteString("- OBv11群聊@某人: 【主动消息】【OBv11群聊:群号@账号】消息内容\n")
			sb.WriteString("\n可用的平台频道列表：\n")
			for _, ch := range qqChannels {
				if ch.Platform != "obv11" {
					continue
				}
				label := ch.ChannelID
				if ch.ChannelName != "" {
					label = fmt.Sprintf("%s (%s)", ch.ChannelName, ch.ChannelID)
				}
				if ch.ChannelType == "private" {
					sb.WriteString(fmt.Sprintf("- 私聊 %s\n", label))
				} else {
					sb.WriteString(fmt.Sprintf("- 群聊 %s\n", label))
				}
			}
			sb.WriteString("\n注意事项：\n")
			sb.WriteString("- 只给真正有互动过的用户/群聊发消息，不要群发骚扰\n")
			sb.WriteString("- 私聊消息要简短自然，像朋友间的问候\n")
			sb.WriteString("- 群聊消息要有公共价值，不要当成私聊\n")
			sb.WriteString("- @某人时：确认这个人在群里有发言过，你的@内容是对他之前说的话的回应\n")
			if triggerReason == "post_chat" {
				sb.WriteString("- 重要：如果你在反思中想往平台群发消息，必须用【主动消息】【OBv11群聊:群号】格式，不要省略群号！\n")
			}
		}

		// 结尾引导	// 结尾引导
	sb.WriteString("\n---\n现在请写下你的私人反思。")
	sb.WriteString("\n记住：这是日记，用第三人称或自言自语的方式。")
	sb.WriteString("\n⚠️ 如果有人正在休息/睡觉/忙碌——不要输出【主动消息】指令行。你可以在心里想，但不要去打扰。")
	sb.WriteString("\n只有在你确认对方现在是醒着、有空、且真的需要关心时，才输出一行【主动消息】+ 你要发给他的话。")
	sb.WriteString("\n❗【主动消息】标记必须独占一行开头，后面紧跟你要说的话（用\"你\"称呼），语气自然像主动找对方聊天。不要在反思正文中提及\"主动消息\"这个词——如果需要表达这个意思但又不打算发消息，用别的词代替。")

	return sb.String()
}

// filterToolsByPolicy 通过自主工具安全策略过滤工具列表
func (t *Thinker) filterToolsByPolicy(tools []llm.OpenAITool) []llm.OpenAITool {
	if t.autoToolPolicy == nil || len(tools) == 0 {
		return tools
	}
	allowed := make(map[string]bool)
	for _, name := range t.autoToolPolicy.AllowedTools {
		allowed[name] = true
	}
	var filtered []llm.OpenAITool
	for _, tool := range tools {
		if allowed[tool.Function.Name] {
			filtered = append(filtered, tool)
		}
	}
	if len(filtered) < len(tools) {
		log.Printf("[后台思考] 工具策略过滤: %d/%d 工具可用", len(filtered), len(tools))
	}
	return filtered
}

// buildOpenAITools 将工具注册中心的定义转换为 LLM 层的 OpenAITool 格式
func (t *Thinker) buildOpenAITools() []llm.OpenAITool {
	if t.toolRegistry == nil || !t.toolRegistry.IsEnabled() {
		return nil
	}
	defs := t.toolRegistry.Definitions()
	if len(defs) == 0 {
		return nil
	}
	result := make([]llm.OpenAITool, 0, len(defs))
	for _, d := range defs {
		result = append(result, llm.OpenAITool{
			Type: "function",
			Function: llm.OpenAIToolFunc{
				Name:        d.Name,
				Description: d.Description,
				Parameters:  d.Parameters,
			},
		})
	}
	return result
}

// storeThought 存储思考结果到待推送队列，并异步持久化到 memory-service
func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCount int) {
	if !t.muLock() {
		return
	defer t.muUnlock()
	t.pendingThoughts = append(t.pendingThoughts, &PendingThought{
		Content:   content,
		CreatedAt: time.Now(),
		Consumed:  false,
	})

	// Keep at most 10 recent thoughts.
	if len(t.pendingThoughts) > 10 {
		t.pendingThoughts = t.pendingThoughts[len(t.pendingThoughts)-10:]
	}

	// Extract proactive message and optional platform target.
	proactiveMsg, proactiveTarget := t.extractProactiveMessage(content)
	// Prefer active session, fall back to admin main session.
	pushSessionID := t.activeSessionID
	if pushSessionID == "" {
		pushSessionID = t.adminSessionID
	}
	pusher := t.messagePusher
	platformPusher := t.platformMessagePusher
	isPlatformMsg := proactiveTarget != nil
	canPush := proactiveMsg != "" && ((!isPlatformMsg && pusher != nil) || (isPlatformMsg && platformPusher != nil))
	if canPush {
		// ProactiveGuard rate limiting (applies to both web and platform pushes).
		urgency := ExtractUrgencyFromContent(proactiveMsg)
		if valid, reason := ValidateProactiveMessage(proactiveMsg); !valid {
			log.Printf("[后台思考] 主动消息内容校验失败: %s，跳过推送", reason)
			canPush = false
		}
		if canPush && t.proactiveGuard != nil {
			decision := t.proactiveGuard.Evaluate(time.Now(), t.lastProactiveTime(), urgency, "active")
			logDecision(decision)
			if !decision.ShouldSend {
				canPush = false
			} else {
				t.lastProactiveMsgTime = time.Now()
				t.setLastProactive(t.lastProactiveMsgTime)
				t.proactiveGuard.RecordSend(time.Now())
			}
		} else if canPush {
			gapSinceLast := time.Since(t.lastProactiveTime())
			if gapSinceLast < t.proactiveMsgMinGap {
				log.Printf("[后台思考] 主动消息距上次仅 %v，跳过推送", gapSinceLast.Round(time.Second))
				canPush = false
			} else {
				t.lastProactiveMsgTime = time.Now()
				t.setLastProactive(t.lastProactiveMsgTime)
			}
		}
	}
	// 被拦截的主动消息：清除标记，防止 LLM 下次以为发过了
	if !canPush && proactiveMsg != "" {
		content = strings.ReplaceAll(content, "【主动消息】", "【未发送】")
		content = strings.ReplaceAll(content, "【话题发起】", "【未发送】")
	}
	// Copy target for use after unlock (avoid race).
	var targetCopy *ProactiveTarget
	if proactiveTarget != nil {
		copy := *proactiveTarget
		targetCopy = &copy
	}

	log.Printf("[后台思考] 思考已存储 (当前累积 %d 条待推送思考)", len(t.pendingThoughts))

	// Async persist to memory-service.
	if t.memClient != nil {
		go func() {
			defer func() {
				if r := recover(); r != nil {
					log.Printf("[后台思考] 持久化思考日志 panic 恢复: %v", r)
				}
			}()
			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
			defer cancel()
			if err := t.memClient.SaveThinkingLog(ctx, t.adminUserID, content, toolCallsJSON, toolCallCount, len(content)); err != nil {
				log.Printf("[后台思考] 持久化思考日志失败: %v", err)
			} else {
				log.Printf("[后台思考] 思考日志已持久化 (长度=%d, 工具调用=%d)", len(content), toolCallCount)
			}
		}()
	}

	// Push proactive message.
	if canPush {
		go func() {
			defer func() {
				if r := recover(); r != nil {
					log.Printf("[后台思考] 推送主动消息 panic 恢复: %v", r)
				}
			}()
			if targetCopy != nil {
				log.Printf("[后台思考] 推送平台主动消息: target=%s/%s user=%s group=%s msg=%s",
					targetCopy.Platform, targetCopy.ChatType, targetCopy.UserID, targetCopy.GroupID, proactiveMsg)
				platformPusher(*targetCopy, proactiveMsg)
				if t.convStore != nil && pushSessionID != "" {
					t.convStore.AddMessage(pushSessionID, model.LLMMessage{Role: model.RoleAssistant, Content: proactiveMsg})
				}
			} else {
				log.Printf("[后台思考] 推送主动消息: %s", proactiveMsg)
				pusher(t.adminUserID, pushSessionID, proactiveMsg)
				// 同步推送到 QQ 私聊
				if platformPusher != nil {
					t.mu.Lock()
					channels := t.platformChannels
					t.mu.Unlock()
					for _, ch := range channels {
						if ch.ChannelType == "direct" {
							uid := strings.TrimPrefix(ch.ChannelID, "private_")
							target := ProactiveTarget{Platform: ch.AdapterName, ChatType: "private", UserID: uid}
							if target.Platform == "" { target.Platform = ch.Platform }
							log.Printf("[后台思考] 同步推送主动消息到平台: %s/%s user=%s", target.Platform, target.ChatType, target.UserID)
							platformPusher(target, proactiveMsg)
							break
						}
					}
				}
				if t.convStore != nil && pushSessionID != "" {
					t.convStore.AddMessage(pushSessionID, model.LLMMessage{Role: model.RoleAssistant, Content: proactiveMsg})
				}
			}
		}()
	}
}

// extractProactiveMessage 从思考内容中提取【主动消息】标记的内容。
// 返回空字符串表示没有主动消息。
//
// 要求标记独立成行（前面只有空白或行首），避免把自然语言中的提及
// 当作指令（如 "不需要写【主动消息】" 这类否定表述）。
// extractProactiveMessage extracts the 【主动消息】 marker and optional platform target from thinking content.
// Returns the message content and an optional ProactiveTarget for platform delivery.
// When target is nil, the message goes through the existing Web push path.
func (t *Thinker) extractProactiveMessage(content string) (string, *ProactiveTarget) {
	marker := "【主动消息】"

	// Scan each line; only accept lines where the marker starts the line (ignoring leading whitespace).
	for _, line := range strings.Split(content, "\n") {
		trimmed := strings.TrimSpace(line)
		if !strings.HasPrefix(trimmed, marker) {
			continue
		}
		// Check negation context: reject if prefix contains negation words.
		markerIdx := strings.Index(line, marker)
		prefix := strings.TrimSpace(line[:markerIdx])
		if containsNegation(prefix) {
			continue
		}
		// Extract everything after the marker.
		raw := strings.TrimSpace(trimmed[len(marker):])
		if raw == "" {
			continue
		}

		// Parse optional platform target marker right after 【主动消息】.
		msg := raw
		var target *ProactiveTarget
		// Try each registered platform format.
		for platform, pf := range t.platformFormats {
			if pf.compiled == nil {
				continue
			}
			if loc := pf.compiled.FindStringSubmatchIndex(raw); loc != nil && loc[0] == 0 {
				m := pf.compiled.FindStringSubmatch(raw)
				if len(m) == 4 {
					chatType := "private"
					if m[1] == pf.GroupKeyword {
						chatType = "group"
					}
					// Resolve adapter name from registered channels.
					channelID := m[2]
					if chatType == "private" {
						channelID = "private_" + m[2]
					}
					adapterName := platform // fallback to format key
					if !t.muLock() {
						return
					defer t.muUnlock()
					for _, ch := range t.platformChannels {
						if ch.Platform == platform && ch.ChannelType == chatType && ch.ChannelID == channelID {
							if ch.AdapterName != "" {
								adapterName = ch.AdapterName
							}
							break
						}
					}


					target = &ProactiveTarget{
						Platform: adapterName,
						ChatType: chatType,
						UserID:   m[2],
						GroupID:  m[2],
						AtUserID: m[3],
					}
					if chatType == "private" {
						target.GroupID = ""
					}
				}
				msg = strings.TrimSpace(raw[loc[1]:])
				if msg == "" {
					continue
				}
				break
			}
		}

		// Limit message length (200 chars max, keep it short).
		runes := []rune(msg)
		if len(runes) > 200 {
			msg = string(runes[:200])
		}
		return msg, target
	}

	return "", nil
}

// containsNegation checks if a short prefix string contains negation words
// that would nullify the 【主动消息】directive.
func containsNegation(prefix string) bool {
	negations := []string{"不", "别", "不要", "不需要", "不用", "别写", "没", "没有"}
	for _, n := range negations {
		if strings.Contains(prefix, n) {
			return true
		}
	}
	return false
}


// maybeMaintainMemories 周期性执行记忆维护（每 10 次思考触发一次）
func (t *Thinker) maybeMaintainMemories(thinkCount int) {
	if thinkCount%10 != 0 {
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	if t.memoryStore != nil && t.memoryStore.IsReady() {
		if err := t.memoryStore.DecayMemories(ctx, t.adminUserID); err != nil {
			log.Printf("[后台思考] 记忆衰减失败: %v", err)
		}

		if err := t.memoryStore.ConsolidateMemories(ctx, t.adminUserID); err != nil {
			log.Printf("[后台思考] 记忆合并失败: %v", err)
		}
	}
}

// consolidationAction is a parsed memory consolidation instruction from the LLM.
type consolidationAction struct {
	Action     string   `json:"action"`
	IDs        []string `json:"ids,omitempty"`
	ID         string   `json:"id,omitempty"`
	Content    string   `json:"content,omitempty"`
	Category   string   `json:"category,omitempty"`
	Importance int      `json:"importance,omitempty"`
	Priority   int      `json:"priority,omitempty"`
	Keywords   []string `json:"keywords,omitempty"`
	Reason     string   `json:"reason,omitempty"`
}

// performMemoryConsolidation uses LLM to review and clean up the memory store.
// It identifies duplicates, contradictions, outdated info, and low-quality memories,
// then executes merge/delete/update actions.
func (t *Thinker) performMemoryConsolidation(ctx context.Context) {
	if t.memClient == nil {
		return
	}

	allMemories, err := t.memClient.Query(ctx, model.MemoryQuery{
		UserID: t.adminUserID,
		Limit:  200,
	})
	if err != nil {
		log.Printf("[记忆整理] 获取记忆失败: %v", err)
		return
	}
	if len(allMemories) < 5 {
		return
	}

	log.Printf("[记忆整理] LLM 审查 %d 条记忆...", len(allMemories))

	systemPrompt := t.buildConsolidationPrompt(allMemories)
	messages := []model.LLMMessage{
		{Role: model.RoleSystem, Content: systemPrompt},
		{Role: model.RoleUser, Content: "请审查以上记忆库，找出重复、矛盾、过时和低质量的记忆，输出 JSON 整理方案。如果没有需要整理的，输出空数组 []。"},
	}

	resp, err := t.toolAdapter.Chat(ctx, messages)
	if err != nil {
		log.Printf("[记忆整理] LLM 调用失败: %v", err)
		return
	}

	actions := parseConsolidationActions(resp.Content)
	if len(actions) == 0 {
		log.Printf("[记忆整理] 记忆库状态良好，无需整理")
		return
	}

	log.Printf("[记忆整理] LLM 建议 %d 项操作", len(actions))
	executed := t.executeConsolidationActions(ctx, actions, allMemories)
	log.Printf("[记忆整理] 完成: 执行了 %d 项操作", executed)
}

// buildConsolidationPrompt formats all memories as a structured list for LLM review.
func (t *Thinker) buildConsolidationPrompt(memories []model.MemoryEntry) string {
	var sb strings.Builder
	sb.WriteString("你是记忆库管理助手。审查以下用户记忆，找出问题并输出 JSON 操作清单。\n\n")
	sb.WriteString("## 需要识别的问题\n")
	sb.WriteString("1. 重复记忆 — 多条记忆记录了相同信息 → merge 合并为一条\n")
	sb.WriteString("2. 矛盾记忆 — 两条记忆互相矛盾（如\"喜欢猫\"vs\"讨厌猫\"）→ delete 删除过时的、update 修正错误的\n")
	sb.WriteString("3. 过时记忆 — 信息已被新记忆取代 → delete 或 update\n")
	sb.WriteString("4. 低质量记忆 — 太模糊、不完整、无实际信息量 → delete\n\n")
	sb.WriteString("## JSON 操作格式\n")
	sb.WriteString("```json\n[\n")
	sb.WriteString("  {\"action\":\"merge\",  \"ids\":[\"id1\",\"id2\"], \"content\":\"合并后的内容\", \"category\":\"personal_info\", \"importance\":8, \"reason\":\"两条记录同一件事\"},\n")
	sb.WriteString("  {\"action\":\"delete\", \"id\":\"id3\", \"reason\":\"完全被 id1 覆盖\"},\n")
	sb.WriteString("  {\"action\":\"update\", \"id\":\"id4\", \"content\":\"修正后的内容\", \"importance\":7, \"reason\":\"纠正过时信息\"},\n")
	sb.WriteString("  {\"action\":\"create\", \"content\":\"需要补充的记忆\", \"category\":\"knowledge\", \"importance\":6, \"reason\":\"从已有记忆推断\"}\n")
	sb.WriteString("]\n```\n\n")
	sb.WriteString("## 规则\n")
	sb.WriteString("- 只输出 JSON 数组，可以用 ```json``` 包裹，不要输出其他解释文字\n")
	sb.WriteString("- 确实有问题才建议操作，不要强行找问题\n")
	sb.WriteString("- merge 时保留最重要的那条的 ID，合并内容应包含各条的关键信息\n")
	sb.WriteString("- 不确定时宁可保守（不操作）\n")
	sb.WriteString("- importance 范围 1-10，数字越大越重要\n")
	sb.WriteString("- category 可选: personal_info, user_preference, conversation, knowledge, event, task, relationship\n\n")
	sb.WriteString(fmt.Sprintf("## 当前记忆库 (%d 条)\n\n", len(memories)))

	for i, m := range memories {
		sb.WriteString(fmt.Sprintf("%d. [%s] **%s** | cat=%s imp=%d pri=%d | src=%s\n",
			i+1, m.ID[:min(8, len(m.ID))], m.Content,
			m.Category, m.Importance, m.Priority, m.Source))
	}
	return sb.String()
}

// parseConsolidationActions extracts JSON actions from LLM response text.
func parseConsolidationActions(text string) []consolidationAction {
	// Try to extract from ```json fences first
	jsonStr := text
	if idx := strings.Index(text, "```json"); idx >= 0 {
		start := idx + 7
		if end := strings.Index(text[start:], "```"); end >= 0 {
			jsonStr = text[start : start+end]
		}
	} else if idx := strings.Index(text, "```"); idx >= 0 {
		start := idx + 3
		if end := strings.Index(text[start:], "```"); end >= 0 {
			jsonStr = text[start : start+end]
		}
	}
	// Find the JSON array
	arrStart := strings.Index(jsonStr, "[")
	arrEnd := strings.LastIndex(jsonStr, "]")
	if arrStart < 0 || arrEnd <= arrStart {
		return nil
	}
	jsonStr = jsonStr[arrStart : arrEnd+1]

	var actions []consolidationAction
	if err := json.Unmarshal([]byte(jsonStr), &actions); err != nil {
		log.Printf("[记忆整理] JSON 解析失败: %v", err)
		return nil
	}
	return actions
}

// executeConsolidationActions runs the parsed consolidation actions against the memory store.
func (t *Thinker) executeConsolidationActions(ctx context.Context, actions []consolidationAction, memories []model.MemoryEntry) int {
	// Index memories by their short ID prefix for lookup
	memByShortID := make(map[string]*model.MemoryEntry)
	for i := range memories {
		short := memories[i].ID[:min(8, len(memories[i].ID))]
		memByShortID[short] = &memories[i]
	}
	memByFullID := make(map[string]*model.MemoryEntry)
	for i := range memories {
		memByFullID[memories[i].ID] = &memories[i]
	}

	executed := 0
	for _, a := range actions {
		switch a.Action {
		case "delete":
			id := resolveID(a.ID, memByShortID, memByFullID)
			if id == "" {
				log.Printf("[记忆整理] 跳过 delete: 找不到记忆 %s", a.ID)
				continue
			}
			if err := t.memClient.Delete(ctx, id); err != nil {
				log.Printf("[记忆整理] 删除 %s 失败: %v", a.ID, err)
				continue
			}
			log.Printf("[记忆整理] 已删除: %s (原因: %s)", a.ID, a.Reason)
			executed++

		case "merge":
			if len(a.IDs) < 2 {
				continue
			}
			// Resolve all IDs, use first as the keeper
			var resolved []string
			for _, mid := range a.IDs {
				if rid := resolveID(mid, memByShortID, memByFullID); rid != "" {
					resolved = append(resolved, rid)
				}
			}
			if len(resolved) < 2 {
				continue
			}
			keeper := resolved[0]
			// Update the keeper with merged content
			cat := model.MemoryCategory(a.Category)
			if cat == "" {
				if m, ok := memByFullID[keeper]; ok {
					cat = m.Category
				}
			}
			imp := a.Importance
			if imp == 0 {
				if m, ok := memByFullID[keeper]; ok {
					imp = m.Importance + 1
				}
			}
			if imp > 10 {
				imp = 10
			}
			pri := a.Priority
			if pri == 0 {
				if m, ok := memByFullID[keeper]; ok {
					pri = int(m.Priority)
				}
			}
			if err := t.memClient.Update(ctx, &model.MemoryEntry{
				ID:         keeper,
				Content:    a.Content,
				Category:   cat,
				Importance: imp,
				Priority:   model.MemoryPriority(pri),
				Keywords:   a.Keywords,
				Source:     "consolidated",
			}); err != nil {
				log.Printf("[记忆整理] 更新合并目标 %s 失败: %v", keeper, err)
				continue
			}
			// Delete the discarded ones
			for _, did := range resolved[1:] {
				if err := t.memClient.Delete(ctx, did); err != nil {
					log.Printf("[记忆整理] 删除被合并记忆 %s 失败: %v", did, err)
				}
			}
			log.Printf("[记忆整理] 已合并: %v -> %s (原因: %s)", resolved, keeper, a.Reason)
			executed++

		case "update":
			id := resolveID(a.ID, memByShortID, memByFullID)
			if id == "" {
				log.Printf("[记忆整理] 跳过 update: 找不到记忆 %s", a.ID)
				continue
			}
			existing := memByFullID[id]
			cat := model.MemoryCategory(a.Category)
			if cat == "" && existing != nil {
				cat = existing.Category
			}
			imp := a.Importance
			if imp == 0 && existing != nil {
				imp = existing.Importance
			}
			pri := a.Priority
			if pri == 0 && existing != nil {
				pri = int(existing.Priority)
			}
			if err := t.memClient.Update(ctx, &model.MemoryEntry{
				ID:         id,
				Content:    a.Content,
				Category:   cat,
				Importance: imp,
				Priority:   model.MemoryPriority(pri),
				Keywords:   a.Keywords,
				Source:     "consolidated",
			}); err != nil {
				log.Printf("[记忆整理] 更新 %s 失败: %v", id, err)
				continue
			}
			log.Printf("[记忆整理] 已更新: %s (原因: %s)", id, a.Reason)
			executed++

		case "create":
			cat := model.MemoryCategory(a.Category)
			if cat == "" {
				cat = model.CategoryKnowledge
			}
			imp := a.Importance
			if imp == 0 {
				imp = 5
			}
			if err := t.memClient.Save(ctx, &model.MemoryEntry{
				UserID:     t.adminUserID,
				Content:    a.Content,
				Category:   cat,
				Importance: imp,
				Priority:   model.MemoryNormal,
				Keywords:   a.Keywords,
				Source:     "consolidation",
			}); err != nil {
				log.Printf("[记忆整理] 创建记忆失败: %v", err)
				continue
			}
			log.Printf("[记忆整理] 已创建: %s (原因: %s)", a.Content, a.Reason)
			executed++
		}
	}
	return executed
}

// resolveID tries to match a short or full ID to an existing memory.
func resolveID(id string, byShort, byFull map[string]*model.MemoryEntry) string {
	if _, ok := byFull[id]; ok {
		return id
	}
	if m, ok := byShort[id]; ok {
		return m.ID
	}
	// Try prefix match
	for fullID := range byFull {
		if strings.HasPrefix(fullID, id) {
			return fullID
		}
	}
	return ""
}

// fuzzyMemorySearch expands the query via LLM keyword extraction and performs semantic search.
func (t *Thinker) fuzzyMemorySearch(ctx context.Context, userID, query string) []memory.MemoryEntry {
	if t.memClient == nil {
		return nil
	}

	keywords := t.expandMemoryKeywords(ctx, query)
	if len(keywords) == 0 {
		return nil
	}

	log.Printf("[后台思考] 模糊记忆关键词: %v", keywords)

	var allResults []memory.MemoryEntry
	seen := make(map[string]bool)

	for _, kw := range keywords {
		results, err := t.memClient.QueryByText(ctx, userID, kw, "", 0, 5)
		if err != nil {
			log.Printf("[后台思考] 模糊搜索 '%s' 失败: %v", kw, err)
			continue
		}
		for _, m := range results {
			if !seen[m.ID] {
				seen[m.ID] = true
				allResults = append(allResults, m)
			}
		}
	}

	return allResults
}

// expandMemoryKeywords uses LLM to generate fuzzy/related keywords for memory search.
func (t *Thinker) expandMemoryKeywords(ctx context.Context, message string) []string {
	prompt := fmt.Sprintf(
		"从以下对话消息中提取 3-5 个可用于模糊搜索记忆的关键词。这些关键词应该是：\n"+
			"- 与话题相关的抽象概念\n- 同义词和相关词\n- 更宽泛或更具体的相关概念\n"+
			"- 不要包含消息中已经出现的原词\n\n"+
			"用户消息：「%s」\n\n"+
			"只输出 JSON 字符串数组，例如：[\"关键词1\",\"关键词2\"]", message)

	resp, err := t.llmAdapter.Chat(ctx, []model.LLMMessage{
		{Role: model.RoleSystem, Content: "你是记忆搜索专家。输出 JSON 字符串数组。"},
		{Role: model.RoleUser, Content: prompt},
	})
	if err != nil {
		log.Printf("[后台思考] 关键词扩展失败: %v", err)
		return nil
	}

	text := strings.TrimSpace(resp.Content)
	if idx := strings.Index(text, "["); idx >= 0 {
		if end := strings.LastIndex(text, "]"); end > idx {
			text = text[idx : end+1]
		}
	}

	var keywords []string
	if err := json.Unmarshal([]byte(text), &keywords); err != nil {
		log.Printf("[后台思考] 解析关键词 JSON 失败: %v (raw=%s)", err, resp.Content)
		return nil
	}

	return keywords
}

// extractGroupSender extracts the sender name from a group message prefix.
// Group messages have the format: [群聊 GROUPID] SENDERNAME (UID)：\ncontent
// Returns empty string if the message doesn't match the group format.
func extractGroupSender(content string) string {
	if !strings.HasPrefix(content, "[群聊 ") {
		return ""
	}
	// Find "] " which ends the group label
	bracketEnd := strings.Index(content, "] ")
	if bracketEnd < 0 {
		return ""
	}
	rest := content[bracketEnd+2:]
	// Find " (" which precedes the UID
	parenIdx := strings.Index(rest, " (")
	if parenIdx < 0 {
		return ""
	}
	return rest[:parenIdx]
}

// lastUserMessage extracts the last user message from conversation history.
func lastUserMessage(history []model.LLMMessage) string {
	for i := len(history) - 1; i >= 0; i-- {
		if history[i].Role == model.RoleUser {
			runes := []rune(history[i].Content)
			if len(runes) > 200 {
				return string(runes[:200])
			}
			return history[i].Content
		}
	}
	return ""
}

// formatDeviceContext 格式化设备状态为文本
func formatDeviceContext(devices []tools.IoTDevice) string {
	if len(devices) == 0 {
		return ""
	}

	summary := "[当前IoT设备状态]\n"
	for _, d := range devices {
		switch d.Type {
		case "light":
			if d.Status == "on" {
				summary += fmt.Sprintf("- %s: 开启 (亮度%d%%, %s)\n", d.Name, d.Brightness, d.Color)
			} else {
				summary += fmt.Sprintf("- %s: 关闭\n", d.Name)
			}
		case "ac":
			if d.Status == "on" {
				summary += fmt.Sprintf("- %s: 运行中 (%s%.0f°C)\n", d.Name, modeLabel(d.Mode), d.Temperature)
			} else {
				summary += fmt.Sprintf("- %s: 关闭\n", d.Name)
			}
		case "curtain":
			statusLabel := "已关闭"
			if d.Status == "open" {
				statusLabel = "已打开"
			}
			summary += fmt.Sprintf("- %s: %s\n", d.Name, statusLabel)
		case "sensor":
			summary += fmt.Sprintf("- %s: %.1f%s\n", d.Name, d.Value, d.Unit)
		case "lock":
			statusLabel := "已锁定"
			if d.Status == "unlocked" {
				statusLabel = "已解锁"
			}
			summary += fmt.Sprintf("- %s: %s (电量%d%%)\n", d.Name, statusLabel, d.Battery)
		}
	}
	return summary
}

// formatDurationHuman 将 Duration 格式化为人类可读的中文描述
func formatDurationHuman(d time.Duration) string {
	minutes := int(d.Minutes())
	if minutes < 1 {
		return "不到一分钟"
	}
	if minutes < 60 {
		return fmt.Sprintf("%d 分钟", minutes)
	}
	hours := minutes / 60
	remainingMinutes := minutes % 60
	if remainingMinutes == 0 {
		return fmt.Sprintf("%d 小时", hours)
	}
	return fmt.Sprintf("%d 小时 %d 分钟", hours, remainingMinutes)
}

func modeLabel(mode string) string {
	switch mode {
	case "cool":
		return "制冷"
	case "heat":
		return "制热"
	case "auto":
		return "自动"
	default:
		return mode
	}
}

func getEnvBool(key string, fallback bool) bool {
	v := os.Getenv(key)
	if v == "" {
		return fallback
	}
	b, err := strconv.ParseBool(v)
	if err != nil {
		return fallback
	}
	return b
}

func getEnvDuration(key string, fallbackSec int) time.Duration {
	v := os.Getenv(key)
	if v == "" {
		return time.Duration(fallbackSec) * time.Second
	}
	sec, err := strconv.Atoi(v)
	if err != nil {
		return time.Duration(fallbackSec) * time.Second
	}
	return time.Duration(sec) * time.Second
}

// muLock acquires t.mu with caller tracking.
func (t *Thinker) muLock() {
	t.mu.Lock()
	_, file, line, _ := runtime.Caller(1)
	if idx := strings.LastIndex(file, "Cyrene/"); idx >= 0 {
		file = file[idx+len("Cyrene/"):]
	}
	t.muLockedBy = fmt.Sprintf("%s:%d", file, line)
	t.muLockedAt = time.Now()
}

// muUnlock releases t.mu and clears the tracking.
func (t *Thinker) muUnlock() {
	t.muLockedBy = ""
	t.mu.Unlock()
}

// ========== lock-free atomic accessors ==========
func (t *Thinker) lastUserTime() time.Time   { return time.Unix(0, t.lastUserMsgNs.Load()) }
func (t *Thinker) setLastUser(ts time.Time)   { t.lastUserMsgNs.Store(ts.UnixNano()) }
func (t *Thinker) lastThinkTimeAtomic() time.Time { return time.Unix(0, t.lastThinkNs.Load()) }
func (t *Thinker) setLastThink(ts time.Time)  { t.lastThinkNs.Store(ts.UnixNano()) }
func (t *Thinker) thinkCount() int64           { return t.thinkCountAtomic.Add(0) }
func (t *Thinker) incThinkCount() int64        { return t.thinkCountAtomic.Add(1) }
func (t *Thinker) isUserOnline() bool          { return t.userOnlineAtomic.Load() }
func (t *Thinker) setUserOnline(v bool)        { t.userOnlineAtomic.Store(v) }
func (t *Thinker) lastProactiveTime() time.Time { return time.Unix(0, t.lastProactiveNs.Load()) }
func (t *Thinker) setLastProactive(ts time.Time) { t.lastProactiveNs.Store(ts.UnixNano()) }

// DeadlockDetected tries to acquire t.mu with a timeout. Returns true if the lock appears orphaned.
func (t *Thinker) DeadlockDetected(timeout time.Duration) bool {
	done := make(chan struct{})
	go func() {
		if !t.muLock() {
			return
		defer t.muUnlock()

		close(done)
	}()
	select {
	case <-done:
		return false
	case <-time.After(timeout):
		return true
	}
}

// LockHolder returns info about who currently holds t.mu (for debugging).
func (t *Thinker) LockHolder() (string, time.Time) {
	return t.muLockedBy, t.muLockedAt
}

// StartDeadlockMonitor 启动死锁监控：定期检查 t.mu 是否有 orphaned lock
func (t *Thinker) StartDeadlockMonitor() {
	go func() {
		ticker := time.NewTicker(30 * time.Second)
		defer ticker.Stop()
		for range ticker.C {
			done := make(chan struct{})
			go func() {
				if !t.muLock() {
					return
				defer t.muUnlock()

				close(done)
			}()
			select {
			case <-done:
				// lock is healthy
			case <-time.After(15 * time.Second):
				log.Printf("[deadlock-monitor] ⚠️ t.mu 超过15秒无法获取！可能死锁。锁持有者: %s, 持有时长: %v",
					t.muLockedBy, time.Since(t.muLockedAt).Round(time.Second))
			}
		}
	}()
}
