// Transcript Engine - Parses Claude Code JSONL transcripts
// Module-level state pattern (like terminal.ts, torch-handler.ts)

import { existsSync, readFileSync, statSync, readdirSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
import { WORKING_DIR } from '../config'

// ── Types ──

export interface TranscriptAnalysis {
  sessionId: string
  model: string
  version: string
  gitBranch: string
  cwd: string
  startTime: string
  endTime: string
  duration: number
  messages: TranscriptMessage[]
  tokens: {
    totalInput: number
    totalOutput: number
    totalCacheRead: number
    totalCacheCreation: number
    byTurn: TurnTokens[]
  }
  tools: {
    summary: Record<string, number>
    calls: ToolCall[]
  }
  filesModified: string[]
  subagents: SubagentInfo[]
  summaries: string[]
  stats: {
    messageCount: number
    userMessageCount: number
    assistantMessageCount: number
    toolCallCount: number
    thinkingBlocks: number
    errors: number
  }
  lastStopReason: string
}

export interface TranscriptMessage {
  uuid: string
  role: 'user' | 'assistant'
  content: string
  timestamp: string
  isMeta: boolean
  tokens?: { input: number; output: number }
  toolCalls?: string[]
  hasThinking: boolean
}

export interface ToolCall {
  name: string
  input: unknown
  output?: string
  timestamp: string
  isError: boolean
}

export interface TurnTokens {
  turnIndex: number
  input: number
  output: number
  cacheRead: number
  cacheCreation: number
  model: string
}

export interface SubagentInfo {
  agentId: string
  prompt: string
  timestamp: string
}

export interface SessionInfo {
  id: string
  startTime: string
  messageCount: number
  model: string
}

export interface ClaudeStats {
  today: {
    date: string
    messageCount: number
    sessionCount: number
    toolCallCount: number
    tokensByModel: Record<string, number>
  } | null
  modelUsage: Record<string, {
    inputTokens: number
    outputTokens: number
    cacheReadInputTokens: number
    cacheCreationInputTokens: number
    costUSD: number
  }>
  totalSessions: number
  totalMessages: number
  firstSessionDate: string
}

export interface ClaudeUsage {
  subscription: { type: string; tier: string; label: string; multiplier: number }
  today: { messages: number; outputTokens: number; sessions: number }
  daily: { used: number; limit: number; percent: number }
  weekly: { used: number; limit: number; percent: number }
  status: 'normal' | 'elevated' | 'extended' | 'limit_approaching'
}

const TIER_LIMITS: Record<string, { windowMessages: number; dailyEstimate: number; weeklyEstimate: number }> = {
  pro:      { windowMessages: 45,  dailyEstimate: 500,   weeklyEstimate: 3500 },
  max_5x:   { windowMessages: 225, dailyEstimate: 2500,  weeklyEstimate: 17500 },
  max_20x:  { windowMessages: 900, dailyEstimate: 10000, weeklyEstimate: 70000 },
}

function parseTier(rateLimitTier: string): { key: string; label: string; multiplier: number } {
  const match = rateLimitTier.match(/max_(\d+)x/)
  if (match) {
    const n = parseInt(match[1])
    return { key: `max_${n}x`, label: `Max ${n}x`, multiplier: n }
  }
  return { key: 'pro', label: 'Pro', multiplier: 1 }
}

// ── Module-level cache ──

const cache = new Map<string, {
  analysis: TranscriptAnalysis
  lastModified: number
}>()

// ── Active session tracking (for real-time chat) ──

const activeAgentSessions = new Map<string, {
  sessionId: string
  transcriptPath: string
}>()

// Byte offset per session (how far we've read)
const readOffsets = new Map<string, number>()

// ── Project hash ──

function getProjectHash(): string {
  // C:\Users\jodar\agent-ui → C--Users-jodar-agent-ui
  return WORKING_DIR.replace(/[\\/]/g, '-').replace(/:/g, '-')
}

function getProjectDir(): string {
  return join(homedir(), '.claude', 'projects', getProjectHash())
}

// ── Path resolution ──

function resolveTranscriptPath(sessionId?: string): string | null {
  const projectDir = getProjectDir()
  if (!existsSync(projectDir)) return null

  if (sessionId && sessionId !== 'latest') {
    const filePath = join(projectDir, `${sessionId}.jsonl`)
    return existsSync(filePath) ? filePath : null
  }

  // Find most recent by mtime
  try {
    const files = readdirSync(projectDir)
      .filter(f => f.endsWith('.jsonl'))
      .map(f => {
        const fullPath = join(projectDir, f)
        return { name: f, path: fullPath, mtime: statSync(fullPath).mtimeMs }
      })
      .sort((a, b) => b.mtime - a.mtime)

    return files.length > 0 ? files[0].path : null
  } catch {
    return null
  }
}

function sessionIdFromPath(filePath: string): string {
  const basename = filePath.split(/[\\/]/).pop() || ''
  return basename.replace('.jsonl', '')
}

// ── Helpers ──

function truncate(str: string, maxLen: number): string {
  if (!str || str.length <= maxLen) return str || ''
  return str.slice(0, maxLen) + '...'
}

function extractText(content: any): string {
  if (typeof content === 'string') {
    return content.replace(/<[^>]+>/g, '').trim()
  }
  if (Array.isArray(content)) {
    return content
      .filter((c: any) => c.type === 'text')
      .map((c: any) => c.text || '')
      .join('\n')
      .replace(/<[^>]+>/g, '')
      .trim()
  }
  return ''
}

// ── JSONL parsing ──

interface ParsedLine {
  type: string
  data: any
}

function parseTranscriptFile(filePath: string): ParsedLine[] {
  const content = readFileSync(filePath, 'utf8')
  const rawLines = content.trim().split('\n')
  const lines: ParsedLine[] = []

  for (const line of rawLines) {
    if (!line.trim()) continue
    try {
      const obj = JSON.parse(line)
      lines.push({ type: obj.type, data: obj })
    } catch {
      // Skip unparseable lines
    }
  }

  return lines
}

// ── Build analysis from parsed lines ──

function buildAnalysis(lines: ParsedLine[], fileSessionId: string): TranscriptAnalysis {
  let sessionId = fileSessionId
  let model = ''
  let version = ''
  let gitBranch = ''
  let cwd = ''
  let startTime = ''
  let endTime = ''

  const messages: TranscriptMessage[] = []
  const toolCalls: ToolCall[] = []
  const filesModified = new Set<string>()
  const subagents: SubagentInfo[] = []
  const summaries: string[] = []
  const turnTokens: TurnTokens[] = []

  let totalInput = 0
  let totalOutput = 0
  let totalCacheRead = 0
  let totalCacheCreation = 0
  let thinkingBlocks = 0
  let errors = 0

  // Track assistant message chunks by message.id (streaming chunks share the same id)
  const assistantChunks = new Map<string, {
    uuid: string
    timestamp: string
    model: string
    textParts: string[]
    toolNames: string[]
    hasThinking: boolean
    usage: any
    stopReason: string
    pendingToolCalls: { name: string; input: any; id: string; timestamp: string }[]
  }>()

  // Track tool results by tool_use_id
  const toolResults = new Map<string, { content: string; isError: boolean }>()

  // ── First pass: collect all data ──
  for (const { type, data } of lines) {
    // Extract metadata from first message that has it
    if (data.sessionId && !sessionId) sessionId = data.sessionId
    if (data.version && !version) version = data.version
    if (data.gitBranch && !gitBranch) gitBranch = data.gitBranch
    if (data.cwd && !cwd) cwd = data.cwd

    // Track time bounds
    if (data.timestamp) {
      if (!startTime || data.timestamp < startTime) startTime = data.timestamp
      if (!endTime || data.timestamp > endTime) endTime = data.timestamp
    }

    switch (type) {
      case 'user': {
        const msg = data.message
        if (!msg) break

        // Collect tool results (user messages contain tool_result blocks)
        if (Array.isArray(msg.content)) {
          for (const block of msg.content) {
            if (block.type === 'tool_result') {
              const resultText = typeof block.content === 'string'
                ? block.content
                : Array.isArray(block.content)
                  ? block.content.map((c: any) => c.text || '').join('\n')
                  : ''
              toolResults.set(block.tool_use_id, {
                content: truncate(resultText, 300),
                isError: !!block.is_error
              })
              if (block.is_error) errors++
            }
          }
        }

        // Add as user message (skip meta, skip tool-result-only messages)
        const isMeta = !!data.isMeta
        const text = extractText(msg.content)
        const hasToolResult = Array.isArray(msg.content) &&
          msg.content.some((c: any) => c.type === 'tool_result')

        if (text && !hasToolResult) {
          messages.push({
            uuid: data.uuid || '',
            role: 'user',
            content: text,
            timestamp: data.timestamp || '',
            isMeta,
            hasThinking: false
          })
        }
        break
      }

      case 'assistant': {
        const msg = data.message
        if (!msg || msg.role !== 'assistant') break

        const msgId = msg.id || data.uuid
        if (!model && msg.model) model = msg.model

        let chunk = assistantChunks.get(msgId)
        if (!chunk) {
          chunk = {
            uuid: data.uuid || '',
            timestamp: data.timestamp || '',
            model: msg.model || '',
            textParts: [],
            toolNames: [],
            hasThinking: false,
            usage: null,
            stopReason: '',
            pendingToolCalls: []
          }
          assistantChunks.set(msgId, chunk)
        }

        // Take latest usage (streaming chunks repeat usage, last is most accurate)
        if (msg.usage) chunk.usage = msg.usage
        if (msg.stop_reason) chunk.stopReason = msg.stop_reason

        // Process content blocks (each JSONL line typically has one block)
        if (Array.isArray(msg.content)) {
          for (const block of msg.content) {
            if (block.type === 'text' && block.text?.trim()) {
              chunk.textParts.push(block.text)
            } else if (block.type === 'thinking') {
              chunk.hasThinking = true
              thinkingBlocks++
            } else if (block.type === 'tool_use') {
              chunk.toolNames.push(block.name)
              chunk.pendingToolCalls.push({
                name: block.name,
                input: block.input,
                id: block.id,
                timestamp: data.timestamp || ''
              })
            }
          }
        }
        break
      }

      case 'progress': {
        if (data.data?.type === 'agent_progress' && data.data.agentId) {
          const existing = subagents.find(s => s.agentId === data.data.agentId)
          if (!existing) {
            subagents.push({
              agentId: data.data.agentId,
              prompt: truncate(data.data.prompt || '', 200),
              timestamp: data.timestamp || ''
            })
          }
        }
        break
      }

      case 'file-history-snapshot': {
        const backups = data.snapshot?.trackedFileBackups
        if (backups && typeof backups === 'object') {
          for (const filePath of Object.keys(backups)) {
            filesModified.add(filePath)
          }
        }
        break
      }

      case 'summary': {
        const summaryText = data.summary || data.message?.content
        if (summaryText) {
          summaries.push(truncate(
            typeof summaryText === 'string' ? summaryText : JSON.stringify(summaryText),
            1000
          ))
        }
        break
      }
    }
  }

  // ── Second pass: assemble assistant messages and finalize tool calls ──
  let turnIndex = 0
  let lastStopReason = ''
  for (const [, chunk] of assistantChunks) {
    if (chunk.stopReason) lastStopReason = chunk.stopReason
    const text = chunk.textParts.join('\n').trim()

    if (text || chunk.toolNames.length > 0) {
      const msgTokens = chunk.usage
        ? { input: chunk.usage.input_tokens || 0, output: chunk.usage.output_tokens || 0 }
        : undefined

      messages.push({
        uuid: chunk.uuid,
        role: 'assistant',
        content: text || `[Tool calls: ${chunk.toolNames.join(', ')}]`,
        timestamp: chunk.timestamp,
        isMeta: false,
        tokens: msgTokens,
        toolCalls: chunk.toolNames.length > 0 ? chunk.toolNames : undefined,
        hasThinking: chunk.hasThinking
      })
    }

    // Finalize tool calls with results
    for (const tc of chunk.pendingToolCalls) {
      const result = toolResults.get(tc.id)
      const inputStr = typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input)
      toolCalls.push({
        name: tc.name,
        input: truncate(inputStr, 500),
        output: result?.content,
        timestamp: tc.timestamp,
        isError: result?.isError || false
      })
    }

    // Token tracking per turn
    if (chunk.usage) {
      const u = chunk.usage
      const input = u.input_tokens || 0
      const output = u.output_tokens || 0
      const cacheRead = u.cache_read_input_tokens || 0
      const cacheCreation = u.cache_creation_input_tokens || 0

      totalInput += input
      totalOutput += output
      totalCacheRead += cacheRead
      totalCacheCreation += cacheCreation

      turnTokens.push({
        turnIndex: turnIndex++,
        input,
        output,
        cacheRead,
        cacheCreation,
        model: chunk.model
      })
    }
  }

  // Sort messages chronologically
  messages.sort((a, b) => a.timestamp.localeCompare(b.timestamp))

  // Build tool summary
  const toolSummary: Record<string, number> = {}
  for (const tc of toolCalls) {
    toolSummary[tc.name] = (toolSummary[tc.name] || 0) + 1
  }

  // Extract files from Edit/Write tool calls
  for (const tc of toolCalls) {
    if (['Edit', 'Write', 'NotebookEdit'].includes(tc.name) && tc.input) {
      try {
        const input = typeof tc.input === 'string' ? JSON.parse(tc.input) : tc.input
        if (input.file_path) filesModified.add(input.file_path)
        if (input.notebook_path) filesModified.add(input.notebook_path)
      } catch { /* skip */ }
    }
  }

  const duration = startTime && endTime
    ? new Date(endTime).getTime() - new Date(startTime).getTime()
    : 0

  const userMsgCount = messages.filter(m => m.role === 'user').length
  const assistantMsgCount = messages.filter(m => m.role === 'assistant').length

  return {
    sessionId,
    model,
    version,
    gitBranch,
    cwd,
    startTime,
    endTime,
    duration,
    messages,
    tokens: {
      totalInput,
      totalOutput,
      totalCacheRead,
      totalCacheCreation,
      byTurn: turnTokens
    },
    tools: {
      summary: toolSummary,
      calls: toolCalls
    },
    filesModified: [...filesModified],
    subagents,
    summaries,
    stats: {
      messageCount: messages.length,
      userMessageCount: userMsgCount,
      assistantMessageCount: assistantMsgCount,
      toolCallCount: toolCalls.length,
      thinkingBlocks,
      errors
    },
    lastStopReason
  }
}

// ── Exported API ──

export function getTranscriptAnalysis(sessionId?: string, transcriptPath?: string): TranscriptAnalysis | null {
  const filePath = transcriptPath && existsSync(transcriptPath)
    ? transcriptPath
    : resolveTranscriptPath(sessionId)
  if (!filePath) return null

  const sid = sessionIdFromPath(filePath)

  try {
    const stat = statSync(filePath)
    const mtime = stat.mtimeMs

    // Return cached if file hasn't changed
    const cached = cache.get(sid)
    if (cached && cached.lastModified === mtime) {
      return cached.analysis
    }

    // Full parse
    const lines = parseTranscriptFile(filePath)
    const analysis = buildAnalysis(lines, sid)

    cache.set(sid, { analysis, lastModified: mtime })
    return analysis
  } catch (e) {
    console.error('[transcript-engine] Error parsing transcript:', e)
    return null
  }
}

// ── Path normalization (Windows compat) ──

function normalizeTranscriptPath(tp: string): string {
  tp = tp.replace(/\\/g, '/')
  if (/^\/[a-zA-Z]\//.test(tp)) {
    tp = tp[1].toUpperCase() + ':' + tp.slice(2)
  }
  return tp
}

// ── Active session API (for real-time chat) ──

export function setActiveSession(agent: string, sessionId: string, transcriptPath: string): void {
  const normalizedPath = normalizeTranscriptPath(transcriptPath)
  activeAgentSessions.set(agent, { sessionId, transcriptPath: normalizedPath })

  // Initialize offset to current file size (don't replay old messages as "new")
  if (!readOffsets.has(sessionId)) {
    try {
      const stat = statSync(normalizedPath)
      readOffsets.set(sessionId, stat.size)
    } catch {
      readOffsets.set(sessionId, 0)
    }
  }
}

export function getActiveSession(agent: string): { sessionId: string; transcriptPath: string } | null {
  return activeAgentSessions.get(agent) || null
}

export function resetSessionOffset(sessionId: string): void {
  readOffsets.set(sessionId, 0)
}

export function getIncrementalMessages(sessionId: string, transcriptPath?: string): TranscriptMessage[] {
  // Resolve path
  let filePath = transcriptPath ? normalizeTranscriptPath(transcriptPath) : null
  if (!filePath) {
    const session = [...activeAgentSessions.values()].find(s => s.sessionId === sessionId)
    filePath = session?.transcriptPath || null
  }
  if (!filePath) {
    filePath = resolveTranscriptPath(sessionId)
  }
  if (!filePath || !existsSync(filePath)) return []

  try {
    const stat = statSync(filePath)
    const fileSize = stat.size
    const offset = readOffsets.get(sessionId) || 0

    if (offset >= fileSize) return []

    // Read only new bytes
    const buffer = readFileSync(filePath)
    const newContent = buffer.slice(offset, fileSize).toString('utf8')

    // Update offset
    readOffsets.set(sessionId, fileSize)

    // Parse new lines
    const rawLines = newContent.split('\n').filter(l => l.trim())
    const messages: TranscriptMessage[] = []

    // Track assistant chunks by message.id for consolidation
    const assistantChunks = new Map<string, {
      uuid: string
      timestamp: string
      textParts: string[]
      toolNames: string[]
      hasThinking: boolean
    }>()

    for (const line of rawLines) {
      try {
        const obj = JSON.parse(line)

        if (obj.type === 'user') {
          const msg = obj.message
          if (!msg) continue

          const isMeta = !!obj.isMeta
          const text = extractText(msg.content)
          const hasToolResult = Array.isArray(msg.content) &&
            msg.content.some((c: any) => c.type === 'tool_result')

          if (text && !hasToolResult && !isMeta) {
            messages.push({
              uuid: obj.uuid || crypto.randomUUID(),
              role: 'user',
              content: text,
              timestamp: obj.timestamp || new Date().toISOString(),
              isMeta: false,
              hasThinking: false
            })
          }
        } else if (obj.type === 'assistant') {
          const msg = obj.message
          if (!msg || msg.role !== 'assistant') continue

          const msgId = msg.id || obj.uuid || 'unknown'
          let chunk = assistantChunks.get(msgId)
          if (!chunk) {
            chunk = {
              uuid: obj.uuid || crypto.randomUUID(),
              timestamp: obj.timestamp || new Date().toISOString(),
              textParts: [],
              toolNames: [],
              hasThinking: false
            }
            assistantChunks.set(msgId, chunk)
          }

          if (Array.isArray(msg.content)) {
            for (const block of msg.content) {
              if (block.type === 'text' && block.text?.trim()) {
                chunk.textParts.push(block.text)
              } else if (block.type === 'thinking') {
                chunk.hasThinking = true
              } else if (block.type === 'tool_use') {
                chunk.toolNames.push(block.name)
              }
            }
          }
        }
        // Ignore progress, file-history-snapshot, summary
      } catch {
        // Skip unparseable lines
      }
    }

    // Assemble assistant messages from consolidated chunks
    for (const [, chunk] of assistantChunks) {
      const text = chunk.textParts.join('\n').trim()
      if (text || chunk.toolNames.length > 0) {
        messages.push({
          uuid: chunk.uuid,
          role: 'assistant',
          content: text || `[Tool calls: ${chunk.toolNames.join(', ')}]`,
          timestamp: chunk.timestamp,
          isMeta: false,
          toolCalls: chunk.toolNames.length > 0 ? chunk.toolNames : undefined,
          hasThinking: chunk.hasThinking
        })
      }
    }

    // Sort chronologically
    messages.sort((a, b) => a.timestamp.localeCompare(b.timestamp))

    return messages
  } catch (e) {
    console.error('[transcript-engine] Incremental read error:', e)
    return []
  }
}

export function getClaudeStats(): ClaudeStats | null {
  const statsPath = join(homedir(), '.claude', 'stats-cache.json')
  if (!existsSync(statsPath)) return null

  try {
    const raw = JSON.parse(readFileSync(statsPath, 'utf8'))
    const todayStr = new Date().toISOString().slice(0, 10)

    // Find today's daily activity
    const todayActivity = raw.dailyActivity?.find((d: any) => d.date === todayStr) || null
    const todayTokens = raw.dailyModelTokens?.find((d: any) => d.date === todayStr) || null

    const today = todayActivity ? {
      date: todayStr,
      messageCount: todayActivity.messageCount || 0,
      sessionCount: todayActivity.sessionCount || 0,
      toolCallCount: todayActivity.toolCallCount || 0,
      tokensByModel: todayTokens?.tokensByModel || {}
    } : null

    // Model usage
    const modelUsage: ClaudeStats['modelUsage'] = {}
    if (raw.modelUsage) {
      for (const [model, usage] of Object.entries(raw.modelUsage as Record<string, any>)) {
        modelUsage[model] = {
          inputTokens: usage.inputTokens || 0,
          outputTokens: usage.outputTokens || 0,
          cacheReadInputTokens: usage.cacheReadInputTokens || 0,
          cacheCreationInputTokens: usage.cacheCreationInputTokens || 0,
          costUSD: usage.costUSD || 0
        }
      }
    }

    return {
      today,
      modelUsage,
      totalSessions: raw.totalSessions || 0,
      totalMessages: raw.totalMessages || 0,
      firstSessionDate: raw.firstSessionDate || ''
    }
  } catch (e) {
    console.error('[transcript-engine] Error reading stats-cache.json:', e)
    return null
  }
}

export function getClaudeUsage(): ClaudeUsage | null {
  const credentialsPath = join(homedir(), '.claude', '.credentials.json')
  const statsPath = join(homedir(), '.claude', 'stats-cache.json')

  if (!existsSync(credentialsPath)) return null

  try {
    // 1. Read credentials (never expose tokens)
    const creds = JSON.parse(readFileSync(credentialsPath, 'utf8'))
    const oauth = creds.claudeAiOauth || {}
    const subType = oauth.subscriptionType || 'pro'
    const rawTier = oauth.rateLimitTier || ''
    const tier = parseTier(rawTier)
    const limits = TIER_LIMITS[tier.key] || TIER_LIMITS.pro

    // 2. Read stats-cache for daily + weekly data
    let todayMessages = 0
    let todayOutputTokens = 0
    let todaySessions = 0
    let weeklyMessages = 0

    if (existsSync(statsPath)) {
      const raw = JSON.parse(readFileSync(statsPath, 'utf8'))
      const todayStr = new Date().toISOString().slice(0, 10)
      const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)

      for (const day of raw.dailyActivity || []) {
        if (day.date === todayStr) {
          todayMessages = day.messageCount || 0
          todaySessions = day.sessionCount || 0
        }
        if (day.date >= sevenDaysAgo) {
          weeklyMessages += day.messageCount || 0
        }
      }

      // Output tokens for today
      const todayTokens = (raw.dailyModelTokens || []).find((d: any) => d.date === todayStr)
      if (todayTokens?.tokensByModel) {
        todayOutputTokens = Object.values(todayTokens.tokensByModel as Record<string, number>)
          .reduce((sum: number, v: number) => sum + v, 0)
      }
    }

    // 3. Calculate percentages
    const dailyPercent = limits.dailyEstimate > 0
      ? Math.round(todayMessages / limits.dailyEstimate * 100)
      : 0
    const weeklyPercent = limits.weeklyEstimate > 0
      ? Math.round(weeklyMessages / limits.weeklyEstimate * 100)
      : 0

    // 4. Determine status based on highest usage (daily or weekly)
    const maxPercent = Math.max(dailyPercent, weeklyPercent)
    let status: ClaudeUsage['status'] = 'normal'
    if (maxPercent >= 100) status = 'extended'
    else if (maxPercent >= 80) status = 'limit_approaching'
    else if (maxPercent >= 50) status = 'elevated'

    return {
      subscription: { type: subType, tier: tier.key, label: tier.label, multiplier: tier.multiplier },
      today: { messages: todayMessages, outputTokens: todayOutputTokens, sessions: todaySessions },
      daily: { used: todayMessages, limit: limits.dailyEstimate, percent: dailyPercent },
      weekly: { used: weeklyMessages, limit: limits.weeklyEstimate, percent: weeklyPercent },
      status
    }
  } catch (e) {
    console.error('[transcript-engine] Error reading claude usage:', e)
    return null
  }
}

export function listSessions(): SessionInfo[] {
  const projectDir = getProjectDir()
  if (!existsSync(projectDir)) return []

  try {
    const files = readdirSync(projectDir)
      .filter(f => f.endsWith('.jsonl'))
      .map(f => {
        const fullPath = join(projectDir, f)
        const stat = statSync(fullPath)
        return { name: f, path: fullPath, mtime: stat.mtimeMs }
      })
      .sort((a, b) => b.mtime - a.mtime)

    return files.map(f => {
      const sid = f.name.replace('.jsonl', '')

      // Try cache first for quick metadata
      const cached = cache.get(sid)
      if (cached && cached.lastModified === f.mtime) {
        return {
          id: sid,
          startTime: cached.analysis.startTime,
          messageCount: cached.analysis.stats.messageCount,
          model: cached.analysis.model
        }
      }

      // Quick scan: read first few lines for metadata without full parse
      try {
        const content = readFileSync(f.path, 'utf8')
        const firstLines = content.split('\n').slice(0, 20)
        let startTime = ''
        let model = ''
        let lineCount = content.split('\n').filter(l => l.trim()).length

        for (const line of firstLines) {
          if (!line.trim()) continue
          try {
            const obj = JSON.parse(line)
            if (obj.timestamp && !startTime) startTime = obj.timestamp
            if (obj.type === 'assistant' && obj.message?.model && !model) {
              model = obj.message.model
            }
          } catch { /* skip */ }
        }

        return {
          id: sid,
          startTime,
          messageCount: lineCount,
          model
        }
      } catch {
        return { id: sid, startTime: '', messageCount: 0, model: '' }
      }
    })
  } catch {
    return []
  }
}
