// Strips <think>...</think> reasoning blocks that some model backends emit
// on stdout before their final answer. These are internal reasoning traces
// and should never reach the visible terminal. The tags/content can be split
// across multiple PTY output chunks, so state (whether we're currently
// "inside" a think block, plus a small carry buffer for tags split at a
// chunk boundary) must persist across calls for a given PTY session.

export interface ThinkTagState {
  inside: boolean;
  carry: string;
}

export function createThinkTagState(): ThinkTagState {
  return { inside: false, carry: '' };
}

const OPEN_TAG = '<think>';
const CLOSE_TAG = '</think>';
const MAX_TAG_LEN = Math.max(OPEN_TAG.length, CLOSE_TAG.length);

// Returns how many trailing characters of `s` must be held back because they
// are a genuine prefix of an (as-yet-incomplete) OPEN_TAG/CLOSE_TAG straddling
// a chunk boundary. Unconditionally holding back the last MAX_TAG_LEN-1
// characters of every chunk (regardless of whether they resemble a tag) was
// the previous behaviour and is WRONG: most PTY output arrives as many small
// chunks (e.g. one per console.log line) with no tag anywhere near the end,
// so that unconditional holdback silently delayed and re-spliced trailing
// characters of one line into the middle of the next line on every single
// chunk boundary -- corrupting box-drawing borders and wrapped help text with
// disconnected/shifted fragments even though no <think> tag was ever present.
function safeHoldbackLength(s: string): number {
  const maxLen = Math.min(MAX_TAG_LEN - 1, s.length);
  for (let len = maxLen; len > 0; len--) {
    const suffix = s.slice(s.length - len);
    if (OPEN_TAG.startsWith(suffix) || CLOSE_TAG.startsWith(suffix)) {
      return len;
    }
  }
  return 0;
}

export function stripThinkTagsFromChunk(chunk: string, state: ThinkTagState): string {
  const combined = state.carry + chunk;
  let out = '';
  let i = 0;

  while (i < combined.length) {
    if (!state.inside) {
      const openIdx = combined.indexOf(OPEN_TAG, i);
      if (openIdx === -1) {
        // No open tag in the remainder. Only hold back a tail if it could
        // genuinely be the start of an open/close tag split across this
        // chunk and the next one -- otherwise emit everything immediately.
        const remainder = combined.slice(i);
        const holdLen = safeHoldbackLength(remainder);
        const safeEnd = combined.length - holdLen;
        out += combined.slice(i, safeEnd);
        state.carry = combined.slice(safeEnd);
        return out;
      }
      out += combined.slice(i, openIdx);
      i = openIdx + OPEN_TAG.length;
      state.inside = true;
    } else {
      const closeIdx = combined.indexOf(CLOSE_TAG, i);
      if (closeIdx === -1) {
        // Still inside a think block; swallow everything found so far but
        // keep a small tail in carry in case the close tag is split. Nothing
        // here reaches `out`, so holding back is always safe (no visible
        // corruption risk), unlike the not-inside branch above.
        const remainder = combined.slice(i);
        const holdLen = safeHoldbackLength(remainder);
        const safeEnd = combined.length - holdLen;
        state.carry = combined.slice(safeEnd);
        return out;
      }
      i = closeIdx + CLOSE_TAG.length;
      state.inside = false;
    }
  }

  state.carry = '';
  return out;
}
