'use strict';

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

function _isCodexPath(fp) {
  return String(fp || '').includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
}

// Lossless hydration for the row-store read path.
//
// transcript_events rows store a LIGHTWEIGHT, truncated, text-only projection of each
// message (good for search + fast pagination) plus a byte offset (jsonl_path, byte_start,
// byte_end) into the source JSONL. To render a session WITHOUT losing content (tool calls,
// tool results, images, >50KB messages), we use the rows only to LOCATE the page's bytes,
// then read that bounded span from the JSONL and parse it with the SAME parser the legacy
// blob/JSONL read path uses (_parseJsonlIntoMessages) — so the hydrated messages are
// byte-identical to what the blob path renders. The rows are an index; the JSONL stays the
// source of truth. The read is bounded to the page's span (not the whole multi-GB file), so
// there is no freeze.

function readByteRange(filePath, start, length) {
  const len = Math.max(0, Number(length) || 0);
  if (!filePath || len === 0) return '';
  const fd = fs.openSync(filePath, 'r');
  try {
    const buf = Buffer.alloc(len);
    const n = fs.readSync(fd, buf, 0, len, Math.max(0, Number(start) || 0));
    return buf.subarray(0, n).toString('utf8');
  } finally {
    try { fs.closeSync(fd); } catch { /* ignore */ }
  }
}

// rows: page rows with { jsonl_path, byte_start, byte_end }.
// lightweightFallback: the row-text messages to serve if NOTHING can be hydrated
// (every source file moved/truncated) — so a missing file degrades to text, never throws.
// Returns FULL-fidelity messages for the page, chronologically sorted.
function hydratePageMessages(rows, lightweightFallback = []) {
  let parser;
  try { parser = require('./jsonl-conversation-parser'); } catch { return lightweightFallback; }
  const claudeParse = parser._parseJsonlIntoMessages;
  if (typeof claudeParse !== 'function') return lightweightFallback;
  // The Codex content parser lives in session-history (jsonl-conversation-parser imports but
  // does NOT re-export it). Without it, Codex content parses as 0/garbage.
  let codexParse = null;
  try {
    const sh = require('./session-history');
    if (typeof sh.parseCodexJsonlIntoMessages === 'function') codexParse = sh.parseCodexJsonlIntoMessages;
  } catch { /* optional */ }

  const list = Array.isArray(rows) ? rows : [];
  if (list.length === 0) return lightweightFallback;
  const liteOf = (r) => ({ role: r && r.role, text: (r && (r.text != null ? r.text : r.content)) || '', timestamp: (r && r.timestamp) || '' });

  // Group the page's rows per source file and decide PER FILE how to hydrate:
  //  - Contiguous span (Claude: timestamps monotonic with byte → the page's rows sit in a
  //    small byte range). Reading that whole span and parsing it captures the tool-use /
  //    tool-result lines BETWEEN the message rows — which the transcript rows omit but the
  //    blob/JSONL render shows. This is what makes Claude render-identical to the blob path.
  //  - Row-by-row (Codex: timestamps are NOT monotonic with byte, so the page's rows scatter
  //    across the whole multi-GB file; a span read would cover ~the entire file = wrong content
  //    + a near-freeze). Reading each row's own line stays bounded and exact.
  // The span-size threshold distinguishes the two automatically from the actual byte layout.
  const MAX_SPAN_BYTES = 16 * 1024 * 1024;
  const byFile = new Map();
  for (const r of list) {
    const fp = r && r.jsonl_path;
    if (!fp || !(Number(r.byte_end) > Number(r.byte_start))) { byFile.set('__lite__:' + (byFile.size), { lite: liteOf(r) }); continue; }
    const g = byFile.get(fp) || { fp, rows: [], min: Infinity, max: 0 };
    g.rows.push(r); g.min = Math.min(g.min, Number(r.byte_start)); g.max = Math.max(g.max, Number(r.byte_end));
    byFile.set(fp, g);
  }

  const out = [];
  let hydratedAny = false;
  const parseInto = (content, fp, sink) => {
    if (_isCodexPath(fp) && typeof codexParse === 'function') codexParse(content, sink, {});
    else claudeParse(content, sink, { sourceFile: fp });
  };
  for (const [, g] of byFile) {
    if (g.lite) { out.push(g.lite); continue; }
    const spanBytes = g.max - g.min;
    // Span-read only for Claude/generic (rows contiguous in byte; captures interleaved tool
    // lines). Codex ALWAYS goes row-by-row: its rows already exclude tool/event payloads, and
    // its timestamps aren't byte-monotonic so even a small span can mis-span. Provider, not
    // span size, is the right discriminator (small-span Codex regressed under a size-only rule).
    if (!_isCodexPath(g.fp) && spanBytes > 0 && spanBytes <= MAX_SPAN_BYTES) {
      let content = '';
      try { content = readByteRange(g.fp, g.min, spanBytes); } catch { content = ''; }
      const parsed = [];
      if (content) { try { parseInto(content, g.fp, parsed); } catch { /* */ } }
      if (parsed.length) { for (const m of parsed) out.push(m); hydratedAny = true; continue; }
      // fall through to row-by-row if the span produced nothing
    }
    for (const r of g.rows) {
      let content = '';
      try { content = readByteRange(g.fp, Number(r.byte_start), Number(r.byte_end) - Number(r.byte_start)); } catch { content = ''; }
      const parsed = [];
      if (content) { try { parseInto(content, g.fp, parsed); } catch { /* */ } }
      if (parsed.length) { for (const m of parsed) out.push(m); hydratedAny = true; }
      else out.push(liteOf(r));
    }
  }
  if (!hydratedAny) return lightweightFallback;
  out.forEach(m => { if (m) { delete m._sourceFile; delete m._sourceLine; delete m._entrySessionId; } });
  // Order chronologically to match the blob path's sortTimelineMessagesInPlace.
  out.sort((a, b) => String(a.timestamp || '').localeCompare(String(b.timestamp || '')));
  return out;
}

module.exports = { hydratePageMessages, readByteRange };
