'use strict';
/**
 * Detect Claude Code's context-compact continuation boundary.
 *
 * When a Claude Code session hits its context window, the CLI:
 *   1. Rotates the current session file `<old>.jsonl` → `<old>.jsonl.bak`
 *   2. Writes a brand-new `<new>.jsonl` whose first user message is a
 *      system-injected "This session is being continued from a previous
 *      conversation..." preamble (or a "Compacted" marker).
 *
 * Before this helper existed, the new file was treated as a standalone
 * session, producing the "ghost session" pain documented in the
 * 2026-04-21 builder-journal entry: the abandoned .bak kept its title
 * while the real (continuation) session appeared untitled.
 *
 * The user chose the "single thread merge" outcome (see
 * gap-reports/viewer-2026-04-23.md item L): detect the continuation,
 * find the predecessor's ctm_session_id, and attach the new
 * agent_session to the same ctm_session so all messages render as one
 * thread. Our existing message loader already merges all agent_sessions
 * under a ctm_session_id, so linking at the DB level is the entire fix.
 *
 * This module is pure: no DB access, no event emit. It does hit the
 * filesystem (readFileSync, readdirSync) but only in a narrow, bounded
 * way suitable for unit testing with a temp dir.
 */

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

// Known compact-continuation preambles that Claude Code injects as the
// first user message of the new JSONL. Matching is a substring check on
// the cleaned first-user text so minor format drift doesn't break the
// detector. Keep these in sync with session-stream.js:_cleanUserText's
// preambleRe and server.js's "continued from a previous conversation"
// stripper.
const COMPACT_MARKERS = Object.freeze([
  'This session is being continued from a previous conversation',
  // Claude sometimes uses "Compacted." as a standalone marker; we match
  // the word at line start rather than anywhere, to avoid catching the
  // literal word in user prose.
  /^\s*Compacted\b/m,
]);

/**
 * Return the text of the first user message inside a JSONL chunk, or ''
 * if none found. Only looks at the first ~max bytes to stay bounded.
 */
function firstUserMessageFromJsonl(content) {
  if (!content) return '';
  const lines = content.split('\n');
  for (const line of lines) {
    if (!line) continue;
    let entry;
    try { entry = JSON.parse(line); } catch { continue; }
    if (entry && entry.type === 'user' && entry.message && entry.message.role === 'user') {
      const c = entry.message.content;
      if (typeof c === 'string') return c;
      if (Array.isArray(c)) {
        const textBlock = c.find(b => b && b.type === 'text' && typeof b.text === 'string');
        if (textBlock) return textBlock.text;
      }
      return '';
    }
  }
  return '';
}

/**
 * Does `text` look like Claude's compact-continuation preamble?
 * Substring + regex check against COMPACT_MARKERS.
 */
function looksLikeCompactContinuation(text) {
  if (!text) return false;
  for (const m of COMPACT_MARKERS) {
    if (typeof m === 'string') {
      if (text.includes(m)) return true;
    } else if (m instanceof RegExp) {
      if (m.test(text)) return true;
    }
  }
  return false;
}

/**
 * Read a bounded prefix of a JSONL file and return the first user-message
 * text, or '' if anything goes wrong. Bounded to 16KB by default.
 */
function readFirstUserMessageFromFile(filePath, maxBytes = 16 * 1024) {
  let fd;
  try {
    const stat = fs.statSync(filePath);
    const readSize = Math.min(stat.size, maxBytes);
    if (readSize === 0) return '';
    fd = fs.openSync(filePath, 'r');
    const buf = Buffer.alloc(readSize);
    fs.readSync(fd, buf, 0, readSize, 0);
    return firstUserMessageFromJsonl(buf.toString('utf8'));
  } catch {
    return '';
  } finally {
    if (fd !== undefined) { try { fs.closeSync(fd); } catch {} }
  }
}

/**
 * Scan a project directory for .jsonl.bak files and return the basename
 * (without .jsonl.bak) of the most-recently-modified one, or null.
 *
 * Used to locate the predecessor agent_session_id after a context
 * compact: Claude rotates `<old>.jsonl` → `<old>.jsonl.bak`, so the .bak
 * basename IS the old agent_session_id. Skips the new file explicitly so
 * we never pick ourselves if the caller hasn't guarded against that.
 */
function findNewestBakAgentSessionId(projectDir, excludeAgentSessionId = null) {
  let entries;
  try { entries = fs.readdirSync(projectDir); } catch { return null; }
  let bestName = null;
  let bestMtime = -Infinity;
  for (const name of entries) {
    if (!name.endsWith('.jsonl.bak')) continue;
    const base = name.replace(/\.jsonl\.bak$/, '');
    if (excludeAgentSessionId && base === excludeAgentSessionId) continue;
    let stat;
    try { stat = fs.statSync(path.join(projectDir, name)); } catch { continue; }
    if (stat.mtimeMs > bestMtime) {
      bestMtime = stat.mtimeMs;
      bestName = base;
    }
  }
  return bestName;
}

module.exports = {
  COMPACT_MARKERS,
  firstUserMessageFromJsonl,
  looksLikeCompactContinuation,
  readFirstUserMessageFromFile,
  findNewestBakAgentSessionId,
};
