/**
 * src/session/ring-buffer.ts (T6) — output scrollback for reconnect replay.
 *
 * M2 (the easy-to-get-wrong part). PTY output is an opaque ANSI byte stream; we
 * must NEVER cut a multi-byte UTF-8 codepoint or an ANSI escape sequence in half,
 * or the first screen on reconnect renders as garbage — exactly the vibe-coding
 * "refresh and the Claude TUI is still there" path. So:
 *
 *   - capacity is measured in REAL UTF-8 bytes (Buffer.byteLength), because
 *     `string.length` counts UTF-16 code units, not bytes;
 *   - eviction happens on whole append-chunk boundaries (drop the oldest chunk),
 *     so no chunk is ever sliced (the trade-off is the live size floats a little
 *     above/below maxBytes);
 *   - snapshot() prepends a soft-reset `\x1b[0m` so that even if the oldest
 *     surviving chunk started mid-attribute, the terminal is reset first.
 *
 * Implements the frozen `RingBuffer` contract in src/types.ts (read-only import).
 */

import type { RingBuffer } from '../types.js';

const SOFT_RESET = '\x1b[0m';

interface Chunk {
  readonly text: string;
  readonly bytes: number;
}

/**
 * Create a ring buffer holding at most ~`maxBytes` UTF-8 bytes of recent output.
 *
 * @param maxBytes capacity in real UTF-8 bytes (e.g. `cfg.scrollbackBytes`).
 */
export function createRingBuffer(maxBytes: number): RingBuffer {
  // Immutable-update style: each append produces a new chunk list / total rather
  // than mutating shared state in place (per coding-style immutability rule).
  let chunks: readonly Chunk[] = [];
  let totalBytes = 0;

  function append(chunk: string): void {
    if (chunk.length === 0) return; // nothing to store; keeps accounting clean

    const bytes = Buffer.byteLength(chunk, 'utf8');
    let nextChunks: Chunk[] = [...chunks, { text: chunk, bytes }];
    let nextTotal = totalBytes + bytes;

    // Evict whole oldest chunks until we fit — but never drop the only remaining
    // chunk, so a lone chunk larger than capacity is kept intact (cannot split).
    while (nextTotal > maxBytes && nextChunks.length > 1) {
      const oldest = nextChunks[0]!;
      nextChunks = nextChunks.slice(1);
      nextTotal -= oldest.bytes;
    }

    chunks = nextChunks;
    totalBytes = nextTotal;
  }

  function snapshot(): string {
    // Soft-reset first (M2 safety net), then the current buffer verbatim.
    return SOFT_RESET + chunks.map((c) => c.text).join('');
  }

  function tail(maxBytes: number): string {
    if (maxBytes <= 0) return '';
    // Collect whole chunks from the newest end until we have ~maxBytes — never
    // split a chunk (so no ANSI/UTF-8 sequence is cut, same rationale as M2).
    const picked: string[] = [];
    let acc = 0;
    for (let i = chunks.length - 1; i >= 0; i -= 1) {
      const c = chunks[i]!;
      picked.push(c.text);
      acc += c.bytes;
      if (acc >= maxBytes) break;
    }
    return picked.reverse().join('');
  }

  return { append, snapshot, tail };
}
