// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.

/**
 * trajectory.js — HEAD+TAIL context compression for long engagement sessions.
 *
 * Implements the HEAD+TAIL algorithm adapted from Hermes Agent's
 * trajectory_compressor.py. Keeps the most important turns intact and
 * compresses the middle, preserving coherence for LLM context injection.
 *
 * The algorithm:
 *   1. Protect HEAD — first N chunks (establish context, scope, early decisions)
 *   2. Protect TAIL — last M chunks (most recent state, current direction)
 *   3. Compress MIDDLE — replace with extractive summary (no LLM required)
 *   4. Only compress as much as needed to fit under the token target
 *
 * Key property: HEAD and TAIL are NEVER modified. The agent always sees:
 *   - Why the engagement started (HEAD)
 *   - Where it is now (TAIL)
 *   - A condensed bridge over everything in between (compressed MIDDLE)
 *
 * This is the correct approach vs uniform truncation, which silently
 * discards arbitrary middle content and can lose critical findings.
 *
 * @module memory/trajectory
 */

// ---------------------------------------------------------------------------
// Token estimation
// ---------------------------------------------------------------------------

/**
 * Rough token estimate for a string.
 * Rule of thumb: ~4 chars per token for English text with code mixed in.
 * This is intentionally conservative (overestimates) to avoid overflow.
 *
 * @param {string} text
 * @returns {number}
 */
export function estimateTokens(text) {
  if (!text) return 0;
  return Math.ceil(text.length / 4);
}

// ---------------------------------------------------------------------------
// Extractive summarizer for the middle section
// ---------------------------------------------------------------------------

/**
 * Extractive summary of middle chunks — no LLM required.
 *
 * Strategy:
 *   - Extract the first sentence from each chunk (high information density)
 *   - Include any line that starts with a markdown heading
 *   - Include any line containing a finding/result signal
 *   - Cap at targetTokens
 *
 * @param {string[]} chunks - Array of text chunks to summarize
 * @param {number} targetTokens - Max tokens for the summary
 * @returns {string}
 */
function extractiveSummary(chunks, targetTokens) {
  if (chunks.length === 0) return '';

  const FINDING_SIGNALS = [
    /^\s*#+\s/,                          // markdown headings
    /\bfound\b|\bdiscovered\b|\bidentified\b/i,
    /\bCVE-\d{4}/i,
    /\b(?:CRITICAL|HIGH|MEDIUM|LOW)\b/,
    /\bT\d{4}(?:\.\d{3})?\b/,           // MITRE technique IDs
    /\b(?:error|fail|success|complet)/i,
    /\bsummary\b|\bresult\b|\bconclusion\b/i,
    /^\s*[-*]\s/,                        // bullet points
  ];

  const lines = [];

  for (const chunk of chunks) {
    const chunkLines = chunk.split('\n');

    // Always take the first non-empty line of each chunk
    const firstLine = chunkLines.find(l => l.trim().length > 10);
    if (firstLine) lines.push(firstLine.trim());

    // Take any line that signals a finding or result
    for (const line of chunkLines.slice(1)) {
      const trimmed = line.trim();
      if (trimmed.length < 10) continue;
      if (FINDING_SIGNALS.some(re => re.test(trimmed))) {
        lines.push(trimmed);
      }
    }
  }

  // Deduplicate preserving order
  const seen = new Set();
  const deduped = lines.filter(l => {
    const key = l.slice(0, 80);
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });

  // Truncate to target token budget
  let result = '';
  for (const line of deduped) {
    const candidate = result ? result + '\n' + line : line;
    if (estimateTokens(candidate) > targetTokens) break;
    result = candidate;
  }

  return result;
}

// ---------------------------------------------------------------------------
// HEAD+TAIL compression
// ---------------------------------------------------------------------------

/**
 * Compress a list of context chunks using HEAD+TAIL protection.
 *
 * @param {string[]} chunks - Ordered list of context sections (e.g. task summaries)
 * @param {object} [opts]
 * @param {number} [opts.targetTokens=4000]   - Target token budget for all chunks combined
 * @param {number} [opts.headCount=2]          - Number of chunks to always protect at HEAD
 * @param {number} [opts.tailCount=3]          - Number of chunks to always protect at TAIL
 * @param {number} [opts.summaryTokens=400]    - Max tokens for the compressed MIDDLE summary
 * @param {string} [opts.summaryLabel]         - Label to prepend to the MIDDLE summary block
 * @returns {{ chunks: string[], compressed: boolean, originalCount: number, summaryTokens: number }}
 */
export function compressChunks(chunks, opts = {}) {
  const {
    targetTokens = 4000,
    headCount = 2,
    tailCount = 3,
    summaryTokens = 400,
    summaryLabel = '[Earlier context — summarized]',
  } = opts;

  if (!Array.isArray(chunks) || chunks.length === 0) {
    return { chunks: [], compressed: false, originalCount: 0, summaryTokens: 0 };
  }

  const originalCount = chunks.length;
  const totalTokens = chunks.reduce((sum, c) => sum + estimateTokens(c), 0);

  // Already fits — no compression needed
  if (totalTokens <= targetTokens) {
    return { chunks, compressed: false, originalCount, summaryTokens: 0 };
  }

  // Not enough chunks to bother protecting HEAD+TAIL separately
  if (chunks.length <= headCount + tailCount) {
    // Just truncate the last chunk to fit
    const truncated = [...chunks];
    while (
      truncated.length > 1 &&
      truncated.reduce((sum, c) => sum + estimateTokens(c), 0) > targetTokens
    ) {
      const last = truncated[truncated.length - 1];
      truncated[truncated.length - 1] = last.slice(0, Math.floor(last.length * 0.6)) +
        '\n[...truncated]';
    }
    return { chunks: truncated, compressed: true, originalCount, summaryTokens: 0 };
  }

  // Split into HEAD, MIDDLE, TAIL
  const head = chunks.slice(0, headCount);
  const tail = chunks.slice(chunks.length - tailCount);
  const middle = chunks.slice(headCount, chunks.length - tailCount);

  if (middle.length === 0) {
    // No middle to compress — truncate tail
    return { chunks: [...head, ...tail], compressed: true, originalCount, summaryTokens: 0 };
  }

  // How many tokens do HEAD+TAIL use?
  const headTokens = head.reduce((sum, c) => sum + estimateTokens(c), 0);
  const tailTokens = tail.reduce((sum, c) => sum + estimateTokens(c), 0);
  const budgetForMiddle = Math.max(0, targetTokens - headTokens - tailTokens - summaryTokens);

  // If the middle still fits in the remaining budget, keep it intact
  const middleTokens = middle.reduce((sum, c) => sum + estimateTokens(c), 0);
  if (middleTokens <= budgetForMiddle) {
    return { chunks, compressed: false, originalCount, summaryTokens: 0 };
  }

  // Compress the middle via extractive summarization
  const summary = extractiveSummary(middle, summaryTokens);
  const summaryBlock = summary
    ? `${summaryLabel}\n${summary}`
    : `${summaryLabel}\n(${middle.length} prior tasks — details omitted to fit context window)`;

  const result = [...head, summaryBlock, ...tail];
  return {
    chunks: result,
    compressed: true,
    originalCount,
    summaryTokens: estimateTokens(summaryBlock),
  };
}

// ---------------------------------------------------------------------------
// Convenience: compress a single long string by splitting on a delimiter
// ---------------------------------------------------------------------------

/**
 * Compress a single context string that contains multiple sections separated
 * by a delimiter (e.g. '---'). Useful for compressing `buildTaskContext` output.
 *
 * @param {string} text - Full context string
 * @param {object} [opts]
 * @param {string} [opts.delimiter='---']   - Section separator
 * @param {number} [opts.targetTokens=4000]
 * @param {number} [opts.headCount=2]
 * @param {number} [opts.tailCount=3]
 * @param {number} [opts.summaryTokens=400]
 * @returns {string} - Compressed context string
 */
export function compressContext(text, opts = {}) {
  const { delimiter = '---', ...rest } = opts;
  if (!text) return text;

  const sections = text.split(new RegExp(`\\n${delimiter}\\n`)).filter(s => s.trim());
  if (sections.length <= 1) return text;

  const { chunks } = compressChunks(sections, rest);
  return chunks.join(`\n${delimiter}\n`);
}
