'use strict';

const fs = require('fs');
const path = require('path');
const os = require('os');
const zlib = require('zlib');

const DESKTOP_PROJECT_ENTRY = 'claude-desktop';
const DESKTOP_PROJECT_PATH = 'Claude Desktop';
const DESKTOP_AGENT = 'claude-desktop';
const VIRTUAL_MARKER = '#ctm-claude-desktop=';
const CACHE_TTL_MS = 10000;
const MAX_CACHE_FILE_SIZE = 25 * 1024 * 1024;
const SSTABLE_MAGIC_HEX = '57fb808b247547db';

let _cache = { key: '', ts: 0, sessions: [] };

function isDisabled(env = process.env) {
  return env.CTM_DISABLE_CLAUDE_DESKTOP_SESSIONS === '1' ||
    env.CLAUDE_DESKTOP_SESSIONS === '0';
}

function getAppSupportDirs(env = process.env) {
  if (isDisabled(env)) return [];
  const explicit = env.CLAUDE_DESKTOP_APP_SUPPORT_DIRS || env.CLAUDE_DESKTOP_APP_SUPPORT_DIR;
  if (explicit) {
    return explicit.split(path.delimiter).filter(Boolean).map(p => expandHome(p, env));
  }

  const home = env.HOME || os.homedir();
  if (!home) return [];
  if (process.platform === 'darwin') {
    return [
      path.join(home, 'Library', 'Application Support', 'Claude'),
      path.join(home, 'Library', 'Application Support', 'Claude-3p'),
    ];
  }
  if (process.platform === 'win32') {
    const appData = env.APPDATA || path.join(home, 'AppData', 'Roaming');
    return [path.join(appData, 'Claude')];
  }
  const configHome = env.XDG_CONFIG_HOME || path.join(home, '.config');
  return [path.join(configHome, 'Claude')];
}

function expandHome(p, env) {
  if (!p || p[0] !== '~') return p;
  const home = env.HOME || os.homedir();
  return home ? path.join(home, p.slice(1)) : p;
}

function virtualSessionPath(sourcePath, sessionId) {
  return `${sourcePath || DESKTOP_PROJECT_PATH}${VIRTUAL_MARKER}${encodeURIComponent(sessionId)}`;
}

function parseVirtualSessionPath(filePath) {
  const idx = String(filePath || '').indexOf(VIRTUAL_MARKER);
  if (idx === -1) return null;
  let sessionId;
  try {
    sessionId = decodeURIComponent(filePath.slice(idx + VIRTUAL_MARKER.length));
  } catch {
    return null;
  }
  return {
    sourcePath: filePath.slice(0, idx),
    sessionId,
  };
}

function isVirtualSessionPath(filePath) {
  return !!parseVirtualSessionPath(filePath);
}

function sourcePathForStat(filePath) {
  const parsed = parseVirtualSessionPath(filePath);
  return parsed ? parsed.sourcePath : filePath;
}

function listSessionFileEntries(options = {}) {
  return listSessions(options).map(session => ({
    filePath: virtualSessionPath(session.sourcePath || session.appDir || DESKTOP_PROJECT_PATH, session.uuid),
    projectPath: DESKTOP_PROJECT_PATH,
    projectEntry: DESKTOP_PROJECT_ENTRY,
    sessionId: session.uuid,
    agent: DESKTOP_AGENT,
  }));
}

function listSessions(options = {}) {
  const env = options.env || process.env;
  const appDirs = (options.appSupportDirs || getAppSupportDirs(env)).filter(Boolean);
  const key = appDirs.join('\0');
  const now = Date.now();
  if (!options.force && _cache.key === key && (now - _cache.ts) < CACHE_TTL_MS) {
    return _cache.sessions.map(cloneSession);
  }

  const byId = new Map();
  for (const appDir of appDirs) {
    try {
      if (!fs.existsSync(appDir) || !fs.statSync(appDir).isDirectory()) continue;
    } catch {
      continue;
    }

    for (const textInfo of readReactQueryCacheTexts(appDir)) {
      for (const session of parseConversationListsFromReactQueryText(textInfo.text)) {
        mergeSession(byId, { ...session, appDir, sourcePath: textInfo.sourcePath });
      }
    }

    for (const session of readCacheConversations(appDir)) {
      mergeSession(byId, session);
    }
  }

  const sessions = [...byId.values()]
    .filter(s => s.uuid)
    .sort((a, b) => new Date(b.updatedAt || b.modifiedAt || 0) - new Date(a.updatedAt || a.modifiedAt || 0));
  _cache = { key, ts: now, sessions: sessions.map(cloneSession) };
  return sessions.map(cloneSession);
}

function getSession(sessionId, options = {}) {
  if (!sessionId) return null;
  return listSessions(options).find(s => s.uuid === sessionId) || null;
}

function getSessionFromVirtualPath(filePath, options = {}) {
  const parsed = parseVirtualSessionPath(filePath);
  return parsed ? getSession(parsed.sessionId, options) : null;
}

function parseSessionFile(filePath, projectPath = DESKTOP_PROJECT_PATH, projectEntry = DESKTOP_PROJECT_ENTRY, options = {}) {
  const parsed = parseVirtualSessionPath(filePath);
  if (!parsed) throw new Error('not a Claude Desktop virtual session path');
  const session = getSession(parsed.sessionId, options) || {
    uuid: parsed.sessionId,
    sourcePath: parsed.sourcePath,
    title: '',
    summary: '',
    messages: [],
  };
  const stat = statSource(session.sourcePath || parsed.sourcePath);
  const messages = Array.isArray(session.messages) ? session.messages : [];
  const firstUser = messages.find(m => m.role === 'user');
  const firstAssistant = messages.find(m => m.role === 'assistant');
  const lastUser = [...messages].reverse().find(m => m.role === 'user');
  const firstMessage = cleanText(firstUser?.text || session.firstUserText || session.summary || session.title || '');
  const title = makeTitle(session.title || session.name || firstMessage || session.summary || session.uuid);
  const updated = session.updatedAt || session.modifiedAt || stat.modifiedAt || '';
  const created = session.createdAt || updated;

  return {
    sessionId: session.uuid,
    project: projectPath || DESKTOP_PROJECT_PATH,
    projectEntry: projectEntry || DESKTOP_PROJECT_ENTRY,
    cwd: DESKTOP_PROJECT_PATH,
    firstMessage,
    lastUserContent: cleanText(lastUser?.text || firstMessage || ''),
    firstAssistantText: cleanText(firstAssistant?.text || ''),
    renameName: '',
    title,
    isEmpty: !firstMessage && messages.length === 0,
    userMsgCount: messages.filter(m => m.role === 'user').length,
    modifiedAt: updated,
    fileModifiedAt: stat.modifiedAt || updated,
    timestamp: created,
    version: '',
    gitBranch: '',
    slug: '',
    fileSize: stat.fileSize,
    hostname: os.hostname(),
    modelId: session.model || '',
    modelProvider: session.model ? 'anthropic' : '',
    model: session.model || '',
    agent: DESKTOP_AGENT,
    jsonlPath: '',
    sourcePath: session.sourcePath || parsed.sourcePath,
    desktopCacheOnly: messages.length === 0,
  };
}

function getMessages(sessionId, options = {}) {
  const session = getSession(sessionId, options);
  if (!session) return null;
  const messages = Array.isArray(session.messages) ? session.messages : [];
  if (messages.length > 0) return messages.map(m => ({ ...m }));
  if (!options.includeMetadataNote) return [];
  return [{
    role: 'system',
    text: 'Claude Desktop has cached this conversation in the recent list, but the full message transcript is not present in the local Desktop HTTP cache yet. Open the conversation in Claude Desktop once, then rescan to populate the transcript.',
    timestamp: session.updatedAt || session.modifiedAt || session.createdAt || new Date().toISOString(),
  }];
}

function toClaudeCodeEntries(sessionOrId, options = {}) {
  const session = typeof sessionOrId === 'string' ? getSession(sessionOrId, options) : sessionOrId;
  if (!session || !Array.isArray(session.messages)) return [];
  return session.messages.map((msg, index) => ({
    type: msg.role === 'assistant' ? 'assistant' : 'user',
    provider: DESKTOP_AGENT,
    timestamp: msg.timestamp,
    uuid: msg.uuid || `${session.uuid}:${index}`,
    message: {
      role: msg.role === 'assistant' ? 'assistant' : 'user',
      content: msg.text || '',
      model: msg.role === 'assistant' ? (session.model || '') : undefined,
    },
    _source: DESKTOP_AGENT,
    _sessionId: session.uuid,
  }));
}

function readReactQueryCacheTexts(appDir) {
  const levelDir = path.join(appDir, 'Local Storage', 'leveldb');
  let files;
  try { files = fs.readdirSync(levelDir); } catch { return []; }
  const out = [];
  for (const file of files) {
    if (!file.endsWith('.ldb')) continue;
    const filePath = path.join(levelDir, file);
    let stat;
    try { stat = fs.statSync(filePath); } catch { continue; }
    if (!stat.isFile() || stat.size > MAX_CACHE_FILE_SIZE) continue;
    for (const { key, value } of readSstableEntries(filePath)) {
      const keyText = key.toString('utf8');
      if (!keyText.includes('react-query-cache-ls')) continue;
      const text = decodeDomStorageValue(value);
      if (text && text.includes('chat_conversation_list')) out.push({ sourcePath: filePath, text });
    }
  }
  return out;
}

function readCacheConversations(appDir) {
  const cacheDir = path.join(appDir, 'Cache', 'Cache_Data');
  let files;
  try { files = fs.readdirSync(cacheDir); } catch { return []; }
  const sessions = [];
  for (const file of files) {
    const filePath = path.join(cacheDir, file);
    let stat;
    try { stat = fs.statSync(filePath); } catch { continue; }
    if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_CACHE_FILE_SIZE) continue;

    // Probe only the 4096-byte HEAD first: the Electron HTTP cache holds hundreds of files
    // and almost none are chat conversations. Reading every file IN FULL just to test the
    // head was ~15s of synchronous readFileSync at boot (named by the fs-census:
    // readFileSync@claude-desktop-sessions.js — 408 full reads). Read the head via one
    // bounded readSync, and only fall back to a full read when the marker actually matches.
    let buf;
    let fd = -1;
    try {
      fd = fs.openSync(filePath, 'r');
      const headLen = Math.min(stat.size, 4096);
      const headBuf = Buffer.allocUnsafe(headLen);
      const n = fs.readSync(fd, headBuf, 0, headLen, 0); // only inspect the bytes actually read
      if (!/chat_conversations/i.test(headBuf.subarray(0, n).toString('latin1'))) { fs.closeSync(fd); continue; }
      buf = fs.readFileSync(filePath); // marker present → now read the whole file
    } catch { if (fd >= 0) { try { fs.closeSync(fd); } catch {} } continue; }
    try { fs.closeSync(fd); } catch {}

    const json = extractJsonFromCacheBuffer(buf);
    if (!json) continue;
    for (const session of normalizeCacheJson(json, filePath, appDir, stat)) {
      sessions.push(session);
    }
  }
  return sessions;
}

function extractJsonFromCacheBuffer(buf) {
  const candidates = [];
  const zstdIdx = buf.indexOf(Buffer.from([0x28, 0xb5, 0x2f, 0xfd]));
  if (zstdIdx >= 0 && typeof zlib.zstdDecompressSync === 'function') {
    candidates.push(() => zlib.zstdDecompressSync(buf.subarray(zstdIdx)).toString('utf8'));
  }
  const gzipIdx = buf.indexOf(Buffer.from([0x1f, 0x8b, 0x08]));
  if (gzipIdx >= 0) {
    candidates.push(() => zlib.gunzipSync(buf.subarray(gzipIdx)).toString('utf8'));
  }
  const brMarker = Buffer.from('content-encoding:brotli', 'latin1');
  if (buf.indexOf(brMarker) >= 0 && typeof zlib.brotliDecompressSync === 'function') {
    const start = findFirstJsonishByte(buf);
    if (start >= 0) candidates.push(() => zlib.brotliDecompressSync(buf.subarray(start)).toString('utf8'));
  }
  candidates.push(() => {
    const text = buf.toString('utf8');
    const idx = text.search(/[\[{]/);
    return idx >= 0 ? text.slice(idx) : '';
  });

  for (const makeText of candidates) {
    let text = '';
    try { text = makeText(); } catch { continue; }
    const parsed = parseFirstJsonValue(text);
    if (parsed) return parsed;
  }
  return null;
}

function findFirstJsonishByte(buf) {
  const brace = buf.indexOf(0x7b);
  const bracket = buf.indexOf(0x5b);
  if (brace === -1) return bracket;
  if (bracket === -1) return brace;
  return Math.min(brace, bracket);
}

function parseFirstJsonValue(text) {
  const start = text.search(/[\[{]/);
  if (start < 0) return null;
  const end = findJsonValueEnd(text, start);
  const raw = (end > start ? text.slice(start, end) : text.slice(start)).replace(/[\u0000-\u001f]/g, ' ');
  try { return JSON.parse(raw); } catch { return null; }
}

function normalizeCacheJson(json, sourcePath, appDir, stat) {
  const out = [];
  if (Array.isArray(json)) {
    for (const item of json) {
      const session = normalizeConversationObject(item, sourcePath, appDir, stat);
      if (session) out.push(session);
    }
    return out;
  }
  if (Array.isArray(json.data)) {
    for (const item of json.data) {
      const session = normalizeConversationObject(item, sourcePath, appDir, stat);
      if (session) out.push(session);
    }
  }
  const direct = normalizeConversationObject(json, sourcePath, appDir, stat);
  if (direct) out.push(direct);
  return out;
}

function parseConversationListsFromReactQueryText(text) {
  const sessions = [];
  let pos = 0;
  while ((pos = text.indexOf('"queryKey":["chat_conversation_list"', pos + 1)) !== -1) {
    const start = text.lastIndexOf('{"state":', pos);
    if (start < 0) continue;
    const end = findJsonValueEnd(text, start);
    if (end <= start) continue;
    const raw = text.slice(start, end).replace(/[\u0000-\u001f]/g, ' ');
    let query;
    try { query = JSON.parse(raw); } catch { continue; }
    const data = query.state && query.state.data;
    const items = Array.isArray(data) ? data
      : Array.isArray(data && data.data) ? data.data
        : Array.isArray(data && data.conversations) ? data.conversations
          : [];
    for (const item of items) {
      const session = normalizeConversationObject(item);
      if (session) sessions.push({ ...session, sourceKind: 'recent-list', metadataOnly: session.messages.length === 0 });
    }
  }
  return sessions;
}

function normalizeConversationObject(obj, sourcePath = '', appDir = '', stat = null) {
  if (!obj || typeof obj !== 'object') return null;
  const uuid = cleanText(obj.uuid || obj.conversation_uuid || obj.id || '');
  if (!isUuid(uuid)) return null;
  const messages = normalizeMessages(obj.chat_messages || obj.messages || []);
  const firstUser = messages.find(m => m.role === 'user');
  const firstAssistant = messages.find(m => m.role === 'assistant');
  const modifiedAt = stat && stat.mtime ? stat.mtime.toISOString() : '';
  return {
    uuid,
    name: cleanText(obj.name || obj.title || ''),
    title: cleanText(obj.name || obj.title || ''),
    summary: cleanText(obj.summary || ''),
    model: cleanText(obj.model || obj.settings?.model || ''),
    createdAt: cleanText(obj.created_at || obj.createdAt || ''),
    updatedAt: cleanText(obj.updated_at || obj.updatedAt || modifiedAt),
    modifiedAt,
    sourcePath,
    appDir,
    isStarred: !!obj.is_starred,
    platform: cleanText(obj.platform || ''),
    desktopSessionId: cleanText(obj.session_id || ''),
    messages,
    firstUserText: cleanText(firstUser?.text || ''),
    firstAssistantText: cleanText(firstAssistant?.text || ''),
    userMsgCount: messages.filter(m => m.role === 'user').length,
    assistantMsgCount: messages.filter(m => m.role === 'assistant').length,
    sourceKind: sourcePath ? 'http-cache' : '',
    metadataOnly: messages.length === 0,
  };
}

function normalizeMessages(messages) {
  if (!Array.isArray(messages)) return [];
  return messages.map((msg, index) => {
    const role = normalizeRole(msg.sender || msg.role || msg.author);
    const text = cleanText(extractMessageText(msg));
    if (!role || !text) return null;
    return {
      role,
      text,
      timestamp: cleanText(msg.created_at || msg.updated_at || msg.timestamp || ''),
      uuid: cleanText(msg.uuid || msg.id || ''),
      index: Number.isFinite(msg.index) ? msg.index : index,
    };
  }).filter(Boolean).sort((a, b) => {
    if (a.index !== b.index) return a.index - b.index;
    return (a.timestamp || '').localeCompare(b.timestamp || '');
  });
}

function normalizeRole(role) {
  const v = String(role || '').toLowerCase();
  if (v === 'human' || v === 'user') return 'user';
  if (v === 'assistant' || v === 'claude') return 'assistant';
  if (v === 'system') return 'system';
  return '';
}

function extractMessageText(msg) {
  if (!msg) return '';
  if (typeof msg.text === 'string' && msg.text) return msg.text;
  return extractContentText(msg.content);
}

function extractContentText(value) {
  if (!value) return '';
  if (typeof value === 'string') return value;
  if (Array.isArray(value)) {
    return value.map(extractContentText).filter(Boolean).join('\n');
  }
  if (typeof value !== 'object') return '';
  if (typeof value.text === 'string') return value.text;
  if (typeof value.content === 'string') return value.content;
  if (Array.isArray(value.content)) return extractContentText(value.content);
  if (value.type === 'tool_use') return `[Tool: ${cleanText(value.name || 'tool')}]`;
  return '';
}

function mergeSession(map, incoming) {
  if (!incoming || !incoming.uuid) return;
  const existing = map.get(incoming.uuid);
  if (!existing) {
    map.set(incoming.uuid, incoming);
    return;
  }
  const existingMsgCount = Array.isArray(existing.messages) ? existing.messages.length : 0;
  const incomingMsgCount = Array.isArray(incoming.messages) ? incoming.messages.length : 0;
  const preferMessages = incomingMsgCount >= existingMsgCount;
  const newerIncoming = new Date(incoming.updatedAt || incoming.modifiedAt || 0) >= new Date(existing.updatedAt || existing.modifiedAt || 0);
  const messages = preferMessages ? (incoming.messages || []) : (existing.messages || []);
  map.set(incoming.uuid, {
    ...existing,
    ...(newerIncoming ? incoming : {}),
    title: incoming.title || existing.title || incoming.summary || existing.summary || '',
    name: incoming.name || existing.name || '',
    summary: incoming.summary || existing.summary || '',
    model: incoming.model || existing.model || '',
    createdAt: existing.createdAt || incoming.createdAt || '',
    updatedAt: newerIncoming ? (incoming.updatedAt || existing.updatedAt || '') : (existing.updatedAt || incoming.updatedAt || ''),
    modifiedAt: newerIncoming ? (incoming.modifiedAt || existing.modifiedAt || '') : (existing.modifiedAt || incoming.modifiedAt || ''),
    sourcePath: preferMessages ? (incoming.sourcePath || existing.sourcePath || '') : (existing.sourcePath || incoming.sourcePath || ''),
    appDir: incoming.appDir || existing.appDir || '',
    messages,
    firstUserText: incoming.firstUserText || existing.firstUserText || '',
    firstAssistantText: incoming.firstAssistantText || existing.firstAssistantText || '',
    userMsgCount: preferMessages ? (incoming.userMsgCount || 0) : (existing.userMsgCount || 0),
    assistantMsgCount: preferMessages ? (incoming.assistantMsgCount || 0) : (existing.assistantMsgCount || 0),
    sourceKind: preferMessages ? (incoming.sourceKind || existing.sourceKind || '') : (existing.sourceKind || incoming.sourceKind || ''),
    metadataOnly: messages.length === 0,
  });
}

function readSstableEntries(filePath) {
  let db;
  try { db = fs.readFileSync(filePath); } catch { return []; }
  if (db.length < 53 || db.subarray(db.length - 8).toString('hex') !== SSTABLE_MAGIC_HEX) return [];
  const footer = db.subarray(db.length - 48);
  const meta = decodeBlockHandle(footer, 0);
  if (!meta) return [];
  const index = decodeBlockHandle(footer, meta.bytes);
  if (!index) return [];

  let indexEntries;
  try { indexEntries = parseBlockEntries(readBlock(db, index.offset, index.size)); } catch { return []; }
  const out = [];
  for (const entry of indexEntries) {
    const handle = decodeBlockHandle(entry.value, 0);
    if (!handle) continue;
    let block;
    try { block = readBlock(db, handle.offset, handle.size); } catch { continue; }
    out.push(...parseBlockEntries(block));
  }
  return out;
}

function decodeBlockHandle(buf, off) {
  const a = readVarint(buf, off);
  if (!a) return null;
  const b = readVarint(buf, off + a.bytes);
  if (!b) return null;
  return { offset: a.value, size: b.value, bytes: a.bytes + b.bytes };
}

function readBlock(db, offset, size) {
  if (offset < 0 || size < 0 || offset + size + 5 > db.length) throw new Error('bad block');
  const raw = db.subarray(offset, offset + size);
  const compression = db[offset + size];
  if (compression === 0) return raw;
  if (compression === 1) return snappyDecompress(raw);
  throw new Error(`unsupported leveldb compression ${compression}`);
}

function parseBlockEntries(block) {
  if (!block || block.length < 4) return [];
  const restartCount = block.readUInt32LE(block.length - 4);
  const restartsStart = block.length - 4 - restartCount * 4;
  if (restartCount < 0 || restartsStart < 0 || restartsStart > block.length) return [];
  let off = 0;
  let prev = Buffer.alloc(0);
  const entries = [];
  while (off < restartsStart) {
    const shared = readVarint(block, off); if (!shared) break; off += shared.bytes;
    const nonShared = readVarint(block, off); if (!nonShared) break; off += nonShared.bytes;
    const valueLen = readVarint(block, off); if (!valueLen) break; off += valueLen.bytes;
    if (shared.value > prev.length || off + nonShared.value + valueLen.value > restartsStart) break;
    const key = Buffer.concat([prev.subarray(0, shared.value), block.subarray(off, off + nonShared.value)]);
    off += nonShared.value;
    const value = block.subarray(off, off + valueLen.value);
    off += valueLen.value;
    prev = key;
    entries.push({ key, value });
  }
  return entries;
}

function readVarint(buf, off) {
  let result = 0;
  let shift = 0;
  for (let i = 0; i < 10 && off + i < buf.length; i++) {
    const byte = buf[off + i];
    result += (byte & 0x7f) * Math.pow(2, shift);
    if ((byte & 0x80) === 0) return { value: result, bytes: i + 1 };
    shift += 7;
  }
  return null;
}

function snappyDecompress(buf) {
  const lenInfo = readVarint(buf, 0);
  if (!lenInfo) throw new Error('bad snappy length');
  const out = Buffer.alloc(lenInfo.value);
  let ip = lenInfo.bytes;
  let op = 0;
  while (ip < buf.length && op < out.length) {
    const tag = buf[ip++];
    const type = tag & 3;
    if (type === 0) {
      let len = tag >>> 2;
      if (len < 60) {
        len += 1;
      } else {
        const n = len - 59;
        len = 0;
        for (let i = 0; i < n; i++) len |= buf[ip++] << (8 * i);
        len += 1;
      }
      if (ip + len > buf.length || op + len > out.length) throw new Error('bad snappy literal');
      buf.copy(out, op, ip, ip + len);
      ip += len;
      op += len;
    } else {
      let len;
      let offset;
      if (type === 1) {
        len = ((tag >>> 2) & 7) + 4;
        offset = ((tag & 0xe0) << 3) | buf[ip++];
      } else if (type === 2) {
        len = (tag >>> 2) + 1;
        offset = buf[ip] | (buf[ip + 1] << 8);
        ip += 2;
      } else {
        len = (tag >>> 2) + 1;
        offset = (buf[ip] | (buf[ip + 1] << 8) | (buf[ip + 2] << 16) | (buf[ip + 3] << 24)) >>> 0;
        ip += 4;
      }
      if (offset <= 0 || offset > op || op + len > out.length) throw new Error('bad snappy copy');
      for (let i = 0; i < len; i++) out[op + i] = out[op - offset + i];
      op += len;
    }
  }
  if (op !== out.length) throw new Error('truncated snappy block');
  return out;
}

function decodeDomStorageValue(buf) {
  if (!buf || buf.length === 0) return '';
  if (buf.length > 2 && buf[0] === 0 && buf[1] !== 0) return decodeUtf16be(buf);
  if (buf.length > 2 && buf[1] === 0) return buf.toString('utf16le');
  return buf.toString('utf8');
}

function decodeUtf16be(buf) {
  const chunks = [];
  const maxBytes = 16384;
  for (let i = 0; i + 1 < buf.length; i += maxBytes) {
    let s = '';
    const end = Math.min(buf.length - 1, i + maxBytes);
    for (let j = i; j < end; j += 2) s += String.fromCharCode((buf[j] << 8) | buf[j + 1]);
    chunks.push(s);
  }
  return chunks.join('');
}

function findJsonValueEnd(text, start) {
  const first = text[start];
  if (first !== '{' && first !== '[') return -1;
  const stack = [first === '{' ? '}' : ']'];
  let inString = false;
  let escaped = false;
  for (let i = start + 1; i < text.length; i++) {
    const ch = text[i];
    if (inString) {
      if (escaped) escaped = false;
      else if (ch === '\\') escaped = true;
      else if (ch === '"') inString = false;
      continue;
    }
    if (ch === '"') {
      inString = true;
    } else if (ch === '{') {
      stack.push('}');
    } else if (ch === '[') {
      stack.push(']');
    } else if (ch === stack[stack.length - 1]) {
      stack.pop();
      if (stack.length === 0) return i + 1;
    }
  }
  return -1;
}

function cleanText(value) {
  if (value == null) return '';
  return String(value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ' ').trim();
}

function makeTitle(value) {
  let title = cleanText(value).split('\n')[0].replace(/^#+\s*/, '').replace(/[*_`]/g, '').trim();
  if (title.length > 80) title = title.slice(0, 77) + '...';
  return title;
}

function isUuid(value) {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value || '');
}

function statSource(filePath) {
  try {
    const st = fs.statSync(sourcePathForStat(filePath));
    return {
      fileSize: st.size || 0,
      modifiedAt: st.mtime ? st.mtime.toISOString() : '',
    };
  } catch {
    return { fileSize: 0, modifiedAt: '' };
  }
}

function cloneSession(session) {
  return {
    ...session,
    messages: Array.isArray(session.messages) ? session.messages.map(m => ({ ...m })) : [],
  };
}

module.exports = {
  DESKTOP_PROJECT_ENTRY,
  DESKTOP_PROJECT_PATH,
  DESKTOP_AGENT,
  VIRTUAL_MARKER,
  getAppSupportDirs,
  listSessions,
  getSession,
  getSessionFromVirtualPath,
  listSessionFileEntries,
  parseSessionFile,
  getMessages,
  toClaudeCodeEntries,
  virtualSessionPath,
  parseVirtualSessionPath,
  isVirtualSessionPath,
  sourcePathForStat,
  _private: {
    parseConversationListsFromReactQueryText,
    normalizeConversationObject,
    normalizeMessages,
    extractJsonFromCacheBuffer,
    decodeDomStorageValue,
    decodeUtf16be,
    findJsonValueEnd,
    snappyDecompress,
    readSstableEntries,
  },
};
