import { parentPort } from 'worker_threads'
import { wcdbService } from './wcdbService'
import { resolveAccountDir } from './accountDirResolver'

export interface TopContact {
  username: string
  displayName: string
  avatarUrl?: string
  messageCount: number
  sentCount: number
  receivedCount: number
}

export interface MonthlyTopFriend {
  month: number
  displayName: string
  avatarUrl?: string
  messageCount: number
}

export interface ChatPeakDay {
  date: string
  messageCount: number
  topFriend?: string
  topFriendCount?: number
}

export interface ActivityHeatmap {
  data: number[][]
}

export interface AnnualReportData {
  year: number
  totalMessages: number
  totalFriends: number
  coreFriends: TopContact[]
  monthlyTopFriends: MonthlyTopFriend[]
  peakDay: ChatPeakDay | null
  longestStreak: {
    friendName: string
    days: number
    startDate: string
    endDate: string
  } | null
  activityHeatmap: ActivityHeatmap
  midnightKing: {
    displayName: string
    count: number
    percentage: number
  } | null
  selfAvatarUrl?: string
  mutualFriend: {
    displayName: string
    avatarUrl?: string
    sentCount: number
    receivedCount: number
    ratio: number
  } | null
  socialInitiative: {
    initiatedChats: number
    receivedChats: number
    initiativeRate: number
    topInitiatedFriend?: string
    topInitiatedCount?: number
  } | null
  responseSpeed: {
    avgResponseTime: number
    fastestFriend: string
    fastestTime: number
  } | null
  topPhrases: {
    phrase: string
    count: number
  }[]
  snsStats?: {
    totalPosts: number
    typeCounts?: Record<string, number>
    topLikers: { username: string; displayName: string; avatarUrl?: string; count: number }[]
    topLiked: { username: string; displayName: string; avatarUrl?: string; count: number }[]
  }
  lostFriend: {
    username: string
    displayName: string
    avatarUrl?: string
    earlyCount: number
    lateCount: number
    periodDesc: string
  } | null
}

export interface AvailableYearsLoadProgress {
  years: number[]
  strategy: 'cache' | 'native' | 'hybrid'
  phase: 'cache' | 'native' | 'scan'
  statusText: string
  nativeElapsedMs: number
  scanElapsedMs: number
  totalElapsedMs: number
  switched?: boolean
  nativeTimedOut?: boolean
}

interface AvailableYearsLoadMeta {
  strategy: 'cache' | 'native' | 'hybrid'
  nativeElapsedMs: number
  scanElapsedMs: number
  totalElapsedMs: number
  switched: boolean
  nativeTimedOut: boolean
  statusText: string
}

class AnnualReportService {
  private readonly availableYearsCacheTtlMs = 10 * 60 * 1000
  private readonly availableYearsScanConcurrency = 4
  private readonly availableYearsColumnCache = new Map<string, string>()
  private readonly availableYearsCache = new Map<string, { years: number[]; updatedAt: number }>()

  constructor() {
  }

  private broadcastProgress(status: string, progress: number) {
    if (parentPort) {
      parentPort.postMessage({
        type: 'annualReport:progress',
        data: { status, progress }
      })
    }
  }

  private reportProgress(status: string, progress: number, onProgress?: (status: string, progress: number) => void) {
    if (onProgress) {
      onProgress(status, progress)
      return
    }
    this.broadcastProgress(status, progress)
  }

  private cleanAccountDirName(dirName: string): string {
    const trimmed = dirName.trim()
    if (!trimmed) return trimmed
    if (trimmed.toLowerCase().startsWith('wxid_')) {
      const match = trimmed.match(/^(wxid_[^_]+)/i)
      if (match) return match[1]
      return trimmed
    }
    const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
    const cleaned = suffixMatch ? suffixMatch[1] : trimmed

    return cleaned
  }

  private async ensureConnectedWithConfig(
    dbPath: string,
    decryptKey: string,
    wxid: string
  ): Promise<{ success: boolean; cleanedWxid?: string; rawWxid?: string; error?: string }> {
    if (!wxid) return { success: false, error: '未配置微信ID' }
    if (!dbPath) return { success: false, error: '未配置数据库路径' }
    if (!decryptKey) return { success: false, error: '未配置解密密钥' }

    const accountDir = resolveAccountDir(dbPath, wxid)
    if (!accountDir) return { success: false, error: '未找到账号目录' }

    const ok = await wcdbService.open(accountDir, decryptKey)
    if (!ok) return { success: false, error: 'WCDB 打开失败' }

    const cleanedWxid = this.cleanAccountDirName(wxid)
    return { success: true, cleanedWxid, rawWxid: wxid }
  }

  private async getPrivateSessions(cleanedWxid: string): Promise<string[]> {
    const sessionResult = await wcdbService.getSessions()
    if (!sessionResult.success || !sessionResult.sessions) return []
    const rows = sessionResult.sessions as Record<string, any>[]

    const excludeList = [
      'qqmail', 'fmessage', 'medianote', 'floatbottle',
      'newsapp', 'brandsessionholder', 'brandservicesessionholder',
      'notifymessage', 'opencustomerservicemsg', 'notification_messages',
      'userexperience_alarm', 'helper_folders', 'placeholder_foldgroup',
      '@helper_folders', '@placeholder_foldgroup'
    ]

    return rows
      .map((row) => row.username || row.user_name || row.userName || '')
      .filter((username) => {
        if (!username) return false
        if (username.includes('@chatroom')) return false
        if (username === 'filehelper') return false
        if (username.startsWith('gh_')) return false
        if (username.toLowerCase() === cleanedWxid.toLowerCase()) return false
        if (username.toLowerCase() === 'weixin') return false

        for (const prefix of excludeList) {
          if (username.startsWith(prefix) || username === prefix) return false
        }

        if (username.includes('@kefu.openim') || username.includes('@openim')) return false
        if (username.includes('service_')) return false

        return true
      })
  }

  private async getEdgeMessageTime(sessionId: string, ascending: boolean): Promise<number | null> {
    const cursor = await wcdbService.openMessageCursor(sessionId, 1, ascending, 0, 0)
    if (!cursor.success || !cursor.cursor) return null
    try {
      const batch = await wcdbService.fetchMessageBatch(cursor.cursor)
      if (!batch.success || !batch.rows || batch.rows.length === 0) return null
      const ts = parseInt(batch.rows[0].create_time || '0', 10)
      return ts > 0 ? ts : null
    } finally {
      await wcdbService.closeMessageCursor(cursor.cursor)
    }
  }

  private quoteSqlIdentifier(identifier: string): string {
    return `"${String(identifier || '').replace(/"/g, '""')}"`
  }

  private toUnixTimestamp(value: any): number {
    const n = Number(value)
    if (!Number.isFinite(n) || n <= 0) return 0
    // 兼容毫秒级时间戳
    const seconds = n > 1e12 ? Math.floor(n / 1000) : Math.floor(n)
    return seconds > 0 ? seconds : 0
  }

  private addYearsFromRange(years: Set<number>, firstTs: number, lastTs: number): boolean {
    let changed = false
    const currentYear = new Date().getFullYear()
    const minTs = firstTs > 0 ? firstTs : lastTs
    const maxTs = lastTs > 0 ? lastTs : firstTs
    if (minTs <= 0 || maxTs <= 0) return changed

    const minYear = new Date(minTs * 1000).getFullYear()
    const maxYear = new Date(maxTs * 1000).getFullYear()
    for (let y = minYear; y <= maxYear; y++) {
      if (y >= 2010 && y <= currentYear && !years.has(y)) {
        years.add(y)
        changed = true
      }
    }
    return changed
  }

  private normalizeAvailableYears(years: Iterable<number>): number[] {
    return Array.from(new Set(Array.from(years)))
      .filter((y) => Number.isFinite(y))
      .map((y) => Math.floor(y))
      .sort((a, b) => b - a)
  }

  private async forEachWithConcurrency<T>(
    items: T[],
    concurrency: number,
    handler: (item: T, index: number) => Promise<void>,
    shouldStop?: () => boolean
  ): Promise<void> {
    if (!items.length) return
    const workerCount = Math.max(1, Math.min(concurrency, items.length))
    let nextIndex = 0
    const workers: Promise<void>[] = []

    for (let i = 0; i < workerCount; i++) {
      workers.push((async () => {
        while (true) {
          if (shouldStop?.()) break
          const current = nextIndex
          nextIndex += 1
          if (current >= items.length) break
          await handler(items[current], current)
        }
      })())
    }

    await Promise.all(workers)
  }

  private async detectTimeColumn(dbPath: string, tableName: string): Promise<string | null> {
    const cacheKey = `${dbPath}\u0001${tableName}`
    if (this.availableYearsColumnCache.has(cacheKey)) {
      const cached = this.availableYearsColumnCache.get(cacheKey) || ''
      return cached || null
    }

    const result = await wcdbService.getMessageTableColumns(dbPath, tableName)
    if (!result.success || !Array.isArray(result.columns) || result.columns.length === 0) {
      this.availableYearsColumnCache.set(cacheKey, '')
      return null
    }

    const candidates = ['create_time', 'createtime', 'msg_create_time', 'msg_time', 'msgtime', 'time']
    const columns = new Set<string>()
    for (const columnName of result.columns) {
      const name = String(columnName || '').trim().toLowerCase()
      if (name) columns.add(name)
    }

    for (const candidate of candidates) {
      if (columns.has(candidate)) {
        this.availableYearsColumnCache.set(cacheKey, candidate)
        return candidate
      }
    }

    this.availableYearsColumnCache.set(cacheKey, '')
    return null
  }

  private async getTableTimeRange(dbPath: string, tableName: string): Promise<{ first: number; last: number } | null> {
    const cacheKey = `${dbPath}\u0001${tableName}`
    const cachedColumn = this.availableYearsColumnCache.get(cacheKey)
    const initialColumn = cachedColumn && cachedColumn.length > 0 ? cachedColumn : 'create_time'
    const tried = new Set<string>()

    const queryByColumn = async (column: string): Promise<{ first: number; last: number } | null> => {
      const result = await wcdbService.getMessageTableTimeRange(dbPath, tableName)
      if (!result.success || !result.data) return null
      const row = result.data as Record<string, any>
      const actualColumn = String(row.column || '').trim().toLowerCase()
      if (column && actualColumn && column.toLowerCase() !== actualColumn) return null
      const first = this.toUnixTimestamp(row.first_ts ?? row.firstTs ?? row.min_ts ?? row.minTs)
      const last = this.toUnixTimestamp(row.last_ts ?? row.lastTs ?? row.max_ts ?? row.maxTs)
      return { first, last }
    }

    tried.add(initialColumn)
    const quick = await queryByColumn(initialColumn)
    if (quick) {
      if (!cachedColumn) this.availableYearsColumnCache.set(cacheKey, initialColumn)
      return quick
    }

    const detectedColumn = await this.detectTimeColumn(dbPath, tableName)
    if (!detectedColumn || tried.has(detectedColumn)) {
      return null
    }

    return queryByColumn(detectedColumn)
  }

  private async getAvailableYearsByTableScan(
    sessionIds: string[],
    options?: { onProgress?: (years: number[]) => void; shouldCancel?: () => boolean }
  ): Promise<number[]> {
    const years = new Set<number>()
    let lastEmittedSize = 0

    const emitIfChanged = (force = false) => {
      if (!options?.onProgress) return
      const next = this.normalizeAvailableYears(years)
      if (!force && next.length === lastEmittedSize) return
      options.onProgress(next)
      lastEmittedSize = next.length
    }

    const shouldCancel = () => options?.shouldCancel?.() === true

    await this.forEachWithConcurrency(sessionIds, this.availableYearsScanConcurrency, async (sessionId) => {
      if (shouldCancel()) return
      const tableStats = await wcdbService.getMessageTableStats(sessionId)
      if (!tableStats.success || !Array.isArray(tableStats.tables) || tableStats.tables.length === 0) {
        return
      }

      for (const table of tableStats.tables as Record<string, any>[]) {
        if (shouldCancel()) return
        const tableName = String(table.table_name || table.name || '').trim()
        const dbPath = String(table.db_path || table.dbPath || '').trim()
        if (!tableName || !dbPath) continue

        const range = await this.getTableTimeRange(dbPath, tableName)
        if (!range) continue
        const changed = this.addYearsFromRange(years, range.first, range.last)
        if (changed) emitIfChanged()
      }
    }, shouldCancel)

    emitIfChanged(true)
    return this.normalizeAvailableYears(years)
  }

  private async getAvailableYearsByEdgeScan(
    sessionIds: string[],
    options?: { onProgress?: (years: number[]) => void; shouldCancel?: () => boolean }
  ): Promise<number[]> {
    const years = new Set<number>()
    let lastEmittedSize = 0
    const shouldCancel = () => options?.shouldCancel?.() === true

    const emitIfChanged = (force = false) => {
      if (!options?.onProgress) return
      const next = this.normalizeAvailableYears(years)
      if (!force && next.length === lastEmittedSize) return
      options.onProgress(next)
      lastEmittedSize = next.length
    }

    for (const sessionId of sessionIds) {
      if (shouldCancel()) break
      const first = await this.getEdgeMessageTime(sessionId, true)
      const last = await this.getEdgeMessageTime(sessionId, false)
      const changed = this.addYearsFromRange(years, first || 0, last || 0)
      if (changed) emitIfChanged()
    }
    emitIfChanged(true)
    return this.normalizeAvailableYears(years)
  }

  private buildAvailableYearsCacheKey(dbPath: string, cleanedWxid: string): string {
    return `${dbPath}\u0001${cleanedWxid}`
  }

  private getCachedAvailableYears(cacheKey: string): number[] | null {
    const cached = this.availableYearsCache.get(cacheKey)
    if (!cached) return null
    if (Date.now() - cached.updatedAt > this.availableYearsCacheTtlMs) {
      this.availableYearsCache.delete(cacheKey)
      return null
    }
    return [...cached.years]
  }

  private setCachedAvailableYears(cacheKey: string, years: number[]): void {
    const normalized = this.normalizeAvailableYears(years)

    this.availableYearsCache.set(cacheKey, {
      years: normalized,
      updatedAt: Date.now()
    })

    if (this.availableYearsCache.size > 8) {
      let oldestKey = ''
      let oldestTime = Number.POSITIVE_INFINITY
      for (const [key, val] of this.availableYearsCache) {
        if (val.updatedAt < oldestTime) {
          oldestTime = val.updatedAt
          oldestKey = key
        }
      }
      if (oldestKey) this.availableYearsCache.delete(oldestKey)
    }
  }

  private decodeMessageContent(messageContent: any, compressContent: any): string {
    let content = this.decodeMaybeCompressed(compressContent)
    if (!content || content.length === 0) {
      content = this.decodeMaybeCompressed(messageContent)
    }
    return content
  }

  private decodeMaybeCompressed(raw: any): string {
    if (!raw) return ''
    if (typeof raw === 'string') {
      if (raw.length === 0) return ''
      // 只有当字符串足够长（超过16字符）且看起来像 hex 时才尝试解码
      // 短字符串（如 "123456" 等纯数字）容易被误判为 hex
      if (raw.length > 16 && this.looksLikeHex(raw)) {
        const bytes = Buffer.from(raw, 'hex')
        if (bytes.length > 0) return this.decodeBinaryContent(bytes)
      }
      // 只有当字符串足够长（超过16字符）且看起来像 base64 时才尝试解码
      // 短字符串（如 "test", "home" 等）容易被误判为 base64
      if (raw.length > 16 && this.looksLikeBase64(raw)) {
        try {
          const bytes = Buffer.from(raw, 'base64')
          return this.decodeBinaryContent(bytes)
        } catch {
          return raw
        }
      }
      return raw
    }
    return ''
  }

  private decodeBinaryContent(data: Buffer): string {
    if (data.length === 0) return ''
    try {
      if (data.length >= 4) {
        const magic = data.readUInt32LE(0)
        if (magic === 0xFD2FB528) {
          const fzstd = require('fzstd')
          const decompressed = fzstd.decompress(data)
          return Buffer.from(decompressed).toString('utf-8')
        }
      }
      const decoded = data.toString('utf-8')
      const replacementCount = (decoded.match(/\uFFFD/g) || []).length
      if (replacementCount < decoded.length * 0.2) {
        return decoded.replace(/\uFFFD/g, '')
      }
      return data.toString('latin1')
    } catch {
      return ''
    }
  }

  private looksLikeHex(s: string): boolean {
    if (s.length % 2 !== 0) return false
    return /^[0-9a-fA-F]+$/.test(s)
  }

  private looksLikeBase64(s: string): boolean {
    if (s.length % 4 !== 0) return false
    return /^[A-Za-z0-9+/=]+$/.test(s)
  }

  private formatDateYmd(date: Date): string {
    const y = date.getFullYear()
    const m = String(date.getMonth() + 1).padStart(2, '0')
    const d = String(date.getDate()).padStart(2, '0')
    return `${y}-${m}-${d}`
  }

  private async computeLongestStreak(
    sessionIds: string[],
    beginTimestamp: number,
    endTimestamp: number,
    onProgress?: (status: string, progress: number) => void,
    progressStart: number = 0,
    progressEnd: number = 0
  ): Promise<{ sessionId: string; days: number; start: Date | null; end: Date | null }> {
    let bestSessionId = ''
    let bestDays = 0
    let bestStart: Date | null = null
    let bestEnd: Date | null = null
    let lastProgressAt = 0
    let lastProgressSent = progressStart

    const shouldReportProgress = onProgress && progressEnd > progressStart && sessionIds.length > 0
    let apiTimeMs = 0
    let jsTimeMs = 0

    for (let i = 0; i < sessionIds.length; i++) {
      const sessionId = sessionIds[i]
      const openStart = Date.now()
      const cursor = await wcdbService.openMessageCursorLite(sessionId, 2000, true, beginTimestamp, endTimestamp)
      apiTimeMs += Date.now() - openStart
      if (!cursor.success || !cursor.cursor) continue

      let lastDayIndex: number | null = null
      let currentStreak = 0
      let currentStart: Date | null = null
      let maxStreak = 0
      let maxStart: Date | null = null
      let maxEnd: Date | null = null

      try {
        let hasMore = true
        while (hasMore) {
          const fetchStart = Date.now()
          const batch = await wcdbService.fetchMessageBatch(cursor.cursor)
          apiTimeMs += Date.now() - fetchStart
          if (!batch.success || !batch.rows) break

          const processStart = Date.now()
          for (const row of batch.rows) {
            const createTime = parseInt(row.create_time || '0', 10)
            if (!createTime) continue

            const dt = new Date(createTime * 1000)
            const dayDate = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate())
            const dayIndex = Math.floor(dayDate.getTime() / 86400000)

            if (lastDayIndex !== null && dayIndex === lastDayIndex) continue

            if (lastDayIndex !== null && dayIndex - lastDayIndex === 1) {
              currentStreak++
            } else {
              currentStreak = 1
              currentStart = dayDate
            }

            if (currentStreak > maxStreak) {
              maxStreak = currentStreak
              maxStart = currentStart
              maxEnd = dayDate
            }

            lastDayIndex = dayIndex
          }
          jsTimeMs += Date.now() - processStart

          hasMore = batch.hasMore === true
          await new Promise(resolve => setImmediate(resolve))
        }
      } finally {
        const closeStart = Date.now()
        await wcdbService.closeMessageCursor(cursor.cursor)
        apiTimeMs += Date.now() - closeStart
      }

      if (maxStreak > bestDays) {
        bestDays = maxStreak
        bestSessionId = sessionId
        bestStart = maxStart
        bestEnd = maxEnd
      }

      if (shouldReportProgress) {
        const now = Date.now()
        if (now - lastProgressAt > 250) {
          const ratio = Math.min(1, (i + 1) / sessionIds.length)
          const progress = Math.floor(progressStart + ratio * (progressEnd - progressStart))
          if (progress > lastProgressSent) {
            lastProgressSent = progress
            lastProgressAt = now
            const label = `${i + 1}/${sessionIds.length}`
            const timing = (apiTimeMs > 0 || jsTimeMs > 0)
              ? `, DB ${(apiTimeMs / 1000).toFixed(1)}s / JS ${(jsTimeMs / 1000).toFixed(1)}s`
              : ''
            onProgress?.(`计算连续聊天... (${label}${timing})`, progress)
          }
        }
      }
    }

    return { sessionId: bestSessionId, days: bestDays, start: bestStart, end: bestEnd }
  }

  async getAvailableYears(params: {
    dbPath: string
    decryptKey: string
    wxid: string
    onProgress?: (payload: AvailableYearsLoadProgress) => void
    shouldCancel?: () => boolean
    nativeTimeoutMs?: number
  }): Promise<{ success: boolean; data?: number[]; error?: string; meta?: AvailableYearsLoadMeta }> {
    try {
      const isCancelled = () => params.shouldCancel?.() === true
      const totalStartedAt = Date.now()
      let nativeElapsedMs = 0
      let scanElapsedMs = 0
      let switched = false
      let nativeTimedOut = false
      let latestYears: number[] = []

      const emitProgress = (payload: {
        years?: number[]
        strategy: 'cache' | 'native' | 'hybrid'
        phase: 'cache' | 'native' | 'scan'
        statusText: string
        switched?: boolean
        nativeTimedOut?: boolean
      }) => {
        if (!params.onProgress) return
        if (Array.isArray(payload.years)) latestYears = payload.years
        params.onProgress({
          years: latestYears,
          strategy: payload.strategy,
          phase: payload.phase,
          statusText: payload.statusText,
          nativeElapsedMs,
          scanElapsedMs,
          totalElapsedMs: Date.now() - totalStartedAt,
          switched: payload.switched ?? switched,
          nativeTimedOut: payload.nativeTimedOut ?? nativeTimedOut
        })
      }

      const buildMeta = (
        strategy: 'cache' | 'native' | 'hybrid',
        statusText: string
      ): AvailableYearsLoadMeta => ({
        strategy,
        nativeElapsedMs,
        scanElapsedMs,
        totalElapsedMs: Date.now() - totalStartedAt,
        switched,
        nativeTimedOut,
        statusText
      })

      const conn = await this.ensureConnectedWithConfig(params.dbPath, params.decryptKey, params.wxid)
      if (!conn.success || !conn.cleanedWxid) return { success: false, error: conn.error, meta: buildMeta('hybrid', '连接数据库失败') }
      if (isCancelled()) return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') }
      const cacheKey = this.buildAvailableYearsCacheKey(params.dbPath, conn.cleanedWxid)
      const cached = this.getCachedAvailableYears(cacheKey)
      if (cached) {
        latestYears = cached
        emitProgress({
          years: cached,
          strategy: 'cache',
          phase: 'cache',
          statusText: '命中缓存，已快速加载年份数据'
        })
        return {
          success: true,
          data: cached,
          meta: buildMeta('cache', '命中缓存，已快速加载年份数据')
        }
      }

      const sessionIds = await this.getPrivateSessions(conn.cleanedWxid)
      if (sessionIds.length === 0) {
        return { success: false, error: '未找到消息会话', meta: buildMeta('hybrid', '未找到消息会话') }
      }
      if (isCancelled()) return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') }

      const nativeTimeoutMs = Math.max(1000, Math.floor(params.nativeTimeoutMs || 5000))
      const nativeStartedAt = Date.now()
      let nativeTicker: ReturnType<typeof setInterval> | null = null

      emitProgress({
        strategy: 'native',
        phase: 'native',
        statusText: '正在使用原生快速模式加载年份...'
      })
      nativeTicker = setInterval(() => {
        nativeElapsedMs = Date.now() - nativeStartedAt
        emitProgress({
          strategy: 'native',
          phase: 'native',
          statusText: '正在使用原生快速模式加载年份...'
        })
      }, 120)

      const nativeRace = await Promise.race([
        wcdbService.getAvailableYears(sessionIds)
          .then((result) => ({ kind: 'result' as const, result }))
          .catch((error) => ({ kind: 'error' as const, error: String(error) })),
        new Promise<{ kind: 'timeout' }>((resolve) => setTimeout(() => resolve({ kind: 'timeout' }), nativeTimeoutMs))
      ])

      if (nativeTicker) {
        clearInterval(nativeTicker)
        nativeTicker = null
      }
      nativeElapsedMs = Math.max(nativeElapsedMs, Date.now() - nativeStartedAt)

      if (isCancelled()) return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') }

      if (nativeRace.kind === 'result' && nativeRace.result.success && Array.isArray(nativeRace.result.data) && nativeRace.result.data.length > 0) {
        const years = this.normalizeAvailableYears(nativeRace.result.data)
        latestYears = years
        this.setCachedAvailableYears(cacheKey, years)
        emitProgress({
          years,
          strategy: 'native',
          phase: 'native',
          statusText: '原生快速模式加载完成'
        })
        return {
          success: true,
          data: years,
          meta: buildMeta('native', '原生快速模式加载完成')
        }
      }

      switched = true
      nativeTimedOut = nativeRace.kind === 'timeout'
      emitProgress({
        strategy: 'hybrid',
        phase: 'native',
        statusText: nativeTimedOut
          ? '原生快速模式超时，已自动切换到扫表兼容模式...'
          : '原生快速模式不可用，已自动切换到扫表兼容模式...',
        switched: true,
        nativeTimedOut
      })

      const scanStartedAt = Date.now()
      let scanTicker: ReturnType<typeof setInterval> | null = null
      scanTicker = setInterval(() => {
        scanElapsedMs = Date.now() - scanStartedAt
        emitProgress({
          strategy: 'hybrid',
          phase: 'scan',
          statusText: nativeTimedOut
            ? '原生已超时，正在使用扫表兼容模式加载年份...'
            : '正在使用扫表兼容模式加载年份...',
          switched: true,
          nativeTimedOut
        })
      }, 120)

      let years = await this.getAvailableYearsByTableScan(sessionIds, {
        onProgress: (items) => {
          latestYears = items
          scanElapsedMs = Date.now() - scanStartedAt
          emitProgress({
            years: items,
            strategy: 'hybrid',
            phase: 'scan',
            statusText: nativeTimedOut
              ? '原生已超时，正在使用扫表兼容模式加载年份...'
              : '正在使用扫表兼容模式加载年份...',
            switched: true,
            nativeTimedOut
          })
        },
        shouldCancel: params.shouldCancel
      })

      if (isCancelled()) {
        if (scanTicker) clearInterval(scanTicker)
        return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') }
      }
      if (years.length === 0) {
        years = await this.getAvailableYearsByEdgeScan(sessionIds, {
          onProgress: (items) => {
            latestYears = items
            scanElapsedMs = Date.now() - scanStartedAt
            emitProgress({
              years: items,
              strategy: 'hybrid',
              phase: 'scan',
              statusText: '扫表结果为空，正在执行游标兜底扫描...',
              switched: true,
              nativeTimedOut
            })
          },
          shouldCancel: params.shouldCancel
        })
      }
      if (scanTicker) {
        clearInterval(scanTicker)
        scanTicker = null
      }
      scanElapsedMs = Math.max(scanElapsedMs, Date.now() - scanStartedAt)

      if (isCancelled()) return { success: false, error: '已取消加载年份数据', meta: buildMeta('hybrid', '已取消加载年份数据') }

      this.setCachedAvailableYears(cacheKey, years)
      latestYears = years
      emitProgress({
        years,
        strategy: 'hybrid',
        phase: 'scan',
        statusText: '扫表兼容模式加载完成',
        switched: true,
        nativeTimedOut
      })
      return {
        success: true,
        data: years,
        meta: buildMeta('hybrid', '扫表兼容模式加载完成')
      }
    } catch (e) {
      return { success: false, error: String(e), meta: { strategy: 'hybrid', nativeElapsedMs: 0, scanElapsedMs: 0, totalElapsedMs: 0, switched: false, nativeTimedOut: false, statusText: '加载年度数据失败' } }
    }
  }

  async generateReportWithConfig(params: {
    year: number
    wxid: string
    dbPath: string
    decryptKey: string
    onProgress?: (status: string, progress: number) => void
  }): Promise<{ success: boolean; data?: AnnualReportData; error?: string }> {
    try {
      const { year, wxid, dbPath, decryptKey, onProgress } = params
      this.reportProgress('正在连接数据库...', 5, onProgress)
      const conn = await this.ensureConnectedWithConfig(dbPath, decryptKey, wxid)
      if (!conn.success || !conn.cleanedWxid || !conn.rawWxid) return { success: false, error: conn.error }

      const cleanedWxid = conn.cleanedWxid
      const rawWxid = conn.rawWxid
      const sessionIds = await this.getPrivateSessions(cleanedWxid)
      if (sessionIds.length === 0) {
        return { success: false, error: '未找到消息会话' }
      }

      this.reportProgress('加载会话列表...', 15, onProgress)

      const isAllTime = year <= 0
      const reportYear = isAllTime ? 0 : year
      const startTime = isAllTime ? 0 : Math.floor(new Date(year, 0, 1).getTime() / 1000)
      const endTime = isAllTime ? 0 : Math.floor(new Date(year, 11, 31, 23, 59, 59).getTime() / 1000)

      const now = new Date()
      // 全局统计始终使用自然年范围 (Jan 1st - Now/YearEnd)
      const actualStartTime = startTime
      const actualEndTime = endTime

      let totalMessages = 0
      const contactStats = new Map<string, { sent: number; received: number }>()
      const monthlyStats = new Map<string, Map<number, number>>()
      const dailyStats = new Map<string, number>()
      const dailyContactStats = new Map<string, Map<string, number>>()
      const heatmapData: number[][] = Array.from({ length: 7 }, () => Array(24).fill(0))
      const midnightStats = new Map<string, number>()
      let longestStreakSessionId = ''
      let longestStreakDays = 0
      let longestStreakStart: Date | null = null
      let longestStreakEnd: Date | null = null

      const conversationStarts = new Map<string, { initiated: number; received: number }>()
      const responseTimeStats = new Map<string, number[]>()
      const phraseCount = new Map<string, number>()
      const lastMessageTime = new Map<string, { time: number; isSent: boolean }>()

      const CONVERSATION_GAP = 3600

      this.reportProgress('统计会话消息...', 20, onProgress)
      const result = await wcdbService.getAnnualReportStats(sessionIds, actualStartTime, actualEndTime)
      if (!result.success || !result.data) {
        return { success: false, error: result.error ? `基础统计失败: ${result.error}` : '基础统计失败' }
      }

      const d = result.data
      totalMessages = d.total
      this.reportProgress('汇总基础统计...', 25, onProgress)

      const totalMessagesForProgress = totalMessages > 0 ? totalMessages : sessionIds.length
      let processedMessages = 0
      let lastProgressSent = 0
      let lastProgressAt = 0

      // 填充基础统计
      for (const [sid, stat] of Object.entries(d.sessions)) {
        const s = stat as any
        contactStats.set(sid, { sent: s.sent, received: s.received })

        const mMap = new Map<number, number>()
        for (const [m, c] of Object.entries(s.monthly || {})) {
          mMap.set(parseInt(m, 10), c as number)
        }
        monthlyStats.set(sid, mMap)
      }

      // 填充全局分布，并锁定峰值日期以减少逐日消息统计
      let peakDayKey = ''
      let peakDayCount = 0
      for (const [day, count] of Object.entries(d.daily)) {
        const c = count as number
        dailyStats.set(day, c)
        if (c > peakDayCount) {
          peakDayCount = c
          peakDayKey = day
        }
      }

      let useSqlExtras = false
      let responseStatsFromSql: Record<string, { avg?: number; fastest?: number; count?: number }> | null = null
      let topPhrasesFromSql: { phrase: string; count: number }[] | null = null
      let streakComputedInLoop = false

      let peakDayBegin = 0
      let peakDayEnd = 0
      if (peakDayKey) {
        const start = new Date(`${peakDayKey}T00:00:00`).getTime()
        if (!Number.isNaN(start)) {
          peakDayBegin = Math.floor(start / 1000)
          peakDayEnd = peakDayBegin + 24 * 3600 - 1
        }
      }

      this.reportProgress('加载扩展统计...', 30, onProgress)
      const extras = await wcdbService.getAnnualReportExtras(sessionIds, actualStartTime, actualEndTime, peakDayBegin, peakDayEnd)
      if (extras.success && extras.data) {
        this.reportProgress('加载扩展统计... (解析热力图)', 32, onProgress)
        const extrasData = extras.data as any
        const heatmap = extrasData.heatmap as number[][] | undefined
        if (Array.isArray(heatmap) && heatmap.length === 7) {
          for (let w = 0; w < 7; w++) {
            if (Array.isArray(heatmap[w])) {
              for (let h = 0; h < 24; h++) {
                heatmapData[w][h] = heatmap[w][h] || 0
              }
            }
          }
        }

        this.reportProgress('加载扩展统计... (解析夜聊统计)', 33, onProgress)
        const midnight = extrasData.midnight as Record<string, number> | undefined
        if (midnight) {
          for (const [sid, count] of Object.entries(midnight)) {
            midnightStats.set(sid, count as number)
          }
        }

        this.reportProgress('加载扩展统计... (解析对话发起)', 34, onProgress)
        const conversation = extrasData.conversation as Record<string, { initiated: number; received: number }> | undefined
        if (conversation) {
          for (const [sid, stats] of Object.entries(conversation)) {
            conversationStarts.set(sid, { initiated: stats.initiated || 0, received: stats.received || 0 })
          }
        }

        this.reportProgress('加载扩展统计... (解析响应速度)', 35, onProgress)
        responseStatsFromSql = extrasData.response || null

        this.reportProgress('加载扩展统计... (解析峰值日)', 36, onProgress)
        const peakDayCounts = extrasData.peakDay as Record<string, number> | undefined
        if (peakDayKey && peakDayCounts) {
          const dayMap = new Map<string, number>()
          for (const [sid, count] of Object.entries(peakDayCounts)) {
            dayMap.set(sid, count as number)
          }
          if (dayMap.size > 0) {
            dailyContactStats.set(peakDayKey, dayMap)
          }
        }

        this.reportProgress('加载扩展统计... (解析常用语)', 37, onProgress)
        const sqlPhrases = extrasData.topPhrases as { phrase: string; count: number }[] | undefined
        if (Array.isArray(sqlPhrases) && sqlPhrases.length > 0) {
          topPhrasesFromSql = sqlPhrases
        }

        const streak = extrasData.streak as { sessionId?: string; days?: number; startDate?: string; endDate?: string } | undefined
        if (streak && streak.sessionId && streak.days && streak.days > 0) {
          longestStreakSessionId = streak.sessionId
          longestStreakDays = streak.days
          longestStreakStart = streak.startDate ? new Date(`${streak.startDate}T00:00:00`) : null
          longestStreakEnd = streak.endDate ? new Date(`${streak.endDate}T00:00:00`) : null
          if (longestStreakStart && !Number.isNaN(longestStreakStart.getTime()) &&
            longestStreakEnd && !Number.isNaN(longestStreakEnd.getTime())) {
            streakComputedInLoop = true
          }
        }

        useSqlExtras = true
        this.reportProgress('加载扩展统计... (完成)', 40, onProgress)
      } else if (!extras.success) {
        const reason = extras.error ? ` (${extras.error})` : ''
        this.reportProgress(`扩展统计失败，转入完整分析...${reason}`, 30, onProgress)
      }

      if (!useSqlExtras) {
        // 注意：原生层目前未返回交叉维度 heatmapData[weekday][hour]，
        // 这里的 heatmapData 仍然需要通过下面的遍历来精确填充。

        // 考虑到 Annual Report 需要一些复杂的序列特征（响应速度、对话发起）和文本特征（常用语），
        // 我们仍然保留一次轻量级循环，但因为有了原生统计，我们可以分步进行，或者如果数据量极大则跳过某些步骤。
        // 为保持功能完整，我们进行深度集成的轻量遍历：
        for (let i = 0; i < sessionIds.length; i++) {
          const sessionId = sessionIds[i]
          const cursor = await wcdbService.openMessageCursorLite(sessionId, 1000, true, actualStartTime, actualEndTime)
          if (!cursor.success || !cursor.cursor) continue

          let lastDayIndex: number | null = null
          let currentStreak = 0
          let currentStart: Date | null = null
          let maxStreak = 0
          let maxStart: Date | null = null
          let maxEnd: Date | null = null

          try {
            let hasMore = true
            while (hasMore) {
              const batch = await wcdbService.fetchMessageBatch(cursor.cursor)
              if (!batch.success || !batch.rows) break

              for (const row of batch.rows) {
                const createTime = parseInt(row.create_time || '0', 10)
                if (!createTime) continue

                const isSendRaw = row.computed_is_send ?? row.is_send ?? '0'
                let isSent = parseInt(isSendRaw, 10) === 1
                const localType = parseInt(row.local_type || row.type || '1', 10)

                // 兼容逻辑
                if (isSendRaw === undefined || isSendRaw === null || isSendRaw === '0') {
                  const sender = String(row.sender_username || row.sender || row.talker || '').toLowerCase()
                  if (sender) {
                    const rawLower = rawWxid.toLowerCase()
                    const cleanedLower = cleanedWxid.toLowerCase()
                    if (sender === rawLower || sender === cleanedLower ||
                      rawLower.startsWith(sender + '_') || cleanedLower.startsWith(sender + '_')) {
                      isSent = true
                    }
                  }
                }

                // 响应速度 & 对话发起
                if (!conversationStarts.has(sessionId)) {
                  conversationStarts.set(sessionId, { initiated: 0, received: 0 })
                }
                const convStats = conversationStarts.get(sessionId)!
                const lastMsg = lastMessageTime.get(sessionId)
                if (!lastMsg || (createTime - lastMsg.time) > CONVERSATION_GAP) {
                  if (isSent) convStats.initiated++
                  else convStats.received++
                } else if (lastMsg.isSent !== isSent) {
                  if (isSent && !lastMsg.isSent) {
                    const responseTime = createTime - lastMsg.time
                    if (responseTime > 0 && responseTime < 86400) {
                      if (!responseTimeStats.has(sessionId)) responseTimeStats.set(sessionId, [])
                      responseTimeStats.get(sessionId)!.push(responseTime)
                    }
                  }
                }
                lastMessageTime.set(sessionId, { time: createTime, isSent })

                // 常用语
                if ((localType === 1 || localType === 244813135921) && isSent) {
                  const content = this.decodeMessageContent(row.message_content, row.compress_content)
                  const text = String(content).trim()
                  if (text.length >= 2 && text.length <= 20 &&
                    !text.includes('http') && !text.includes('<') &&
                    !text.startsWith('[') && !text.startsWith('<?xml')) {
                    phraseCount.set(text, (phraseCount.get(text) || 0) + 1)
                  }
                }

                // 交叉维度补全
                const dt = new Date(createTime * 1000)
                const weekdayIndex = dt.getDay() === 0 ? 6 : dt.getDay() - 1
                heatmapData[weekdayIndex][dt.getHours()]++

                const dayDate = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate())
                const dayIndex = Math.floor(dayDate.getTime() / 86400000)
                if (lastDayIndex === null || dayIndex !== lastDayIndex) {
                  if (lastDayIndex !== null && dayIndex - lastDayIndex === 1) {
                    currentStreak++
                  } else {
                    currentStreak = 1
                    currentStart = dayDate
                  }
                  if (currentStreak > maxStreak) {
                    maxStreak = currentStreak
                    maxStart = currentStart
                    maxEnd = dayDate
                  }
                  lastDayIndex = dayIndex
                }

                if (dt.getHours() >= 0 && dt.getHours() < 6) {
                  midnightStats.set(sessionId, (midnightStats.get(sessionId) || 0) + 1)
                }

                if (peakDayKey) {
                  const dayKey = `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`
                  if (dayKey === peakDayKey) {
                    if (!dailyContactStats.has(dayKey)) dailyContactStats.set(dayKey, new Map())
                    const dayContactMap = dailyContactStats.get(dayKey)!
                    dayContactMap.set(sessionId, (dayContactMap.get(sessionId) || 0) + 1)
                  }
                }

                if (totalMessagesForProgress > 0) {
                  processedMessages++
                }
              }
              hasMore = batch.hasMore === true

              const now = Date.now()
              if (now - lastProgressAt > 200) {
                let progress: number
                if (totalMessagesForProgress > 0) {
                  const ratio = Math.min(1, processedMessages / totalMessagesForProgress)
                  progress = 30 + Math.floor(ratio * 50)
                } else {
                  const ratio = Math.min(1, (i + 1) / sessionIds.length)
                  progress = 30 + Math.floor(ratio * 50)
                }
                if (progress > lastProgressSent) {
                  lastProgressSent = progress
                  lastProgressAt = now
                  let label = `${i + 1}/${sessionIds.length}`
                  if (totalMessagesForProgress > 0) {
                    const done = Math.min(processedMessages, totalMessagesForProgress)
                    label = `${done}/${totalMessagesForProgress}`
                  }
                  this.reportProgress(`分析聊天记录... (${label})`, progress, onProgress)
                }
              }
              await new Promise(resolve => setImmediate(resolve))
            }
          } finally {
            await wcdbService.closeMessageCursor(cursor.cursor)
          }

          if (maxStreak > longestStreakDays) {
            longestStreakDays = maxStreak
            longestStreakSessionId = sessionId
            longestStreakStart = maxStart
            longestStreakEnd = maxEnd
          }
        }
        streakComputedInLoop = true
      }

      if (!streakComputedInLoop) {
        this.reportProgress('计算连续聊天...', 45, onProgress)
        const streakResult = await this.computeLongestStreak(sessionIds, actualStartTime, actualEndTime, onProgress, 45, 75)
        if (streakResult.days > longestStreakDays) {
          longestStreakDays = streakResult.days
          longestStreakSessionId = streakResult.sessionId
          longestStreakStart = streakResult.start
          longestStreakEnd = streakResult.end
        }
      }

      // 获取朋友圈统计
      this.reportProgress('分析朋友圈数据...', 75, onProgress)
      let snsStatsResult: {
        totalPosts: number
        typeCounts?: Record<string, number>
        topLikers: { username: string; displayName: string; avatarUrl?: string; count: number }[]
        topLiked: { username: string; displayName: string; avatarUrl?: string; count: number }[]
      } | undefined

      const snsBeginTime = isAllTime ? 0 : actualStartTime
      const snsEndTime = isAllTime ? Math.floor(Date.now() / 1000) : actualEndTime
      const snsStats = await wcdbService.getSnsAnnualStats(snsBeginTime, snsEndTime)

      if (snsStats.success && snsStats.data) {
        const d = snsStats.data
        const usersToFetch = new Set<string>()
        d.topLikers?.forEach((u: any) => usersToFetch.add(u.username))
        d.topLiked?.forEach((u: any) => usersToFetch.add(u.username))

        const snsUserIds = Array.from(usersToFetch)
        const [snsDisplayNames, snsAvatarUrls] = await Promise.all([
          wcdbService.getDisplayNames(snsUserIds),
          wcdbService.getAvatarUrls(snsUserIds)
        ])

        const getSnsUserInfo = (username: string) => ({
          displayName: snsDisplayNames.success && snsDisplayNames.map ? (snsDisplayNames.map[username] || username) : username,
          avatarUrl: snsAvatarUrls.success && snsAvatarUrls.map ? snsAvatarUrls.map[username] : undefined
        })

        snsStatsResult = {
          totalPosts: d.totalPosts || 0,
          typeCounts: d.typeCounts,
          topLikers: (d.topLikers || []).map((u: any) => ({ ...u, ...getSnsUserInfo(u.username) })),
          topLiked: (d.topLiked || []).map((u: any) => ({ ...u, ...getSnsUserInfo(u.username) }))
        }
      }

      // ALL YEARS 兼容：部分底层实现 begin/end 为 0 时会返回 0，兜底使用导出统计总数。
      if (isAllTime && (!snsStatsResult || Number(snsStatsResult.totalPosts || 0) <= 0)) {
        const snsExportStats = await wcdbService.getSnsExportStats(cleanedWxid || rawWxid)
        if (snsExportStats.success && snsExportStats.data) {
          const fallbackTotalPosts = Math.max(0, Number(snsExportStats.data.totalPosts || 0))
          snsStatsResult = {
            totalPosts: fallbackTotalPosts,
            typeCounts: snsStatsResult?.typeCounts,
            topLikers: snsStatsResult?.topLikers || [],
            topLiked: snsStatsResult?.topLiked || []
          }
        }
      }

      this.reportProgress('整理联系人信息...', 85, onProgress)

      const contactIds = Array.from(contactStats.keys())
      const [displayNames, avatarUrls] = await Promise.all([
        wcdbService.getDisplayNames(contactIds),
        wcdbService.getAvatarUrls(contactIds)
      ])

      const contactInfoMap = new Map<string, { displayName: string; avatarUrl?: string }>()
      for (const sessionId of contactIds) {
        contactInfoMap.set(sessionId, {
          displayName: displayNames.success && displayNames.map ? (displayNames.map[sessionId] || sessionId) : sessionId,
          avatarUrl: avatarUrls.success && avatarUrls.map ? avatarUrls.map[sessionId] : undefined
        })
      }

      const selfAvatarResult = await wcdbService.getAvatarUrls([rawWxid, cleanedWxid])
      const selfAvatarUrl = selfAvatarResult.success && selfAvatarResult.map
        ? (selfAvatarResult.map[rawWxid] || selfAvatarResult.map[cleanedWxid])
        : undefined

      const coreFriends: TopContact[] = Array.from(contactStats.entries())
        .map(([sessionId, stats]) => {
          const info = contactInfoMap.get(sessionId)
          return {
            username: sessionId,
            displayName: info?.displayName || sessionId,
            avatarUrl: info?.avatarUrl,
            messageCount: stats.sent + stats.received,
            sentCount: stats.sent,
            receivedCount: stats.received
          }
        })
        .sort((a, b) => b.messageCount - a.messageCount)
        .slice(0, 3)

      const monthlyTopFriends: MonthlyTopFriend[] = []
      for (let month = 1; month <= 12; month++) {
        let maxCount = 0
        let topSessionId = ''
        for (const [sessionId, monthMap] of monthlyStats.entries()) {
          const count = monthMap.get(month) || 0
          if (count > maxCount) {
            maxCount = count
            topSessionId = sessionId
          }
        }
        const info = contactInfoMap.get(topSessionId)
        monthlyTopFriends.push({
          month,
          displayName: info?.displayName || (topSessionId ? topSessionId : '暂无'),
          avatarUrl: info?.avatarUrl,
          messageCount: maxCount
        })
      }

      let peakDay: ChatPeakDay | null = null
      let maxDayCount = 0
      for (const [day, count] of dailyStats.entries()) {
        if (count > maxDayCount) {
          maxDayCount = count
          const dayContactMap = dailyContactStats.get(day)
          let topFriend = ''
          let topFriendCount = 0
          if (dayContactMap) {
            for (const [sessionId, c] of dayContactMap.entries()) {
              if (c > topFriendCount) {
                topFriendCount = c
                topFriend = contactInfoMap.get(sessionId)?.displayName || sessionId
              }
            }
          }
          peakDay = { date: day, messageCount: count, topFriend, topFriendCount }
        }
      }

      let midnightKing: AnnualReportData['midnightKing'] = null
      const totalMidnight = Array.from(midnightStats.values()).reduce((a, b) => a + b, 0)
      if (totalMidnight > 0) {
        let maxMidnight = 0
        let midnightSessionId = ''
        for (const [sessionId, count] of midnightStats.entries()) {
          if (count > maxMidnight) {
            maxMidnight = count
            midnightSessionId = sessionId
          }
        }
        const info = contactInfoMap.get(midnightSessionId)
        midnightKing = {
          displayName: info?.displayName || midnightSessionId,
          count: maxMidnight,
          percentage: Math.round((maxMidnight / totalMidnight) * 1000) / 10
        }
      }

      let longestStreak: AnnualReportData['longestStreak'] = null
      if (longestStreakSessionId && longestStreakDays > 0 && longestStreakStart && longestStreakEnd) {
        const info = contactInfoMap.get(longestStreakSessionId)
        longestStreak = {
          friendName: info?.displayName || longestStreakSessionId,
          days: longestStreakDays,
          startDate: this.formatDateYmd(longestStreakStart),
          endDate: this.formatDateYmd(longestStreakEnd)
        }
      }

      let mutualFriend: AnnualReportData['mutualFriend'] = null
      let bestRatioDiff = Infinity
      for (const [sessionId, stats] of contactStats.entries()) {
        if (stats.sent >= 50 && stats.received >= 50) {
          const ratio = stats.sent / stats.received
          const ratioDiff = Math.abs(ratio - 1)
          if (ratioDiff < bestRatioDiff) {
            bestRatioDiff = ratioDiff
            const info = contactInfoMap.get(sessionId)
            mutualFriend = {
              displayName: info?.displayName || sessionId,
              avatarUrl: info?.avatarUrl,
              sentCount: stats.sent,
              receivedCount: stats.received,
              ratio: Math.round(ratio * 100) / 100
            }
          }
        }
      }

      let socialInitiative: AnnualReportData['socialInitiative'] = null
      let totalInitiated = 0
      let totalReceived = 0
      let topInitiatedSessionId = ''
      let topInitiatedCount = 0
      for (const [sessionId, stats] of conversationStarts.entries()) {
        totalInitiated += stats.initiated
        totalReceived += stats.received
        if (stats.initiated > topInitiatedCount) {
          topInitiatedCount = stats.initiated
          topInitiatedSessionId = sessionId
        }
      }
      const totalConversations = totalInitiated + totalReceived
      if (totalConversations > 0) {
        const topInitiatedInfo = topInitiatedSessionId ? contactInfoMap.get(topInitiatedSessionId) : null
        socialInitiative = {
          initiatedChats: totalInitiated,
          receivedChats: totalReceived,
          initiativeRate: Math.round((totalInitiated / totalConversations) * 1000) / 10,
          topInitiatedFriend: topInitiatedCount > 0
            ? (topInitiatedInfo?.displayName || topInitiatedSessionId)
            : undefined,
          topInitiatedCount: topInitiatedCount > 0 ? topInitiatedCount : undefined
        }
      }

      this.reportProgress('生成报告...', 95, onProgress)

      let responseSpeed: AnnualReportData['responseSpeed'] = null
      if (responseStatsFromSql && Object.keys(responseStatsFromSql).length > 0) {
        let totalSum = 0
        let totalCount = 0
        let fastestFriendId = ''
        let fastestAvgTime = Infinity
        for (const [sessionId, stats] of Object.entries(responseStatsFromSql)) {
          const count = stats.count || 0
          const avg = stats.avg || 0
          if (count <= 0 || avg <= 0) continue
          totalSum += avg * count
          totalCount += count
          if (avg < fastestAvgTime) {
            fastestAvgTime = avg
            fastestFriendId = sessionId
          }
        }
        if (totalCount > 0) {
          const avgResponseTime = totalSum / totalCount
          const fastestInfo = contactInfoMap.get(fastestFriendId)
          responseSpeed = {
            avgResponseTime: Math.round(avgResponseTime),
            fastestFriend: fastestInfo?.displayName || fastestFriendId,
            fastestTime: Math.round(fastestAvgTime)
          }
        }
      } else {
        const allResponseTimes: number[] = []
        let fastestFriendId = ''
        let fastestAvgTime = Infinity
        for (const [sessionId, times] of responseTimeStats.entries()) {
          if (times.length >= 10) {
            allResponseTimes.push(...times)
            const avgTime = times.reduce((a, b) => a + b, 0) / times.length
            if (avgTime < fastestAvgTime) {
              fastestAvgTime = avgTime
              fastestFriendId = sessionId
            }
          }
        }
        if (allResponseTimes.length > 0) {
          const avgResponseTime = allResponseTimes.reduce((a, b) => a + b, 0) / allResponseTimes.length
          const fastestInfo = contactInfoMap.get(fastestFriendId)
          responseSpeed = {
            avgResponseTime: Math.round(avgResponseTime),
            fastestFriend: fastestInfo?.displayName || fastestFriendId,
            fastestTime: Math.round(fastestAvgTime)
          }
        }
      }

      const topPhrases = topPhrasesFromSql && topPhrasesFromSql.length > 0
        ? topPhrasesFromSql
        : Array.from(phraseCount.entries())
          .filter(([_, count]) => count >= 2)
          .sort((a, b) => b[1] - a[1])
          .slice(0, 32)
          .map(([phrase, count]) => ({ phrase, count }))

      // 曾经的好朋友 (Once Best Friend / Lost Friend)
      let lostFriend: AnnualReportData['lostFriend'] = null
      let maxEarlyCount = 80  // 最低门槛
      let bestEarlyCount = 0
      let bestLateCount = 0
      let bestSid = ''
      let bestPeriodDesc = ''

      const currentMonthIndex = new Date().getMonth() + 1 // 1-12

      const currentYearNum = now.getFullYear()

      if (isAllTime) {
        const days = Object.keys(d.daily).sort()
        if (days.length >= 2) {
          const firstDay = Math.floor(new Date(days[0]).getTime() / 1000)
          const lastDay = Math.floor(new Date(days[days.length - 1]).getTime() / 1000)
          const midPoint = Math.floor((firstDay + lastDay) / 2)

          this.reportProgress('分析历史趋势 (1/2)...', 86, onProgress)
          const earlyRes = await wcdbService.getAggregateStats(sessionIds, 0, midPoint)
          this.reportProgress('分析历史趋势 (2/2)...', 88, onProgress)
          const lateRes = await wcdbService.getAggregateStats(sessionIds, midPoint, 0)

          if (earlyRes.success && lateRes.success && earlyRes.data) {
            const earlyData = earlyRes.data.sessions || {}
            const lateData = (lateRes.data?.sessions) || {}
            for (const sid of sessionIds) {
              const e = earlyData[sid] || { sent: 0, received: 0 }
              const l = lateData[sid] || { sent: 0, received: 0 }
              const early = (e.sent || 0) + (e.received || 0)
              const late = (l.sent || 0) + (l.received || 0)
              if (early > 100 && early > late * 5) {
                // 选择前期消息量最多的
                if (early > maxEarlyCount) {
                  maxEarlyCount = early
                  bestEarlyCount = early
                  bestLateCount = late
                  bestSid = sid
                  bestPeriodDesc = '这段时间以来'
                }
              }
            }
          }
        }
      } else if (year === currentYearNum) {
        // 当前年份：独立获取过去12个月的滚动数据
        this.reportProgress('分析近期好友趋势...', 86, onProgress)
        // 往前数12个月的起点、中点、终点
        const rollingStart = Math.floor(new Date(now.getFullYear(), now.getMonth() - 11, 1).getTime() / 1000)
        const rollingMid = Math.floor(new Date(now.getFullYear(), now.getMonth() - 5, 1).getTime() / 1000)
        const rollingEnd = Math.floor(now.getTime() / 1000)

        const earlyRes = await wcdbService.getAggregateStats(sessionIds, rollingStart, rollingMid - 1)
        const lateRes = await wcdbService.getAggregateStats(sessionIds, rollingMid, rollingEnd)

        if (earlyRes.success && lateRes.success && earlyRes.data) {
          const earlyData = earlyRes.data.sessions || {}
          const lateData = lateRes.data?.sessions || {}
          for (const sid of sessionIds) {
            const e = earlyData[sid] || { sent: 0, received: 0 }
            const l = lateData[sid] || { sent: 0, received: 0 }
            const early = (e.sent || 0) + (e.received || 0)
            const late = (l.sent || 0) + (l.received || 0)
            if (early > 80 && early > late * 5) {
              // 选择前期消息量最多的
              if (early > maxEarlyCount) {
                maxEarlyCount = early
                bestEarlyCount = early
                bestLateCount = late
                bestSid = sid
                bestPeriodDesc = '去年的这个时候'
              }
            }
          }
        }
      } else {
        // 指定完整年份 (1-6 vs 7-12)
        for (const [sid, stat] of Object.entries(d.sessions)) {
          const s = stat as any
          const mWeights = s.monthly || {}
          let early = 0
          let late = 0
          for (let m = 1; m <= 6; m++) early += mWeights[m] || 0
          for (let m = 7; m <= 12; m++) late += mWeights[m] || 0

          if (early > 80 && early > late * 5) {
            // 选择前期消息量最多的
            if (early > maxEarlyCount) {
              maxEarlyCount = early
              bestEarlyCount = early
              bestLateCount = late
              bestSid = sid
              bestPeriodDesc = `${year}年上半年`
            }
          }
        }
      }

      if (bestSid) {
        let info = contactInfoMap.get(bestSid)
        // 如果 contactInfoMap 中没有该联系人，则单独获取
        if (!info) {
          const [displayNameRes, avatarUrlRes] = await Promise.all([
            wcdbService.getDisplayNames([bestSid]),
            wcdbService.getAvatarUrls([bestSid])
          ])
          info = {
            displayName: displayNameRes.success && displayNameRes.map ? (displayNameRes.map[bestSid] || bestSid) : bestSid,
            avatarUrl: avatarUrlRes.success && avatarUrlRes.map ? avatarUrlRes.map[bestSid] : undefined
          }
        }
        lostFriend = {
          username: bestSid,
          displayName: info?.displayName || bestSid,
          avatarUrl: info?.avatarUrl,
          earlyCount: bestEarlyCount,
          lateCount: bestLateCount,
          periodDesc: bestPeriodDesc
        }
      }

      const reportData: AnnualReportData = {
        year: reportYear,
        totalMessages,
        totalFriends: contactStats.size,
        coreFriends,
        monthlyTopFriends,
        peakDay,
        longestStreak,
        activityHeatmap: { data: heatmapData },
        midnightKing,
        selfAvatarUrl,
        mutualFriend,
        socialInitiative,
        responseSpeed,
        topPhrases,
        snsStats: snsStatsResult,
        lostFriend
      }

      return { success: true, data: reportData }
    } catch (e) {
      return { success: false, error: String(e) }
    }
  }
}

export const annualReportService = new AnnualReportService()
