import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { showErrorAlert } from '~/composables/useErrorNotice'
import {
  dateToUnixSeconds,
  formatMessageFullTime,
  highlightKeyword
} from '~/lib/chat/formatters'

const DEFAULT_CHAT_SOURCE = 'auto'

export const createEmptySearchContext = () => ({
  active: false,
  kind: 'search',
  label: '',
  username: '',
  anchorId: '',
  anchorIndex: -1,
  hasMoreBefore: false,
  hasMoreAfter: false,
  loadingBefore: false,
  loadingAfter: false,
  savedMessages: null,
  savedMeta: null
})

export const useChatSearch = ({
  api,
  heatColor,
  contacts,
  selectedAccount,
  selectedContact,
  privacyMode,
  allMessages,
  messagesMeta,
  messages,
  messageContainerRef,
  messagePageSize,
  hasMoreMessages,
  isLoadingMessages,
  normalizeMessage,
  updateJumpToBottomState,
  scrollToMessageId,
  flashMessage,
  highlightMessageId,
  searchContext,
  selectContact,
  loadMoreMessages
}) => {
const isDesktopRenderer = () => {
if (!process.client || typeof window === 'undefined') return false
return !!window.wechatDesktop?.__brand
}

const logSearchPhase = (phase, details = {}) => {
const payload = {
  account: String(selectedAccount.value || '').trim(),
  selectedUsername: String(selectedContact.value?.username || '').trim(),
  contextUsername: String(searchContext.value?.username || '').trim(),
  ...details
}

if (isDesktopRenderer()) {
  try {
    window.wechatDesktop?.logDebug?.('chat-search', phase, payload)
  } catch {}
}

console.info(`[chat-search] ${phase}`, payload)
}

const describeEventTarget = (target) => {
if (!target || typeof target !== 'object') return ''
const nodeName = String(target.nodeName || '').trim().toLowerCase()
let cls = ''
try {
  cls = typeof target.className === 'string'
    ? target.className
    : String(target.className?.baseVal || '')
} catch {
  cls = ''
}
cls = String(cls || '').trim().replace(/\s+/g, '.')
if (!nodeName) return cls.slice(0, 120)
if (!cls) return nodeName
return `${nodeName}.${cls}`.slice(0, 120)
}

const summarizeHitForLog = (hit, idx) => ({
index: Number(idx ?? -1),
hitId: String(hit?.id || '').trim(),
hitUsername: String(hit?.username || '').trim(),
conversationName: String(hit?.conversationName || '').trim(),
senderUsername: String(hit?.senderUsername || '').trim(),
renderType: String(hit?.renderType || '').trim(),
createTime: Number(hit?.createTime || 0),
isSent: !!hit?.isSent
})

const buildRunSearchLogDetails = ({ source = 'unknown', reset = false } = {}) => {
const q = String(messageSearchQuery.value || '').trim()
return {
  source: String(source || 'unknown'),
  reset: !!reset,
  scope: String(messageSearchScope.value || 'conversation'),
  queryLength: q.length,
  selectedContactUsername: String(selectedContact.value?.username || '').trim(),
  sender: String(messageSearchSender.value || '').trim(),
  sessionType: String(messageSearchSessionType.value || '').trim(),
  rangeDays: String(messageSearchRangeDays.value || '').trim(),
  startDate: String(messageSearchStartDate.value || '').trim(),
  endDate: String(messageSearchEndDate.value || '').trim(),
  offset: Number(messageSearchOffset.value || 0),
  resultCount: Number(messageSearchResults.value?.length || 0)
}
}

const messageSearchOpen = ref(false)
const messageSearchQuery = ref('')
const messageSearchScope = ref('global') // conversation | global
const messageSearchRangeDays = ref('') // empty means no time filter
const messageSearchSessionType = ref('') // empty means all (global only): group | single
const messageSearchSender = ref('') // 发送者筛选
const messageSearchSenderOptions = ref([])
const messageSearchSenderLoading = ref(false)
const messageSearchSenderError = ref('')
const messageSearchSenderOptionsKey = ref('')
const messageSearchSenderDropdownOpen = ref(false)
const messageSearchSenderDropdownRef = ref(null)
const messageSearchSenderDropdownInputRef = ref(null)
const messageSearchSenderDropdownQuery = ref('')
const messageSearchStartDate = ref('') // 自定义开始日期
const messageSearchEndDate = ref('') // 自定义结束日期
const messageSearchResults = ref([])
const messageSearchLoading = ref(false)
const messageSearchError = ref('')
const messageSearchBackendStatus = ref('')
const messageSearchIndexInfo = ref(null)
const messageSearchHasMore = ref(false)
const messageSearchOffset = ref(0)
const messageSearchLimit = 50
const messageSearchTotal = ref(0)
const messageSearchSelectedIndex = ref(-1)
const messageSearchInputRef = ref(null)
let messageSearchDebounceTimer = null
let messageSearchIndexPollTimer = null

// 搜索UI增强
const searchInputFocused = ref(false)
const showAdvancedFilters = ref(false)
const searchHistory = ref([])
const SEARCH_HISTORY_KEY = 'wechat_search_history'
const MAX_SEARCH_HISTORY = 10

// 加载搜索历史
const loadSearchHistory = () => {
if (!process.client) return
try {
  const saved = localStorage.getItem(SEARCH_HISTORY_KEY)
  if (saved) {
    searchHistory.value = JSON.parse(saved) || []
  }
} catch (e) {
  searchHistory.value = []
}
}

// 保存搜索历史
const saveSearchHistory = (query) => {
if (!process.client) return
if (!query || !query.trim()) return
const q = query.trim()
try {
  let history = [...searchHistory.value]
  // 移除重复项
  history = history.filter(item => item !== q)
  // 添加到开头
  history.unshift(q)
  // 限制数量
  if (history.length > MAX_SEARCH_HISTORY) {
    history = history.slice(0, MAX_SEARCH_HISTORY)
  }
  searchHistory.value = history
  localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history))
} catch (e) {
  // ignore
}
}

// 清空搜索历史
const clearSearchHistory = () => {
if (!process.client) return
searchHistory.value = []
try {
  localStorage.removeItem(SEARCH_HISTORY_KEY)
} catch (e) {
  // ignore
}
}

// 应用搜索历史
const applySearchHistory = async (query) => {
messageSearchQuery.value = query
await runMessageSearch({ reset: true, source: 'history-apply' })
}

const messageSearchIndexExists = computed(() => !!messageSearchIndexInfo.value?.exists)
const messageSearchIndexReady = computed(() => !!messageSearchIndexInfo.value?.ready)
const messageSearchIndexBuildStatus = computed(() => String(messageSearchIndexInfo.value?.build?.status || ''))
const messageSearchIndexBuildIndexed = computed(() => Number(messageSearchIndexInfo.value?.build?.indexedMessages || 0))
const messageSearchIndexMetaCount = computed(() => {
const meta = messageSearchIndexInfo.value?.meta || {}
const v = meta.message_count ?? meta.messageCount ?? meta.message_count ?? 0
return Number(v || 0)
})

const messageSearchIndexProgressText = computed(() => {
if (messageSearchIndexBuildStatus.value !== 'building') return ''
const build = messageSearchIndexInfo.value?.build || {}
const n = Number(messageSearchIndexBuildIndexed.value || 0)
const completed = Number(build.completedConversations || 0)
const total = Number(build.totalConversations || 0)
const speed = Number(build.messagesPerSec || 0)
const parts = []
if (n > 0) parts.push(`已索引 ${n.toLocaleString()} 条`)
else parts.push('准备中')
if (total > 0) parts.push(`会话 ${completed.toLocaleString()}/${total.toLocaleString()}`)
if (speed > 0) parts.push(`${Math.round(speed).toLocaleString()} 条/秒`)
return parts.join('，')
})

const messageSearchIndexText = computed(() => {
if (!messageSearchIndexInfo.value) return ''
if (!messageSearchIndexExists.value) return '索引未建立'
if (messageSearchIndexBuildStatus.value === 'error') return '索引异常'
if (!messageSearchIndexReady.value) return '索引未完成，需重建'
const n = Number(messageSearchIndexMetaCount.value || 0)
return n > 0 ? `索引已就绪（${n.toLocaleString()} 条）` : '索引已就绪'
})

const messageSearchIndexActionText = computed(() => {
if (messageSearchIndexBuildStatus.value === 'building') return '建立中'
return messageSearchIndexExists.value ? '重建索引' : '建立索引'
})

const messageSearchIndexActionDisabled = computed(() => {
return messageSearchIndexBuildStatus.value === 'building' || messageSearchLoading.value
})

const formatCount = (n) => {
const v = Number(n || 0)
if (!Number.isFinite(v) || v <= 0) return ''
try {
  return v.toLocaleString()
} catch {
  return String(v)
}
}

const messageSearchSenderDisabled = computed(() => {
if (!selectedAccount.value) return true
return false
})

const messageSearchSelectedSenderInfo = computed(() => {
const u = String(messageSearchSender.value || '').trim()
if (!u) return null
const list = Array.isArray(messageSearchSenderOptions.value) ? messageSearchSenderOptions.value : []
const found = list.find((s) => String(s?.username || '').trim() === u)
if (found) return found
return { username: u, displayName: u, avatar: null, count: null }
})

const messageSearchSelectedSenderInitial = computed(() => {
const info = messageSearchSelectedSenderInfo.value
if (!info) return '人'
const n = String(info.displayName || info.username || '').trim()
return n ? n.charAt(0) : '人'
})

const messageSearchSenderLabel = computed(() => {
const cur = String(messageSearchSender.value || '').trim()
if (!cur) {
  return '不限发送者'
}
const info = messageSearchSelectedSenderInfo.value
return String(info?.displayName || info?.username || cur)
})

const filteredMessageSearchSenderOptions = computed(() => {
const list = Array.isArray(messageSearchSenderOptions.value) ? messageSearchSenderOptions.value : []
const q = String(messageSearchSenderDropdownQuery.value || '').trim().toLowerCase()
if (!q) return list
return list.filter((s) => {
  const u = String(s?.username || '').toLowerCase()
  const n = String(s?.displayName || '').toLowerCase()
  return u.includes(q) || n.includes(q)
})
})

const closeMessageSearchSenderDropdown = () => {
messageSearchSenderDropdownOpen.value = false
messageSearchSenderDropdownQuery.value = ''
}

const getMessageSearchSenderFacetKey = () => {
const acc = String(selectedAccount.value || '').trim()
if (!acc) return ''
const scope = String(messageSearchScope.value || 'conversation')
const conv = scope === 'conversation' ? String(selectedContact.value?.username || '') : ''
const q = String(messageSearchQuery.value || '').trim()
const range = String(messageSearchRangeDays.value || '')
const sd = String(messageSearchStartDate.value || '')
const ed = String(messageSearchEndDate.value || '')
const st = scope === 'global' ? String(messageSearchSessionType.value || '').trim() : ''
return [acc, scope, conv, q, range, sd, ed, st].join('|')
}

const ensureMessageSearchSendersLoaded = async () => {
const key = getMessageSearchSenderFacetKey()
if (!key) return
if (messageSearchSenderOptionsKey.value === key && !messageSearchSenderLoading.value) return
const list = await fetchMessageSearchSenders()
messageSearchSenderOptionsKey.value = key
return list
}

const toggleMessageSearchSenderDropdown = async () => {
if (messageSearchSenderDisabled.value) return
if (messageSearchSenderDropdownOpen.value) {
  closeMessageSearchSenderDropdown()
  return
}
messageSearchSenderDropdownOpen.value = true
await ensureMessageSearchSendersLoaded()
await nextTick()
try {
  messageSearchSenderDropdownInputRef.value?.focus?.()
} catch {}
}

const selectMessageSearchSender = (username) => {
messageSearchSender.value = String(username || '')
closeMessageSearchSenderDropdown()
}

const fetchMessageSearchIndexStatus = async () => {
if (!selectedAccount.value) return null
logSearchPhase('search-index-status:start', {
  queryLength: String(messageSearchQuery.value || '').trim().length
})
try {
  const resp = await api.getChatSearchIndexStatus({ account: selectedAccount.value, source: DEFAULT_CHAT_SOURCE })
  messageSearchIndexInfo.value = resp?.index || null
  logSearchPhase('search-index-status:end', {
    exists: !!messageSearchIndexInfo.value?.exists,
    ready: !!messageSearchIndexInfo.value?.ready,
    buildStatus: String(messageSearchIndexInfo.value?.build?.status || '').trim()
  })
  return messageSearchIndexInfo.value
} catch (e) {
  logSearchPhase('search-index-status:error', {
    error: String(e?.message || e || '')
  })
  return null
}
}

const fetchMessageSearchSenders = async () => {
messageSearchSenderError.value = ''
if (!selectedAccount.value) {
  messageSearchSenderOptions.value = []
  messageSearchSenderOptionsKey.value = ''
  logSearchPhase('search-senders:skip:no-account')
  return []
}

const scope = String(messageSearchScope.value || 'conversation')
const msgQ = String(messageSearchQuery.value || '').trim()

const params = {
  account: selectedAccount.value,
  source: DEFAULT_CHAT_SOURCE,
  limit: 200
}

if (scope === 'conversation') {
  if (!selectedContact.value?.username) {
    // 没有当前会话时，“发送者”筛选仍应可点开；按全局索引加载发送者，
    // 避免按钮在搜索前/结果后看起来失效。
    logSearchPhase('search-senders:fallback-global:no-selected-contact', { scope })
  } else {
    params.username = selectedContact.value.username
  }
}

if (msgQ) {
  params.message_q = msgQ
}

params.render_types = 'text'

const range = String(messageSearchRangeDays.value || '')
if (range === 'custom') {
  const start = dateToUnixSeconds(messageSearchStartDate.value, false)
  const end = dateToUnixSeconds(messageSearchEndDate.value, true)
  if (start != null) params.start_time = start
  if (end != null) params.end_time = end
  if (start != null && end != null && start > end) {
    messageSearchSenderError.value = '时间范围不合法：开始日期不能晚于结束日期'
    messageSearchSenderOptions.value = []
    messageSearchSenderOptionsKey.value = ''
    return []
  }
} else {
  const days = Number(range || 0)
  if (days > 0 && Number.isFinite(days)) {
    const end = Math.floor(Date.now() / 1000)
    const start = Math.max(0, end - Math.floor(days * 24 * 3600))
    params.start_time = start
    params.end_time = end
  }
}

if (scope === 'global') {
  const st = String(messageSearchSessionType.value || '').trim()
  if (st) params.session_type = st
}

messageSearchSenderLoading.value = true
logSearchPhase('search-senders:start', {
  scope,
  queryLength: msgQ.length,
  username: String(params.username || '').trim()
})
try {
  const resp = await api.listChatSearchSenders(params)
  const status = String(resp?.status || 'success')
  if (status !== 'success') {
    logSearchPhase('search-senders:non-success', {
      scope,
      status,
      queryLength: msgQ.length,
      message: String(resp?.message || '')
    })
    if (status !== 'index_building') {
      messageSearchSenderError.value = String(resp?.message || '加载发送者失败')
    }
    messageSearchSenderOptions.value = []
    messageSearchSenderOptionsKey.value = ''
    return []
  }
  const list = Array.isArray(resp?.senders) ? resp.senders : []
  messageSearchSenderOptions.value = list
  messageSearchSenderOptionsKey.value = getMessageSearchSenderFacetKey()
  const cur = String(messageSearchSender.value || '').trim()
  if (cur && !list.some((s) => String(s?.username || '').trim() === cur)) {
    messageSearchSender.value = ''
  }
  logSearchPhase('search-senders:end', {
    scope,
    queryLength: msgQ.length,
    username: String(params.username || '').trim(),
    optionCount: list.length
  })
  return list
} catch (e) {
  messageSearchSenderError.value = e?.message || '加载发送者失败'
  messageSearchSenderOptions.value = []
  messageSearchSenderOptionsKey.value = ''
  logSearchPhase('search-senders:error', {
    scope,
    queryLength: msgQ.length,
    error: String(e?.message || e || '')
  })
  return []
} finally {
  messageSearchSenderLoading.value = false
}
}

const stopMessageSearchIndexPolling = () => {
if (messageSearchIndexPollTimer) clearInterval(messageSearchIndexPollTimer)
messageSearchIndexPollTimer = null
}

const ensureMessageSearchIndexPolling = () => {
if (messageSearchIndexPollTimer) return
messageSearchIndexPollTimer = setInterval(async () => {
  if (!messageSearchOpen.value) {
    stopMessageSearchIndexPolling()
    return
  }

  const info = await fetchMessageSearchIndexStatus()
  const exists = !!info?.exists
  const ready = !!info?.ready
  const bs = String(info?.build?.status || '')
  const done = exists && ready && bs !== 'building'
  if (done) {
    stopMessageSearchIndexPolling()
    if (String(messageSearchScope.value || '') === 'conversation') {
      await fetchMessageSearchSenders()
    }
    if (String(messageSearchQuery.value || '').trim()) {
      await runMessageSearch({ reset: true, source: 'index-poll-ready' })
    }
  }
}, 1200)
}

const onMessageSearchIndexAction = async () => {
if (!selectedAccount.value) return
const rebuild = messageSearchIndexExists.value
try {
  const resp = await api.buildChatSearchIndex({ account: selectedAccount.value, rebuild, source: DEFAULT_CHAT_SOURCE })
  messageSearchIndexInfo.value = resp?.index || null
  messageSearchBackendStatus.value = 'index_building'
  ensureMessageSearchIndexPolling()
} catch (e) {
  messageSearchError.value = e?.message || '建立索引失败'
}
}
const getMessageSearchHitAvatarUrl = (hit) => {
if (!hit) return ''
const scope = String(messageSearchScope.value || '')
const url =
  scope === 'global'
    ? (hit.conversationAvatar || hit.senderAvatar || '')
    : (hit.senderAvatar || hit.conversationAvatar || '')
return String(url || '').trim()
}

const getMessageSearchHitAvatarAlt = (hit) => {
if (!hit) return '头像'
const scope = String(messageSearchScope.value || '')
if (scope === 'global') {
  const name = String(hit.conversationName || hit.username || '').trim()
  return name ? `${name} 头像` : '头像'
}
let name = String(hit.senderDisplayName || '').trim()
if (!name) {
  name = hit.isSent ? '我' : String(hit.senderUsername || '').trim()
}
return name ? `${name} 头像` : '头像'
}

const getMessageSearchHitAvatarInitial = (hit) => {
if (!hit) return '?'
const scope = String(messageSearchScope.value || '')
let text = ''
if (scope === 'global') {
  text = String(hit.conversationName || hit.username || '').trim()
} else {
  text = String(hit.senderDisplayName || '').trim()
  if (!text) {
    text = hit.isSent ? '我' : String(hit.senderUsername || '').trim()
  }
}
return (text.charAt(0) || '?').toString()
}

const buildTransientSearchTargetContact = ({ username, displayName = '', avatar = '', isGroup = null } = {}) => {
const u = String(username || '').trim()
if (!u) return null
const name = String(displayName || u).trim() || u
return {
  id: u,
  username: u,
  name,
  avatar: String(avatar || '').trim() || null,
  avatarColor: '#4B5563',
  lastMessage: '',
  lastMessageTime: '',
  unreadCount: 0,
  isGroup: typeof isGroup === 'boolean' ? isGroup : u.endsWith('@chatroom'),
  isTop: false
}
}

const resolveSearchTargetContact = ({ username, displayName = '', avatar = '', isGroup = null } = {}) => {
const u = String(username || '').trim()
if (!u) return null
const existing = contacts.value.find((c) => String(c?.username || '').trim() === u)
if (existing) return existing
if (String(selectedContact.value?.username || '').trim() === u) return selectedContact.value
return buildTransientSearchTargetContact({ username: u, displayName, avatar, isGroup })
}
const searchContextBannerText = computed(() => {
if (!searchContext.value?.active) return ''
const kind = String(searchContext.value.kind || 'search')
if (kind === 'date') {
  const label = String(searchContext.value.label || '').trim()
  return label ? `已定位到 ${label}（上下文模式）` : '已定位到指定日期（上下文模式）'
}
if (kind === 'first') {
  return '已从第一条消息开始阅读（上下文模式）'
}
return '已定位到搜索结果（上下文模式）'
})

// 回到最新按钮
const showJumpToBottom = ref(false)

// 时间侧边栏（按日期定位）
const timeSidebarOpen = ref(false)
const timeSidebarYear = ref(null)
const timeSidebarMonth = ref(null) // 1-12
const timeSidebarCounts = ref({}) // { 'YYYY-MM-DD': count }
const timeSidebarMax = ref(0)
const timeSidebarTotal = ref(0)
const timeSidebarLoading = ref(false)
const timeSidebarError = ref('')
const timeSidebarSelectedDate = ref('') // YYYY-MM-DD (current/selected day)
const isJumpingToFirst = ref(false)
// Simple in-memory cache per (account|username|YYYY-MM)
const timeSidebarCache = ref({})
const timeSidebarWeekdays = ['一', '二', '三', '四', '五', '六', '日']

const timeSidebarMonthLabel = computed(() => {
const y = Number(timeSidebarYear.value || 0)
const m = Number(timeSidebarMonth.value || 0)
if (!y || !m) return ''
return `${y}年${m}月`
})

const timeSidebarYearOptions = computed(() => {
// WeChat history normally starts after 2011, but keep a broader range for safety.
const nowY = new Date().getFullYear()
const minY = 2000
const maxY = Math.max(nowY, Number(timeSidebarYear.value || 0) || nowY)
const years = []
for (let y = maxY; y >= minY; y--) years.push(y)
return years
})

const timeSidebarActiveDays = computed(() => {
const counts = timeSidebarCounts.value || {}
const keys = Object.keys(counts)
return keys.length
})

const _pad2 = (n) => String(n).padStart(2, '0')

const _dateStrFromEpochSeconds = (ts) => {
  const t = Number(ts || 0)
  if (!t) return ''
  try {
    const d = new Date(t * 1000)
    return `${d.getFullYear()}-${_pad2(d.getMonth() + 1)}-${_pad2(d.getDate())}`
  } catch {
    return ''
  }
}

// Calendar heatmap color: keep the date readable while still showing activity intensity.
const _calendarHeatColor = (count, maxV) => {
  const v = Math.max(0, Number(count || 0))
  const m = Math.max(0, Number(maxV || 0))
  if (!(v > 0)) return ''
  if (!(m > 0) || typeof heatColor !== 'function') return 'rgba(3, 193, 96, 0.12)'
  const levels = 6
  const ratio = Math.max(0, Math.min(1, v / m))
  const level = Math.min(levels, Math.max(1, Math.ceil(ratio * levels)))
  const valueForLevel = Math.max(1, Math.round(level * (m / levels)))
  return heatColor(valueForLevel, m)
}

const timeSidebarCalendarCells = computed(() => {
  const y = Number(timeSidebarYear.value || 0)
  const m = Number(timeSidebarMonth.value || 0) // 1-12
  if (!y || !m) return []

  const daysInMonth = new Date(y, m, 0).getDate()
  const firstDow = new Date(y, m - 1, 1).getDay() // 0=Sun..6=Sat
  const offset = (firstDow + 6) % 7 // Monday=0

  const maxV = Math.max(0, Number(timeSidebarMax.value || 0))
  const counts = timeSidebarCounts.value || {}
  const selected = String(timeSidebarSelectedDate.value || '').trim()

  const out = []
  for (let i = 0; i < 42; i++) {
    const dayNum = i - offset + 1
    const inMonth = dayNum >= 1 && dayNum <= daysInMonth
    if (!inMonth) {
      out.push({
        key: `e:${y}-${m}:${i}`,
        day: '',
        dateStr: '',
        count: 0,
        countText: '',
        disabled: true,
        className: 'calendar-day-outside',
        style: null,
        title: ''
      })
      continue
    }

    const dateStr = `${y}-${_pad2(m)}-${_pad2(dayNum)}`
    const count = Math.max(0, Number(counts[dateStr] || 0))
    const disabled = count <= 0
    const style = !disabled
      ? { backgroundColor: _calendarHeatColor(count, Math.max(maxV, count)) }
      : null

    const className = [
      disabled ? 'calendar-day-empty' : '',
      !disabled ? 'calendar-day-has-messages' : '',
      (selected && dateStr === selected) ? 'calendar-day-selected' : ''
    ].filter(Boolean).join(' ')

    out.push({
      key: dateStr,
      day: String(dayNum),
      dateStr,
      count,
      countText: String(count),
      disabled,
      className,
      style,
      title: `${dateStr}：${count} 条`
    })
  }
  return out
})
const closeMessageSearch = (reason = 'manual') => {
logSearchPhase('message-search:close', {
  reason: String(reason || 'manual'),
  queryLength: String(messageSearchQuery.value || '').trim().length,
  resultCount: Number(messageSearchResults.value?.length || 0),
  selectedIndex: Number(messageSearchSelectedIndex.value ?? -1)
})
messageSearchOpen.value = false
closeMessageSearchSenderDropdown()
messageSearchError.value = ''
messageSearchLoading.value = false
messageSearchBackendStatus.value = ''
stopMessageSearchIndexPolling()
if (messageSearchDebounceTimer) clearTimeout(messageSearchDebounceTimer)
messageSearchDebounceTimer = null
}

let timeSidebarReqId = 0

const closeTimeSidebar = () => {
timeSidebarOpen.value = false
timeSidebarError.value = ''
}

const _timeSidebarCacheKey = ({ account, username, year, month }) => {
const acc = String(account || '').trim()
const u = String(username || '').trim()
const y = Number(year || 0)
const m = Number(month || 0)
return `${acc}|${u}|${y}-${_pad2(m)}`
}

const _applyTimeSidebarMonthData = (data) => {
const counts = (data && typeof data.counts === 'object' && !Array.isArray(data.counts)) ? data.counts : {}
timeSidebarCounts.value = counts
timeSidebarMax.value = Math.max(0, Number(data?.max || 0))
timeSidebarTotal.value = Math.max(0, Number(data?.total || 0))
}

const loadTimeSidebarMonth = async ({ year, month, force } = {}) => {
if (!selectedAccount.value) return
if (!selectedContact.value?.username) return

const y = Number(year || timeSidebarYear.value || 0)
const m = Number(month || timeSidebarMonth.value || 0)
if (!y || !m) return

timeSidebarYear.value = y
timeSidebarMonth.value = m

const key = _timeSidebarCacheKey({
  account: selectedAccount.value,
  username: selectedContact.value.username,
  year: y,
  month: m
})

if (!force) {
  const cached = timeSidebarCache.value[key]
  if (cached) {
    timeSidebarError.value = ''
    _applyTimeSidebarMonthData(cached)
    return
  }
}

const reqId = ++timeSidebarReqId
timeSidebarLoading.value = true
timeSidebarError.value = ''

try {
  const resp = await api.getChatMessageDailyCounts({
    account: selectedAccount.value,
    username: selectedContact.value.username,
    year: y,
    month: m,
    source: DEFAULT_CHAT_SOURCE
  })
  if (reqId !== timeSidebarReqId) return
  if (String(resp?.status || '') !== 'success') {
    throw new Error(String(resp?.message || '加载日历失败'))
  }

  const data = {
    counts: resp?.counts || {},
    max: Number(resp?.max || 0),
    total: Number(resp?.total || 0)
  }

  _applyTimeSidebarMonthData(data)
  timeSidebarCache.value = { ...timeSidebarCache.value, [key]: data }
} catch (e) {
  if (reqId !== timeSidebarReqId) return
  timeSidebarError.value = e?.message || '加载日历失败'
  _applyTimeSidebarMonthData({ counts: {}, max: 0, total: 0 })
} finally {
  if (reqId === timeSidebarReqId) {
    timeSidebarLoading.value = false
  }
}
}

const _pickTimeSidebarInitialYearMonth = () => {
const list = messages.value || []
const last = Array.isArray(list) && list.length ? list[list.length - 1] : null
const ts = Number(last?.createTime || 0)
const d = ts ? new Date(ts * 1000) : new Date()
return { year: d.getFullYear(), month: d.getMonth() + 1 }
}

const _applyTimeSidebarSelectedDate = async (dateStr, { syncMonth } = {}) => {
const ds = String(dateStr || '').trim()
if (!ds) return
if (timeSidebarSelectedDate.value !== ds) {
  timeSidebarSelectedDate.value = ds
}
if (!syncMonth || !timeSidebarOpen.value) return

const parts = ds.split('-')
const y = Number(parts?.[0] || 0)
const m = Number(parts?.[1] || 0)
if (!y || !m) return

if (Number(timeSidebarYear.value || 0) !== y || Number(timeSidebarMonth.value || 0) !== m) {
  timeSidebarYear.value = y
  timeSidebarMonth.value = m
  // Fire and forget; request id guard + cache inside loadTimeSidebarMonth will handle racing.
  await loadTimeSidebarMonth({ year: y, month: m, force: false })
}
}

const toggleTimeSidebar = async () => {
timeSidebarOpen.value = !timeSidebarOpen.value
if (!timeSidebarOpen.value) return
closeMessageSearch()

const { year, month } = _pickTimeSidebarInitialYearMonth()
timeSidebarYear.value = year
timeSidebarMonth.value = month

// Default selected day: current viewport's latest loaded message day (usually "latest").
const list = messages.value || []
const last = Array.isArray(list) && list.length ? list[list.length - 1] : null
const ds = _dateStrFromEpochSeconds(Number(last?.createTime || 0))
if (ds) await _applyTimeSidebarSelectedDate(ds, { syncMonth: false })

await loadTimeSidebarMonth({ year, month, force: false })
}

const prevTimeSidebarMonth = async () => {
const y0 = Number(timeSidebarYear.value || 0)
const m0 = Number(timeSidebarMonth.value || 0)
if (!y0 || !m0) return
const y = m0 === 1 ? (y0 - 1) : y0
const m = m0 === 1 ? 12 : (m0 - 1)
await loadTimeSidebarMonth({ year: y, month: m, force: false })
}

const nextTimeSidebarMonth = async () => {
const y0 = Number(timeSidebarYear.value || 0)
const m0 = Number(timeSidebarMonth.value || 0)
if (!y0 || !m0) return
const y = m0 === 12 ? (y0 + 1) : y0
const m = m0 === 12 ? 1 : (m0 + 1)
await loadTimeSidebarMonth({ year: y, month: m, force: false })
}

const onTimeSidebarYearMonthChange = async () => {
if (!timeSidebarOpen.value) return
const y = Number(timeSidebarYear.value || 0)
const m = Number(timeSidebarMonth.value || 0)
if (!y || !m) return
await loadTimeSidebarMonth({ year: y, month: m, force: false })
}

const ensureMessageSearchScopeValid = () => {
if (messageSearchScope.value === 'conversation' && !selectedContact.value) {
  messageSearchScope.value = 'global'
}
}

const toggleMessageSearch = async () => {
const nextOpen = !messageSearchOpen.value
logSearchPhase('message-search:toggle', {
  nextOpen,
  queryLength: String(messageSearchQuery.value || '').trim().length,
  resultCount: Number(messageSearchResults.value?.length || 0)
})
messageSearchOpen.value = nextOpen
ensureMessageSearchScopeValid()
if (!messageSearchOpen.value) {
  closeMessageSearch('toggle-close')
  return
}
closeTimeSidebar()
await nextTick()
try {
  messageSearchInputRef.value?.focus?.()
} catch {}
logSearchPhase('message-search:open:ready', {
  scope: String(messageSearchScope.value || 'conversation'),
  selectedContactUsername: String(selectedContact.value?.username || '').trim()
})
await fetchMessageSearchIndexStatus()
await fetchMessageSearchSenders()
if (String(messageSearchQuery.value || '').trim()) {
  await runMessageSearch({ reset: true, source: 'toggle-open-with-query' })
}
}

let messageSearchReqId = 0

const runMessageSearch = async ({ reset, source = 'unknown' } = {}) => {
if (!selectedAccount.value) {
  logSearchPhase('runMessageSearch:skip:no-account', buildRunSearchLogDetails({ source, reset }))
  return
}
ensureMessageSearchScopeValid()

const q = String(messageSearchQuery.value || '').trim()
logSearchPhase('runMessageSearch:start', buildRunSearchLogDetails({ source, reset }))
if (!q) {
  logSearchPhase('runMessageSearch:skip:empty-query', buildRunSearchLogDetails({ source, reset }))
  messageSearchResults.value = []
  messageSearchHasMore.value = false
  messageSearchError.value = ''
  messageSearchSelectedIndex.value = -1
  messageSearchBackendStatus.value = ''
  messageSearchTotal.value = 0
  stopMessageSearchIndexPolling()
  return
}

if (reset) {
  messageSearchOffset.value = 0
  messageSearchResults.value = []
  messageSearchSelectedIndex.value = -1
}

const reqId = ++messageSearchReqId
messageSearchLoading.value = true
messageSearchError.value = ''
messageSearchBackendStatus.value = ''
logSearchPhase('runMessageSearch:request:start', {
  ...buildRunSearchLogDetails({ source, reset }),
  reqId
})

const scope = String(messageSearchScope.value || 'conversation')

const params = {
  account: selectedAccount.value,
  source: DEFAULT_CHAT_SOURCE,
  q,
  limit: messageSearchLimit,
  offset: messageSearchOffset.value
}

params.render_types = 'text'

const range = String(messageSearchRangeDays.value || '')
if (range === 'custom') {
  const start = dateToUnixSeconds(messageSearchStartDate.value, false)
  const end = dateToUnixSeconds(messageSearchEndDate.value, true)
  if (start != null) params.start_time = start
  if (end != null) params.end_time = end
  if (start != null && end != null && start > end) {
    messageSearchLoading.value = false
    messageSearchError.value = '时间范围不合法：开始日期不能晚于结束日期'
    return
  }
} else {
  const days = Number(range || 0)
  if (days > 0 && Number.isFinite(days)) {
    const end = Math.floor(Date.now() / 1000)
    const start = Math.max(0, end - Math.floor(days * 24 * 3600))
    params.start_time = start
    params.end_time = end
  }
}

if (scope === 'global') {
  const st = String(messageSearchSessionType.value || '').trim()
  if (st) params.session_type = st
}

if (String(messageSearchSender.value || '').trim()) {
  params.sender = String(messageSearchSender.value || '').trim()
}

if (scope === 'conversation') {
  if (!selectedContact.value?.username) {
    logSearchPhase('runMessageSearch:skip:no-selected-contact', buildRunSearchLogDetails({ source, reset }))
    messageSearchLoading.value = false
    messageSearchError.value = '请选择一个会话再搜索'
    return
  }
  params.username = selectedContact.value.username
}

try {
  const resp = await api.searchChatMessages(params)
  if (reqId !== messageSearchReqId) {
    logSearchPhase('runMessageSearch:response:stale', {
      ...buildRunSearchLogDetails({ source, reset }),
      reqId,
      activeReqId: Number(messageSearchReqId || 0),
      status: String(resp?.status || '').trim()
    })
    return
  }

  if (resp?.index) {
    messageSearchIndexInfo.value = resp.index
  }

  const status = String(resp?.status || 'success')
  messageSearchBackendStatus.value = status

  if (status === 'index_building') {
    logSearchPhase('runMessageSearch:response:index-building', {
      ...buildRunSearchLogDetails({ source, reset }),
      reqId,
      status
    })
    if (reset) {
      messageSearchResults.value = []
      messageSearchSelectedIndex.value = -1
    }
    messageSearchHasMore.value = false
    messageSearchTotal.value = 0
    ensureMessageSearchIndexPolling()
    return
  }

  if (status === 'index_error') {
    logSearchPhase('runMessageSearch:response:index-error', {
      ...buildRunSearchLogDetails({ source, reset }),
      reqId,
      status,
      message: String(resp?.message || '')
    })
    if (reset) {
      messageSearchResults.value = []
      messageSearchSelectedIndex.value = -1
    }
    messageSearchHasMore.value = false
    messageSearchTotal.value = 0
    messageSearchError.value = String(resp?.message || '索引错误')
    stopMessageSearchIndexPolling()
    return
  }

  if (status !== 'success') {
    logSearchPhase('runMessageSearch:response:non-success', {
      ...buildRunSearchLogDetails({ source, reset }),
      reqId,
      status,
      message: String(resp?.message || '')
    })
    if (reset) {
      messageSearchResults.value = []
      messageSearchSelectedIndex.value = -1
    }
    messageSearchHasMore.value = false
    messageSearchTotal.value = 0
    messageSearchError.value = String(resp?.message || '搜索失败')
    stopMessageSearchIndexPolling()
    return
  }

  const hits = Array.isArray(resp?.hits) ? resp.hits : []
  if (reset) {
    messageSearchResults.value = hits
  } else {
    messageSearchResults.value = [...messageSearchResults.value, ...hits]
  }
  messageSearchHasMore.value = !!resp?.hasMore
  messageSearchTotal.value = Number(resp?.total ?? resp?.totalInScan ?? 0)
  stopMessageSearchIndexPolling()

  if (messageSearchSelectedIndex.value < 0 && messageSearchResults.value.length) {
    messageSearchSelectedIndex.value = 0
  }

  logSearchPhase('runMessageSearch:response:success', {
    ...buildRunSearchLogDetails({ source, reset }),
    reqId,
    status,
    hitsCount: hits.length,
    total: Number(resp?.total ?? resp?.totalInScan ?? 0),
    hasMore: !!resp?.hasMore,
    backendScope: String(resp?.scope || '').trim(),
    backendUsername: String(resp?.username || '').trim()
  })

  // 保存搜索历史（仅在有结果时保存）
  if (!privacyMode.value && reset && hits.length > 0) {
    saveSearchHistory(q)
  }
} catch (e) {
  if (reqId !== messageSearchReqId) return
  messageSearchError.value = e?.message || '搜索失败'
  logSearchPhase('runMessageSearch:error', {
    ...buildRunSearchLogDetails({ source, reset }),
    reqId,
    error: String(e?.message || e || '')
  })
} finally {
  if (reqId === messageSearchReqId) {
    messageSearchLoading.value = false
    logSearchPhase('runMessageSearch:finalize', {
      ...buildRunSearchLogDetails({ source, reset }),
      reqId,
      loading: !!messageSearchLoading.value,
      error: String(messageSearchError.value || '')
    })
  }
}
}

const loadMoreSearchResults = async () => {
if (!messageSearchHasMore.value) return
if (messageSearchLoading.value) return
messageSearchOffset.value = Number(messageSearchOffset.value || 0) + messageSearchLimit
await runMessageSearch({ reset: false, source: 'load-more' })
}

const exitSearchContext = async () => {
if (!searchContext.value?.active) return
const u = String(searchContext.value.username || '').trim()
const saved = searchContext.value.savedMessages
const savedMeta = searchContext.value.savedMeta

if (u && saved) {
  allMessages.value = { ...allMessages.value, [u]: saved }
}
if (u && savedMeta) {
  messagesMeta.value = { ...messagesMeta.value, [u]: savedMeta }
}

searchContext.value = {
  active: false,
  kind: 'search',
  label: '',
  username: '',
  anchorId: '',
  anchorIndex: -1,
  hasMoreBefore: false,
  hasMoreAfter: false,
  loadingBefore: false,
  loadingAfter: false,
  savedMessages: null,
  savedMeta: null
}
highlightMessageId.value = ''
await nextTick()
updateJumpToBottomState()
}

const locateSearchHit = async (hit) => {
if (!process.client) return
if (!selectedAccount.value) {
  logSearchPhase('locateSearchHit:skip:no-account', {
    hitId: String(hit?.id || '').trim(),
    hitUsername: String(hit?.username || '').trim()
  })
  return
}
if (!hit?.id) {
  logSearchPhase('locateSearchHit:skip:missing-hit-id', {
    hitKeys: Object.keys(hit || {})
  })
  return
}

const targetUsername = String(hit?.username || selectedContact.value?.username || '').trim()
if (!targetUsername) {
  logSearchPhase('locateSearchHit:skip:missing-target-username', {
    hitId: String(hit?.id || '').trim(),
    hitUsername: String(hit?.username || '').trim(),
    selectedUsernameFallback: String(selectedContact.value?.username || '').trim()
  })
  return
}

logSearchPhase('locateSearchHit:start', {
  hitId: String(hit?.id || '').trim(),
  hitUsername: String(hit?.username || '').trim(),
  targetUsername,
  conversationName: String(hit?.conversationName || '').trim()
})

const targetContact = resolveSearchTargetContact({
  username: targetUsername,
  displayName: String(hit?.conversationName || hit?.username || targetUsername).trim(),
  avatar: String(hit?.conversationAvatar || hit?.senderAvatar || '').trim(),
  isGroup: targetUsername.endsWith('@chatroom')
})
logSearchPhase('locateSearchHit:target-resolved', {
  hitId: String(hit?.id || '').trim(),
  targetUsername,
  contactResolved: !!targetContact,
  contactSource: targetContact
    ? (contacts.value.find((c) => String(c?.username || '').trim() === targetUsername) ? 'contacts' : 'transient')
    : 'none'
})
if (targetContact && selectedContact.value?.username !== targetUsername) {
  logSearchPhase('locateSearchHit:selectContact:start', {
    hitId: String(hit?.id || '').trim(),
    targetUsername
  })
  await selectContact(targetContact, { skipLoadMessages: true })
  logSearchPhase('locateSearchHit:selectContact:done', {
    hitId: String(hit?.id || '').trim(),
    targetUsername
  })
}

if (searchContext.value?.active && searchContext.value.username !== targetUsername) {
  await exitSearchContext()
  logSearchPhase('locateSearchHit:exitSearchContext:done', {
    hitId: String(hit?.id || '').trim(),
    targetUsername
  })
}

if (!searchContext.value?.active) {
  searchContext.value = {
    active: true,
    kind: 'search',
    label: '',
    username: targetUsername,
    anchorId: String(hit.id),
    anchorIndex: -1,
    hasMoreBefore: true,
    hasMoreAfter: true,
    loadingBefore: false,
    loadingAfter: false,
    savedMessages: allMessages.value[targetUsername] || [],
    savedMeta: messagesMeta.value[targetUsername] || null
  }
} else {
  searchContext.value.kind = 'search'
  searchContext.value.label = ''
  searchContext.value.anchorId = String(hit.id)
  searchContext.value.hasMoreBefore = true
  searchContext.value.hasMoreAfter = true
  searchContext.value.loadingBefore = false
  searchContext.value.loadingAfter = false
}
logSearchPhase('locateSearchHit:search-context:ready', {
  hitId: String(hit?.id || '').trim(),
  targetUsername,
  contextActive: !!searchContext.value?.active,
  savedMessagesCount: Number(searchContext.value?.savedMessages?.length || 0)
})

try {
  logSearchPhase('locateSearchHit:messagesAround:start', {
    hitId: String(hit?.id || '').trim(),
    targetUsername,
    before: 35,
    after: 35
  })
  const resp = await api.getChatMessagesAround({
    account: selectedAccount.value,
    username: targetUsername,
    anchor_id: String(hit.id),
    before: 35,
    after: 35,
    source: DEFAULT_CHAT_SOURCE
  })

  const raw = resp?.messages || []
  const mapped = raw.map(normalizeMessage)
  allMessages.value = { ...allMessages.value, [targetUsername]: mapped }
  messagesMeta.value = { ...messagesMeta.value, [targetUsername]: { total: mapped.length, hasMore: false } }
  logSearchPhase('locateSearchHit:messagesAround:end', {
    hitId: String(hit?.id || '').trim(),
    targetUsername,
    messageCount: mapped.length,
    anchorId: String(resp?.anchorId || hit?.id || '').trim(),
    anchorIndex: Number(resp?.anchorIndex ?? -1)
  })

  searchContext.value.anchorId = String(resp?.anchorId || hit.id)
  searchContext.value.anchorIndex = Number(resp?.anchorIndex ?? -1)

  logSearchPhase('locateSearchHit:scroll:start', {
    hitId: String(hit?.id || '').trim(),
    targetUsername,
    anchorId: String(searchContext.value.anchorId || '').trim(),
    messageCount: mapped.length
  })
  const ok = await scrollToMessageId(searchContext.value.anchorId)
  logSearchPhase('locateSearchHit:scroll:end', {
    hitId: String(hit?.id || '').trim(),
    targetUsername,
    anchorId: String(searchContext.value.anchorId || '').trim(),
    scrollFound: !!ok
  })
  if (ok) flashMessage(searchContext.value.anchorId)
} catch (e) {
  logSearchPhase('locateSearchHit:error', {
    hitId: String(hit?.id || '').trim(),
    targetUsername,
    error: String(e?.message || e || '')
  })
  showErrorAlert(e?.message || '定位失败')
}
}

const locateByAnchorId = async ({ targetUsername, anchorId, kind, label } = {}) => {
if (!process.client) return
if (!selectedAccount.value) return
const u = String(targetUsername || selectedContact.value?.username || '').trim()
const anchor = String(anchorId || '').trim()
if (!u || !anchor) return

const targetContact = resolveSearchTargetContact({
  username: u,
  displayName: String(selectedContact.value?.name || u).trim(),
  avatar: String(selectedContact.value?.avatar || '').trim(),
  isGroup: u.endsWith('@chatroom')
})
if (targetContact && selectedContact.value?.username !== u) {
  await selectContact(targetContact, { skipLoadMessages: true })
}

if (searchContext.value?.active && searchContext.value.username !== u) {
  await exitSearchContext()
}

const kindNorm = String(kind || 'search').trim() || 'search'
const labelNorm = String(label || '').trim()
const hasMoreBeforeInit = kindNorm === 'first' ? false : true

if (!searchContext.value?.active) {
  searchContext.value = {
    active: true,
    kind: kindNorm,
    label: labelNorm,
    username: u,
    anchorId: anchor,
    anchorIndex: -1,
    hasMoreBefore: hasMoreBeforeInit,
    hasMoreAfter: true,
    loadingBefore: false,
    loadingAfter: false,
    savedMessages: allMessages.value[u] || [],
    savedMeta: messagesMeta.value[u] || null
  }
} else {
  searchContext.value.kind = kindNorm
  searchContext.value.label = labelNorm
  searchContext.value.anchorId = anchor
  searchContext.value.username = u
  searchContext.value.hasMoreBefore = hasMoreBeforeInit
  searchContext.value.hasMoreAfter = true
  searchContext.value.loadingBefore = false
  searchContext.value.loadingAfter = false
}

try {
  const resp = await api.getChatMessagesAround({
    account: selectedAccount.value,
    username: u,
    anchor_id: anchor,
    before: 35,
    after: 35,
    source: DEFAULT_CHAT_SOURCE
  })

  const raw = resp?.messages || []
  const mapped = raw.map(normalizeMessage)
  allMessages.value = { ...allMessages.value, [u]: mapped }
  messagesMeta.value = { ...messagesMeta.value, [u]: { total: mapped.length, hasMore: false } }

  searchContext.value.anchorId = String(resp?.anchorId || anchor)
  searchContext.value.anchorIndex = Number(resp?.anchorIndex ?? -1)

  if (kindNorm === 'first') {
    await nextTick()
    const container = messageContainerRef.value
    if (container) container.scrollTop = 0
    updateJumpToBottomState()
    const firstMessageId = String(mapped[0]?.id || '').trim()
    if (firstMessageId) flashMessage(firstMessageId)
  } else {
    const ok = await scrollToMessageId(searchContext.value.anchorId)
    if (ok) flashMessage(searchContext.value.anchorId)
  }
} catch (e) {
  showErrorAlert(e?.message || '定位失败')
}
}

const locateByDate = async (dateStr) => {
if (!process.client) return
if (!selectedAccount.value) return
if (!selectedContact.value?.username) return

const ds = String(dateStr || '').trim()
if (!ds) return
await _applyTimeSidebarSelectedDate(ds, { syncMonth: true })

try {
  const resp = await api.getChatMessageAnchor({
    account: selectedAccount.value,
    username: selectedContact.value.username,
    kind: 'day',
    date: ds,
    source: DEFAULT_CHAT_SOURCE
  })
  const status = String(resp?.status || '')
  const anchorId = String(resp?.anchorId || '').trim()
  if (status !== 'success' || !anchorId) {
    window.alert('当日暂无聊天记录')
    return
  }
  await locateByAnchorId({ targetUsername: selectedContact.value.username, anchorId, kind: 'date', label: ds })
} catch (e) {
  showErrorAlert(e?.message || '定位失败')
}
}

const jumpToConversationFirst = async () => {
if (!process.client) return
if (!selectedAccount.value) return
if (!selectedContact.value?.username) return
if (isJumpingToFirst.value) return

isJumpingToFirst.value = true
try {
  const resp = await api.getChatMessageAnchor({
    account: selectedAccount.value,
    username: selectedContact.value.username,
    kind: 'first',
    source: DEFAULT_CHAT_SOURCE
  })
  const status = String(resp?.status || '')
  const anchorId = String(resp?.anchorId || '').trim()
  if (status !== 'success' || !anchorId) {
    window.alert('暂无聊天记录')
    return
  }
  const ds = _dateStrFromEpochSeconds(Number(resp?.createTime || 0))
  if (ds) await _applyTimeSidebarSelectedDate(ds, { syncMonth: true })
  await locateByAnchorId({ targetUsername: selectedContact.value.username, anchorId, kind: 'first', label: '' })
} catch (e) {
  showErrorAlert(e?.message || '定位失败')
} finally {
  isJumpingToFirst.value = false
}
}

const onTimeSidebarDayClick = async (cell) => {
if (!cell || cell.disabled) return
const ds = String(cell.dateStr || '').trim()
if (!ds) return
await locateByDate(ds)
}

const _mergeContextMessages = (username, nextList) => {
const u = String(username || '').trim()
if (!u) return
const list = Array.isArray(nextList) ? nextList : []
allMessages.value = { ...allMessages.value, [u]: list }
// Keep meta aligned; context mode doesn't rely on hasMore from meta.
const prevMeta = messagesMeta.value[u] || null
messagesMeta.value = {
  ...messagesMeta.value,
  [u]: {
    total: Math.max(Number(prevMeta?.total || 0), list.length),
    hasMore: false
  }
}
}

const loadMoreSearchContextAfter = async () => {
if (!process.client) return
if (!selectedAccount.value) return
if (!searchContext.value?.active) return
if (searchContext.value.loadingAfter) return
if (!searchContext.value.hasMoreAfter) return

const u = String(searchContext.value.username || selectedContact.value?.username || '').trim()
if (!u) return
const existing = allMessages.value[u] || []
const last = Array.isArray(existing) && existing.length ? existing[existing.length - 1] : null
const anchorId = String(last?.id || '').trim()
if (!anchorId) {
  searchContext.value.hasMoreAfter = false
  return
}

const ctxUsername = u
searchContext.value.loadingAfter = true
try {
  const resp = await api.getChatMessagesAround({
    account: selectedAccount.value,
    username: ctxUsername,
    anchor_id: anchorId,
    before: 0,
    after: messagePageSize,
    source: DEFAULT_CHAT_SOURCE
  })

  if (!searchContext.value?.active || String(searchContext.value.username || '').trim() !== ctxUsername) return

  const raw = resp?.messages || []
  const mapped = raw.map(normalizeMessage)

  const existingIds = new Set(existing.map((m) => String(m?.id || '')))
  const appended = []
  for (const m of mapped) {
    const id = String(m?.id || '').trim()
    if (!id) continue
    if (existingIds.has(id)) continue
    existingIds.add(id)
    appended.push(m)
  }

  if (!appended.length) {
    searchContext.value.hasMoreAfter = false
    return
  }

  _mergeContextMessages(ctxUsername, [...existing, ...appended])
} catch (e) {
  showErrorAlert(e?.message || '加载更多消息失败')
} finally {
  if (searchContext.value?.active && String(searchContext.value.username || '').trim() === ctxUsername) {
    searchContext.value.loadingAfter = false
  }
}
}

const loadMoreSearchContextBefore = async () => {
if (!process.client) return
if (!selectedAccount.value) return
if (!searchContext.value?.active) return
if (searchContext.value.loadingBefore) return
if (!searchContext.value.hasMoreBefore) return

const u = String(searchContext.value.username || selectedContact.value?.username || '').trim()
if (!u) return
const existing = allMessages.value[u] || []
const first = Array.isArray(existing) && existing.length ? existing[0] : null
const anchorId = String(first?.id || '').trim()
if (!anchorId) {
  searchContext.value.hasMoreBefore = false
  return
}

const c = messageContainerRef.value
const beforeScrollHeight = c ? c.scrollHeight : 0
const beforeScrollTop = c ? c.scrollTop : 0

const ctxUsername = u
searchContext.value.loadingBefore = true
try {
  const resp = await api.getChatMessagesAround({
    account: selectedAccount.value,
    username: ctxUsername,
    anchor_id: anchorId,
    before: messagePageSize,
    after: 0,
    source: DEFAULT_CHAT_SOURCE
  })

  if (!searchContext.value?.active || String(searchContext.value.username || '').trim() !== ctxUsername) return

  const raw = resp?.messages || []
  const mapped = raw.map(normalizeMessage)

  const existingIds = new Set(existing.map((m) => String(m?.id || '')))
  const prepended = []
  for (const m of mapped) {
    const id = String(m?.id || '').trim()
    if (!id) continue
    if (existingIds.has(id)) continue
    existingIds.add(id)
    prepended.push(m)
  }

  if (!prepended.length) {
    searchContext.value.hasMoreBefore = false
    return
  }

  _mergeContextMessages(ctxUsername, [...prepended, ...existing])

  await nextTick()
  const c2 = messageContainerRef.value
  if (c2) {
    const afterScrollHeight = c2.scrollHeight
    c2.scrollTop = beforeScrollTop + (afterScrollHeight - beforeScrollHeight)
  }
} catch (e) {
  showErrorAlert(e?.message || '加载更多消息失败')
} finally {
  if (searchContext.value?.active && String(searchContext.value.username || '').trim() === ctxUsername) {
    searchContext.value.loadingBefore = false
  }
}
}

const onSearchHitPointerDown = (hit, idx, event) => {
logSearchPhase('search-result:pointerdown', {
  ...summarizeHitForLog(hit, idx),
  button: Number(event?.button ?? -1),
  detail: Number(event?.detail ?? 0),
  target: describeEventTarget(event?.target),
  currentTarget: describeEventTarget(event?.currentTarget)
})
}

const onSearchHitClickCapture = (hit, idx, event) => {
logSearchPhase('search-result:click-capture', {
  ...summarizeHitForLog(hit, idx),
  button: Number(event?.button ?? -1),
  detail: Number(event?.detail ?? 0),
  target: describeEventTarget(event?.target),
  currentTarget: describeEventTarget(event?.currentTarget)
})
}

const isSearchResultSenderEvent = (event) => {
try {
  const target = event?.target
  if (!target || typeof target.closest !== 'function') return false
  return !!target.closest('.sidebar-result-sender-clickable')
} catch {
  return false
}
}

const onSearchHitClick = async (hit, idx, event) => {
if (isSearchResultSenderEvent(event)) {
  logSearchPhase('onSearchHitClick:ignored-sender', summarizeHitForLog(hit, idx))
  return
}
messageSearchSelectedIndex.value = Number(idx || 0)
logSearchPhase('onSearchHitClick', {
  ...summarizeHitForLog(hit, idx),
  selectedIndex: Number(messageSearchSelectedIndex.value || 0)
})
await locateSearchHit(hit)
logSearchPhase('onSearchHitClick:done', {
  ...summarizeHitForLog(hit, idx),
  selectedIndex: Number(messageSearchSelectedIndex.value || 0)
})
}

const onSearchResultSenderClick = async (hit, event) => {
try {
  event?.stopPropagation?.()
  event?.preventDefault?.()
} catch {}
const senderUsername = String(hit?.senderUsername || '').trim()
if (!senderUsername) return
logSearchPhase('search-result-sender:click', {
  hitId: String(hit?.id || '').trim(),
  senderUsername
})
if (String(messageSearchSender.value || '').trim() === senderUsername) {
  if (String(messageSearchQuery.value || '').trim()) {
    await runMessageSearch({ reset: true, source: 'result-sender-click-same' })
  }
  return
}
messageSearchSender.value = senderUsername
}

const onSearchNext = async () => {
const q = String(messageSearchQuery.value || '').trim()
if (!q) return

if (!messageSearchResults.value.length && !messageSearchLoading.value) {
  await runMessageSearch({ reset: true, source: 'search-next-bootstrap' })
}
if (!messageSearchResults.value.length) return

const cur = Number(messageSearchSelectedIndex.value || 0)
const next = (cur + 1) % messageSearchResults.value.length
messageSearchSelectedIndex.value = next
await locateSearchHit(messageSearchResults.value[next])
}

const onSearchPrev = async () => {
const q = String(messageSearchQuery.value || '').trim()
if (!q) return

if (!messageSearchResults.value.length && !messageSearchLoading.value) {
  await runMessageSearch({ reset: true, source: 'search-prev-bootstrap' })
}
if (!messageSearchResults.value.length) return

const cur = Number(messageSearchSelectedIndex.value || 0)
const prev = (cur - 1 + messageSearchResults.value.length) % messageSearchResults.value.length
messageSearchSelectedIndex.value = prev
await locateSearchHit(messageSearchResults.value[prev])
}
const openMessageSearch = async () => {
closeTimeSidebar()
messageSearchOpen.value = true
ensureMessageSearchScopeValid()
logSearchPhase('message-search:open:start', {
  scope: String(messageSearchScope.value || 'conversation'),
  queryLength: String(messageSearchQuery.value || '').trim().length
})
await nextTick()
try {
  messageSearchInputRef.value?.focus?.()
} catch {}
await fetchMessageSearchIndexStatus()
logSearchPhase('message-search:open:end', {
  scope: String(messageSearchScope.value || 'conversation'),
  queryLength: String(messageSearchQuery.value || '').trim().length
})
}
watch(messageSearchScope, async (next, prev) => {
logSearchPhase('message-search-scope:change', {
  previous: String(prev || '').trim(),
  next: String(next || '').trim(),
  open: !!messageSearchOpen.value,
  queryLength: String(messageSearchQuery.value || '').trim().length
})
if (!messageSearchOpen.value) return
ensureMessageSearchScopeValid()
closeMessageSearchSenderDropdown()
messageSearchSender.value = ''
messageSearchSenderOptions.value = []
messageSearchSenderOptionsKey.value = ''
await fetchMessageSearchSenders()
messageSearchOffset.value = 0
messageSearchResults.value = []
messageSearchSelectedIndex.value = -1
if (String(messageSearchQuery.value || '').trim()) {
  await runMessageSearch({ reset: true, source: 'scope-change' })
}
})

watch(messageSearchRangeDays, async (next, prev) => {
logSearchPhase('message-search-range:change', {
  previous: String(prev || '').trim(),
  next: String(next || '').trim(),
  open: !!messageSearchOpen.value,
  queryLength: String(messageSearchQuery.value || '').trim().length
})
if (!messageSearchOpen.value) return
closeMessageSearchSenderDropdown()
messageSearchOffset.value = 0
messageSearchResults.value = []
messageSearchSelectedIndex.value = -1
if (String(messageSearchQuery.value || '').trim()) {
  await runMessageSearch({ reset: true, source: 'range-change' })
}
})

watch(messageSearchSessionType, async (next, prev) => {
logSearchPhase('message-search-session-type:change', {
  previous: String(prev || '').trim(),
  next: String(next || '').trim(),
  open: !!messageSearchOpen.value,
  queryLength: String(messageSearchQuery.value || '').trim().length
})
if (!messageSearchOpen.value) return
if (String(messageSearchScope.value || '') !== 'global') return
closeMessageSearchSenderDropdown()
messageSearchSender.value = ''
messageSearchSenderOptions.value = []
messageSearchSenderOptionsKey.value = ''
await fetchMessageSearchSenders()
messageSearchOffset.value = 0
messageSearchResults.value = []
messageSearchSelectedIndex.value = -1
if (String(messageSearchQuery.value || '').trim()) {
  await runMessageSearch({ reset: true, source: 'session-type-change' })
}
})

watch([messageSearchStartDate, messageSearchEndDate], async ([nextStart, nextEnd], [prevStart, prevEnd]) => {
logSearchPhase('message-search-custom-range:change', {
  previousStart: String(prevStart || '').trim(),
  previousEnd: String(prevEnd || '').trim(),
  nextStart: String(nextStart || '').trim(),
  nextEnd: String(nextEnd || '').trim(),
  open: !!messageSearchOpen.value
})
if (!messageSearchOpen.value) return
if (String(messageSearchRangeDays.value || '') !== 'custom') return
closeMessageSearchSenderDropdown()
messageSearchOffset.value = 0
messageSearchResults.value = []
messageSearchSelectedIndex.value = -1
if (String(messageSearchQuery.value || '').trim()) {
  await runMessageSearch({ reset: true, source: 'custom-range-change' })
}
})

watch(messageSearchSender, async (next, prev) => {
logSearchPhase('message-search-sender:change', {
  previous: String(prev || '').trim(),
  next: String(next || '').trim(),
  open: !!messageSearchOpen.value,
  queryLength: String(messageSearchQuery.value || '').trim().length
})
if (!messageSearchOpen.value) return
messageSearchOffset.value = 0
messageSearchResults.value = []
messageSearchSelectedIndex.value = -1
if (String(messageSearchQuery.value || '').trim()) {
  await runMessageSearch({ reset: true, source: 'sender-change' })
}
})

watch(messageSearchQuery, (next, prev) => {
logSearchPhase('message-search-query:change', {
  previousLength: String(prev || '').trim().length,
  nextLength: String(next || '').trim().length,
  open: !!messageSearchOpen.value
})
if (!messageSearchOpen.value) return
if (messageSearchDebounceTimer) clearTimeout(messageSearchDebounceTimer)
messageSearchDebounceTimer = null
const q = String(messageSearchQuery.value || '').trim()
if (!q) {
  logSearchPhase('message-search-query:debounce:skip-empty', {
    queryLength: q.length
  })
  return
}
logSearchPhase('message-search-query:debounce:scheduled', {
  queryLength: q.length,
  delayMs: 280
})
messageSearchDebounceTimer = setTimeout(() => {
  logSearchPhase('message-search-query:debounce:fire', {
    queryLength: String(messageSearchQuery.value || '').trim().length
  })
  runMessageSearch({ reset: true, source: 'query-debounce' })
}, 280)
})

watch(
() => selectedContact.value?.username,
async () => {
  logSearchPhase('message-search-selected-contact:change', {
    open: !!messageSearchOpen.value,
    scope: String(messageSearchScope.value || '').trim(),
    selectedContactUsername: String(selectedContact.value?.username || '').trim()
  })
  if (!messageSearchOpen.value) return
  if (String(messageSearchScope.value || '') !== 'conversation') return
  closeMessageSearchSenderDropdown()
  messageSearchSender.value = ''
  messageSearchSenderOptions.value = []
  messageSearchSenderOptionsKey.value = ''
  await fetchMessageSearchSenders()
  if (String(messageSearchQuery.value || '').trim()) {
    await runMessageSearch({ reset: true, source: 'selected-contact-change' })
  }
}
)

watch(messageSearchOpen, (next, prev) => {
logSearchPhase('message-search-open:change', {
  previous: !!prev,
  next: !!next,
  queryLength: String(messageSearchQuery.value || '').trim().length,
  resultCount: Number(messageSearchResults.value?.length || 0)
})
})

watch(messageSearchResults, (next, prev) => {
const nextList = Array.isArray(next) ? next : []
const prevList = Array.isArray(prev) ? prev : []
logSearchPhase('message-search-results:change', {
  previousCount: prevList.length,
  nextCount: nextList.length,
  firstHitId: String(nextList[0]?.id || '').trim(),
  selectedIndex: Number(messageSearchSelectedIndex.value ?? -1)
})
})

const autoLoadReady = ref(true)

let timeSidebarScrollSyncRaf = null
const syncTimeSidebarSelectedDateFromScroll = () => {
if (!process.client) return
if (!timeSidebarOpen.value) return
if (!selectedContact.value) return

const c = messageContainerRef.value
if (!c) return

if (timeSidebarScrollSyncRaf) return
timeSidebarScrollSyncRaf = requestAnimationFrame(() => {
  timeSidebarScrollSyncRaf = null
  try {
    const containerRect = c.getBoundingClientRect()
    const targetY = containerRect.top + 24
    const els = c.querySelectorAll?.('[data-msg-id][data-create-time]') || []
    if (!els || !els.length) return

    let chosen = null
    for (const el of els) {
      const r = el.getBoundingClientRect?.()
      if (!r) continue
      if (r.bottom >= targetY) {
        chosen = el
        break
      }
    }
    if (!chosen) chosen = els[els.length - 1]
    const ts = Number(chosen?.getAttribute?.('data-create-time') || 0)
    const ds = _dateStrFromEpochSeconds(ts)
    if (!ds) return
    // Don't await inside rAF; keep scroll handler snappy.
    _applyTimeSidebarSelectedDate(ds, { syncMonth: true })
  } catch {}
})
}

const contextAutoLoadTopReady = ref(true)
const contextAutoLoadBottomReady = ref(true)

const onMessageScrollInContextMode = async () => {
const c = messageContainerRef.value
if (!c) return
if (!searchContext.value?.active) return

const distBottom = c.scrollHeight - c.scrollTop - c.clientHeight

// Reset "ready" gates when user scrolls away from edges.
if (c.scrollTop > 160) contextAutoLoadTopReady.value = true
if (distBottom > 160) contextAutoLoadBottomReady.value = true

if (c.scrollTop <= 60 && contextAutoLoadTopReady.value && searchContext.value.hasMoreBefore && !searchContext.value.loadingBefore) {
  contextAutoLoadTopReady.value = false
  await loadMoreSearchContextBefore()
  return
}

if (distBottom <= 80 && contextAutoLoadBottomReady.value && searchContext.value.hasMoreAfter && !searchContext.value.loadingAfter) {
  contextAutoLoadBottomReady.value = false
  await loadMoreSearchContextAfter()
}
}

const onMessageScroll = async () => {
const c = messageContainerRef.value
if (!c) return
updateJumpToBottomState()
if (!selectedContact.value) return

// Keep the time sidebar selection in sync with the current viewport.
syncTimeSidebarSelectedDateFromScroll()

if (searchContext.value?.active) {
  await onMessageScrollInContextMode()
  return
}

if (c.scrollTop > 300) {
  autoLoadReady.value = true
  return
}

if (c.scrollTop <= 240 && autoLoadReady.value && hasMoreMessages.value && !isLoadingMessages.value) {
  autoLoadReady.value = false
  await loadMoreMessages()
}
}

  const resetSearchState = () => {
    closeMessageSearch()
    closeTimeSidebar()
    timeSidebarYear.value = null
    timeSidebarMonth.value = null
    _applyTimeSidebarMonthData({ counts: {}, max: 0, total: 0 })
    timeSidebarError.value = ''
    timeSidebarSelectedDate.value = ''
    messageSearchResults.value = []
    messageSearchOffset.value = 0
    messageSearchHasMore.value = false
    messageSearchBackendStatus.value = ''
    messageSearchTotal.value = 0
    messageSearchIndexInfo.value = null
    messageSearchSelectedIndex.value = -1
    searchContext.value = createEmptySearchContext()
    highlightMessageId.value = ''
  }

  onMounted(() => {
    loadSearchHistory()
  })

  onUnmounted(() => {
    if (messageSearchDebounceTimer) clearTimeout(messageSearchDebounceTimer)
    messageSearchDebounceTimer = null
    stopMessageSearchIndexPolling()
    if (timeSidebarScrollSyncRaf) {
      cancelAnimationFrame(timeSidebarScrollSyncRaf)
      timeSidebarScrollSyncRaf = null
    }
  })

  return {
    messageSearchOpen,
    messageSearchQuery,
    messageSearchScope,
    messageSearchRangeDays,
    messageSearchSessionType,
    messageSearchSender,
    messageSearchSenderOptions,
    messageSearchSenderLoading,
    messageSearchSenderError,
    messageSearchSenderOptionsKey,
    messageSearchSenderDropdownOpen,
    messageSearchSenderDropdownRef,
    messageSearchSenderDropdownInputRef,
    messageSearchSenderDropdownQuery,
    messageSearchStartDate,
    messageSearchEndDate,
    messageSearchResults,
    messageSearchLoading,
    messageSearchError,
    messageSearchBackendStatus,
    messageSearchIndexInfo,
    messageSearchHasMore,
    messageSearchOffset,
    messageSearchTotal,
    messageSearchSelectedIndex,
    messageSearchInputRef,
    searchInputFocused,
    showAdvancedFilters,
    searchHistory,
    messageSearchIndexExists,
    messageSearchIndexReady,
    messageSearchIndexBuildStatus,
    messageSearchIndexBuildIndexed,
    messageSearchIndexMetaCount,
    messageSearchIndexProgressText,
    messageSearchIndexText,
    messageSearchIndexActionText,
    messageSearchIndexActionDisabled,
    messageSearchSenderDisabled,
    messageSearchSelectedSenderInfo,
    messageSearchSelectedSenderInitial,
    messageSearchSenderLabel,
    filteredMessageSearchSenderOptions,
    searchContextBannerText,
    timeSidebarOpen,
    timeSidebarYear,
    timeSidebarMonth,
    timeSidebarCounts,
    timeSidebarMax,
    timeSidebarTotal,
    timeSidebarLoading,
    timeSidebarError,
    timeSidebarSelectedDate,
    isJumpingToFirst,
    timeSidebarWeekdays,
    timeSidebarMonthLabel,
    timeSidebarYearOptions,
    timeSidebarActiveDays,
    timeSidebarCalendarCells,
    getMessageSearchHitAvatarUrl,
    getMessageSearchHitAvatarAlt,
    getMessageSearchHitAvatarInitial,
    closeMessageSearchSenderDropdown,
    ensureMessageSearchSendersLoaded,
    toggleMessageSearchSenderDropdown,
    selectMessageSearchSender,
    fetchMessageSearchIndexStatus,
    fetchMessageSearchSenders,
    onMessageSearchIndexAction,
    closeMessageSearch,
    closeTimeSidebar,
    loadTimeSidebarMonth,
    toggleTimeSidebar,
    prevTimeSidebarMonth,
    nextTimeSidebarMonth,
    onTimeSidebarYearMonthChange,
    toggleMessageSearch,
    openMessageSearch,
    runMessageSearch,
    loadMoreSearchResults,
    exitSearchContext,
    locateSearchHit,
    locateByAnchorId,
    locateByDate,
    jumpToConversationFirst,
    onTimeSidebarDayClick,
    loadMoreSearchContextAfter,
    loadMoreSearchContextBefore,
    onSearchHitPointerDown,
    onSearchHitClickCapture,
    onSearchHitClick,
    onSearchResultSenderClick,
    onSearchNext,
    onSearchPrev,
    syncTimeSidebarSelectedDateFromScroll,
    onMessageScrollInContextMode,
    onMessageScroll,
    clearSearchHistory,
    applySearchHistory,
    ensureMessageSearchScopeValid,
    resetSearchState
  }
}
