/**
 * cost/transcript.mjs — runtime-callable Claude Code transcript parser (C11).
 *
 * Mirror of cost/transcript.ts kept in .mjs so the CLI dispatcher (uncompiled
 * .mjs runtime) can import without a TypeScript build step. The TS file is
 * the type-checked source of truth for library consumers; this file is the
 * source of truth for the CLI runtime. Algorithms are byte-for-byte identical
 * so the transcript.test.ts coverage (against the .ts file) doubles as a
 * regression net for this one — any divergence shows up as either:
 *   (a) the unit suite passing while CLI integration tests fail, or
 *   (b) a fixture-aggregation mismatch in tokens-record.test.ts.
 *
 * See transcript.ts for the full design notes.
 */

import { existsSync, readFileSync } from 'node:fs'

function toNumber(v) {
	if (typeof v === 'number' && Number.isFinite(v)) return v
	return 0
}

function durationBetween(first, last) {
	if (!first || !last) return 0
	const a = Date.parse(first)
	const b = Date.parse(last)
	if (Number.isNaN(a) || Number.isNaN(b)) return 0
	return Math.max(0, b - a)
}

function newAccumulator(sessionId, modelId) {
	return {
		sessionId,
		modelId,
		inputTokens: 0,
		outputTokens: 0,
		cacheReadTokens: 0,
		cacheCreationTokens: 0,
		thinkingTokens: 0,
		firstTimestamp: null,
		lastTimestamp: null,
	}
}

/**
 * Parse a Claude Code JSONL transcript file. Returns one entry per session.
 * Pure: only reads `path` once via readFileSync; no env reads, no cwd.
 */
export function parseClaudeCodeTranscript(path) {
	if (!existsSync(path)) return []
	let raw
	try {
		raw = readFileSync(path, 'utf-8')
	} catch {
		return []
	}

	const sessions = new Map()
	for (const line of raw.split('\n')) {
		if (!line) continue
		let evt
		try {
			evt = JSON.parse(line)
		} catch {
			continue
		}
		if (!evt || typeof evt !== 'object') continue
		if (evt.type !== 'assistant') continue
		const message = evt.message
		if (!message || typeof message !== 'object') continue
		const usage = message.usage
		if (!usage || typeof usage !== 'object') continue

		const sessionId =
			(typeof evt.session_id === 'string' && evt.session_id) ||
			(typeof evt.sessionId === 'string' && evt.sessionId) ||
			'unknown-session'
		const modelId = (typeof message.model === 'string' && message.model) || 'unknown-model'
		const timestamp =
			(typeof evt.timestamp === 'string' && evt.timestamp) ||
			(typeof evt.t === 'string' && evt.t) ||
			null

		const acc = sessions.get(sessionId) ?? newAccumulator(sessionId, modelId)
		acc.modelId = modelId
		acc.inputTokens += toNumber(usage.input_tokens)
		acc.outputTokens += toNumber(usage.output_tokens)
		acc.cacheReadTokens += toNumber(usage.cache_read_input_tokens)
		acc.cacheCreationTokens += toNumber(usage.cache_creation_input_tokens)
		const details = usage.output_tokens_details
		acc.thinkingTokens +=
			toNumber(usage.thinking_tokens) +
			(details && typeof details === 'object' ? toNumber(details.thinking_tokens) : 0)

		if (timestamp) {
			if (!acc.firstTimestamp) acc.firstTimestamp = timestamp
			acc.lastTimestamp = timestamp
		}
		sessions.set(sessionId, acc)
	}

	const out = []
	for (const acc of sessions.values()) {
		const ts = acc.firstTimestamp ?? acc.lastTimestamp ?? new Date(0).toISOString()
		out.push({
			sessionId: acc.sessionId,
			modelId: acc.modelId,
			inputTokens: acc.inputTokens,
			outputTokens: acc.outputTokens,
			cacheReadTokens: acc.cacheReadTokens,
			cacheCreationTokens: acc.cacheCreationTokens,
			thinkingTokens: acc.thinkingTokens,
			timestamp: ts,
			durationMs: durationBetween(acc.firstTimestamp, acc.lastTimestamp),
		})
	}
	return out
}
