'use strict';
// Base / default state detector.
//
// Per-CLI state detectors express how a provider's terminal output maps to
// the idle/busy/waiting_input state machine. Each provider can declare its
// own `stateDetector` in providers/<id>.js; if none is declared, the base
// detector below is used.
//
// Contract expected by lib/session-state-bus and its callers:
//   - id: string identifier
//   - idleDebounceMs: how long of no output before we declare idle
//   - isActiveChunk(data): does this chunk represent real activity?
//     (e.g. the claude detector ignores spinner-only frames)
//   - filterOutput(data): optionally pre-process data before activity check
//
// A detector MAY also opt into custom waiting-input detection by providing
// a `detectWaitingInput(text)` hook, but most providers get that for free
// via providers/<id>.detect() which is already wired into approval-agent.
//
// A detector MAY also provide `detectTurnFinished(text)`: positive evidence the
// agent returned to its ready prompt (e.g. a TUI repainting its idle composer
// with no in-progress status line). Distinct from detectWaitingInput — finished
// means Idle (nothing needed), not Needs-You. Hookless TUIs like Codex rely on
// this to close their active turn; without it a finished session would ride the
// long open-turn "running" hold. server.js _idlePromptDetections consumes it.

const DEFAULT_IDLE_DEBOUNCE_MS = 1500;

// Spinner glyphs + box-drawing chars seen in most modern TUIs. When a chunk
// is ONLY these (no substantive content), treat it as non-activity so the
// idle debounce can still fire.
const TRIVIAL_CHUNK_RE = /^[\s\x1b\[\]\d;A-Za-z⠁⠂⠄⠈⠐⠠⡀⢀\\|/\-─│┌┐└┘]*$/u;

function stripAnsi(data) {
  return String(data || '')
    // `<=>` covers private-parameter CSI like modifyOtherKeys "\x1b[>4;0m"
    // and kitty keyboard queries "\x1b[>7u" — without them the prefix byte
    // survives and "[>4;0m[>7u" reads as printable content.
    .replace(/\x1b\[[0-9;?<=>]*[a-zA-Z]/g, '')
    // CSI sequences with intermediate bytes (0x20-0x2F) before the final byte —
    // e.g. DECSCUSR cursor-shape "\x1b[0 q" (note the space). The rule above
    // requires the final byte immediately after the params, so it misses these
    // and leaves a stray "q"/"0 q" that reads as printable content (which made
    // composer-cursor repaints score as activity). Additive: only matches the
    // intermediate form the rule above cannot.
    .replace(/\x1b\[[0-9;?]*[ -/]+[@-~]/g, '')
    .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
    .replace(/[\x00-\x1f\x7f]/g, '');
}

const baseDetector = {
  id: 'base',
  idleDebounceMs: DEFAULT_IDLE_DEBOUNCE_MS,

  // A chunk is "active" if, after stripping control codes and trimming, it
  // contains more than a couple of printable characters. Keeps spinner
  // jitter from holding sessions in 'busy' forever.
  //
  // Threshold matches the existing activity detector in server.js
  // (> 2 stripped chars) — not negotiable without an end-to-end change.
  isActiveChunk(data) {
    if (!data) return false;
    const stripped = stripAnsi(data).trim();
    return stripped.length > 2;
  },

  // Passthrough — subclasses can override to e.g. strip mouse sequences.
  filterOutput(data) { return data; },
};

module.exports = { baseDetector, DEFAULT_IDLE_DEBOUNCE_MS, TRIVIAL_CHUNK_RE, stripAnsi };
