/**
 * @file codeMapStubRenderer.js
 * @description Optional renderer that replaces code-map tool-result content
 *   in PRIOR turns with a compact stub. The current turn's content stays
 *   intact so the agent can act on what it just fetched.
 *
 * Design principle (from the focused-agent thread):
 *   - conversation = action log
 *   - cache = knowledge store
 *   - index injection = library map
 *
 * The agent's working context shouldn't hold duplicates of bulky tool
 * results that are already in the cache and surfaced in the index.
 *
 * This module is INERT until called. Production renderers can opt in via
 * `applyCodeMapStubbing(messages, { currentTurnId })`. Existing flows
 * are unaffected.
 *
 * Stub format (descriptive, NOT prescriptive — minimal-format rule):
 *   [code-map skeleton src/foo.ts — cached; see ## CODEBASE KNOWLEDGE]
 *
 * IDEMPOTENT — running this twice on the same message list produces
 * the same result. Safe to call on every render.
 */

/**
 * Detect a code-map tool-result message body so we can stub it.
 *
 * The agentScheduler's `formatToolResult` produces bodies that include
 * the toolId; we look for a small set of clear markers and refuse to
 * touch anything we're not certain is a code-map result.
 *
 * @param {string} content
 * @returns {{ filePath?: string, kind: 'skeleton'|'read-range'|null }}
 */
export function detectCodeMapResult(content) {
  if (typeof content !== 'string' || content.length === 0) {
    return { kind: null };
  }
  // We only stub when we see a clear "code-map" + ("skeleton" or "read-range") signature.
  // Substring checks only — no regex.
  const isCodeMap = content.includes('code-map') || content.includes('codeMap');
  if (!isCodeMap) return { kind: null };
  const isSkeleton = content.includes('skeleton');
  const isReadRange = content.includes('read-range') || content.includes('readRange');
  if (!isSkeleton && !isReadRange) return { kind: null };

  // Try to pull a filePath out of common shapes:
  //   filePath: src/foo.ts
  //   "filePath": "src/foo.ts"
  //   file: src/foo.ts
  // String-native (no regex): scan for the first occurrence and slice.
  let filePath = null;
  for (const marker of ['"filePath":', 'filePath:', '"file":', 'file:']) {
    const idx = content.indexOf(marker);
    if (idx === -1) continue;
    let rest = content.slice(idx + marker.length).trimStart();
    // Strip an opening quote if present.
    if (rest.startsWith('"')) rest = rest.slice(1);
    // Take up to the next quote, comma, newline, or space.
    let end = rest.length;
    for (const stop of ['"', ',', '\n', ' ', '\r']) {
      const i = rest.indexOf(stop);
      if (i !== -1 && i < end) end = i;
    }
    filePath = rest.slice(0, end).trim();
    if (filePath) break;
  }

  return {
    filePath: filePath || null,
    kind: isSkeleton ? 'skeleton' : 'read-range',
  };
}

/**
 * Build the stub text for a detected code-map result.
 * Descriptive only — no prescriptive prose (per the bait-avoidance rule).
 */
export function buildStub({ filePath, kind }) {
  const where = filePath ? ` ${filePath}` : '';
  return `[code-map ${kind}${where} — cached; see \`## CODEBASE KNOWLEDGE\`]`;
}

/**
 * Rewrite the `content` field of prior-turn code-map tool-result messages
 * to a stub. The CURRENT turn's tool results are left untouched so the
 * agent can act on what it just fetched.
 *
 * @param {Array<object>} messages - conversation messages
 * @param {object} opts
 * @param {string|number|null} [opts.currentTurnId] - identifier of the
 *   currently-active turn. Messages with this turnId (matched by
 *   `responseTurnId` or `_turnId` if present) are NOT stubbed.
 *   If null/undefined, the LAST message in the array is considered
 *   "current" and is preserved verbatim.
 * @returns {Array<object>} new array — original input is not mutated
 */
export function applyCodeMapStubbing(messages, opts = {}) {
  if (!Array.isArray(messages) || messages.length === 0) return messages;
  const { currentTurnId = null } = opts;

  // If no currentTurnId was given, fall back to "preserve the last
  // tool-result message" — that's the freshest one the agent just got.
  let lastToolResultIdx = -1;
  if (currentTurnId == null) {
    for (let i = messages.length - 1; i >= 0; i--) {
      if (messages[i]?.type === 'tool-result') { lastToolResultIdx = i; break; }
    }
  }

  return messages.map((msg, idx) => {
    if (!msg || typeof msg !== 'object') return msg;
    if (msg.type !== 'tool-result') return msg;

    // Already stubbed (from a prior render pass) — leave as-is so we
    // don't re-stub the stub and clobber `_originalContent`.
    if (msg._stubbed === true) return msg;

    // Skip the current turn.
    if (currentTurnId != null) {
      const tid = msg.responseTurnId || msg._turnId;
      if (tid === currentTurnId) return msg;
    } else if (idx === lastToolResultIdx) {
      return msg;
    }

    const detection = detectCodeMapResult(msg.content);
    if (detection.kind == null) return msg;

    const stub = buildStub(detection);
    // Preserve the original content under a side field so persistence /
    // replay / audit aren't lossy. The rendered field is what goes to
    // the LLM.
    return {
      ...msg,
      _originalContent: msg.content,
      content: stub,
      _stubbed: true,
    };
  });
}
