'use strict';
// Cursor (cursor-agent) TUI state detector.
//
// cursor-agent renders an interactive TUI: a prompt/composer region, a status
// footer (model + workspace), and a spinner/status line while the model works.
// Like OpenCode, it redraws those regions while sitting idle at the prompt, so
// those frames must paint but must NOT be treated as model activity. Real tool
// output, approval widgets, and "generating" status keep the session busy.
//
// The footer / prompt / busy regexes below are a grounded first cut; they are
// tuned against live cursor-agent output during the dev verification pass.
// Anything we don't recognize falls through to baseDetector, so an unmatched
// frame is treated as activity (fail-safe: never wrongly idle a working agent).

const { baseDetector } = require('./base');

// Footer: model id + workspace path (e.g. "composer-2.5 · ~/ws/tools").
const CURSOR_FOOTER_LINE_RE = /^(?:composer|gpt|claude|sonnet|opus|haiku|gemini|o\d|auto)[\w.+-]*(?:\s+[\w.+-]+){0,8}\s+[·|-]\s+(?:~|\/|\.\.?\/)/i;
// Newer Cursor Agent builds render an idle welcome/composer frame with title,
// version, help, model, and workspace lines split across rows. These are UI
// chrome, not agent output, and selecting an idle tab can replay exactly this
// frame.
const CURSOR_STATIC_HEADER_LINE_RE = /^Cursor Agent$/i;
const CURSOR_VERSION_LINE_RE = /^v\d{4}\.\d{2}\.\d{2}[-\w.]*$/i;
const CURSOR_IDLE_HELP_LINE_RE = /^Use\s+\/plan\b.*\bbefore code changes\.?$/i;
const CURSOR_IDLE_PLACEHOLDER_RE = /^(?:Plan,\s*)?search,\s*build anything$/i;
const CURSOR_MODEL_LINE_RE = /^Composer\s+\d+(?:\.\d+)*(?:[-\w.]*)?(?:\s+\w+){0,3}$/i;
const CURSOR_WORKSPACE_LINE_RE = /^(?:~|\/|\.\.?\/).+\s+[·|-]\s+[\w.+-]+$/;
// Composer prompt glyphs.
const CURSOR_PROMPT_LINE_RE = /^(?:>|›|❯|→|▌|\|)\s*(?:\S.*)?$/;
// Spinner/status while the model is producing output. cursor-agent shows a
// running verb plus an interrupt hint (esc/ctrl-c) while busy.
const CURSOR_BUSY_LINE_RE = /^(?:[•●◦▪·*⠿⠷⠯⠟⠻⠽⠾]\s*)?(?:(?:Waiting\s+for\s+background\s+terminal\b.*)|(?:Composing|Generating|Thinking|Working|Reading|Writing|Editing|Running|Searching|Planning|Analyzing|Calling|Using|Processing|Reasoning|Applying|Streaming)\b.*\b(?:esc|interrupt|cancel|stop)\b)/i;
// Passive status lines that should not hold the session busy.
const CURSOR_PASSIVE_STATUS_LINE_RE = /^(?:[•●◦▪·*]\s*)?(?:Ready|Waiting|Idle|Stopped|Done|Complete|Completed|Accept|Reject|Run everything|Allowlist)\b/i;
const CURSOR_CONTROL_HINT_RE = /^(?:esc|ctrl-c|ctrl\+c|tab|enter|press\s+enter)\b.*(?:interrupt|cancel|confirm|send|accept|reject|toggle)/i;

function stripAnsiPreserveLines(data) {
  return String(data || '')
    // Cursor-addressed redraws otherwise collapse rows together after stripping
    // CSI. Preserve row boundaries before removing control codes.
    .replace(/\x1b\[[0-9]+;[0-9]+[Hf]/g, '\n')
    .replace(/\x1b\[[0-9]+[ABCD]/g, '\n')
    .replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')
    .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
    .replace(/\r/g, '\n')
    .replace(/[\x00-\x09\x0b\x0c\x0e-\x1f\x7f]/g, '');
}

function printableLines(data) {
  return stripAnsiPreserveLines(data)
    .replace(/\u00a0/g, ' ')
    .split(/\n+/)
    .map(line => line.trim())
    .filter(Boolean);
}

function isCursorPassiveLine(line) {
  const text = String(line || '').trim();
  if (!text) return true;
  return CURSOR_FOOTER_LINE_RE.test(text) ||
    CURSOR_STATIC_HEADER_LINE_RE.test(text) ||
    CURSOR_VERSION_LINE_RE.test(text) ||
    CURSOR_IDLE_HELP_LINE_RE.test(text) ||
    CURSOR_IDLE_PLACEHOLDER_RE.test(text) ||
    CURSOR_MODEL_LINE_RE.test(text) ||
    CURSOR_WORKSPACE_LINE_RE.test(text) ||
    CURSOR_PROMPT_LINE_RE.test(text) ||
    CURSOR_PASSIVE_STATUS_LINE_RE.test(text) ||
    CURSOR_CONTROL_HINT_RE.test(text);
}

function isCursorBusyLine(line) {
  return CURSOR_BUSY_LINE_RE.test(String(line || '').trim());
}

function detectWaitingInput(text) {
  const lines = printableLines(text).slice(-10);
  if (!lines.length) return null;
  if (lines.some(isCursorBusyLine)) return null;
  if (!lines.some(line => CURSOR_PROMPT_LINE_RE.test(line))) return null;
  return { reason: 'input' };
}

// The agent is mid-turn when its busy/spinner line ("Generating… esc to interrupt")
// is on screen. server.js's generating-gate uses this to suppress the idle-notify so
// a long, output-quiet Cursor turn whose composer matches a global prompt pattern is
// not mislabeled "Needs You". Tail-scoped so a stale spinner up in scrollback doesn't
// count; same busy signal detectWaitingInput already gates on.
function detectActivelyGenerating(text) {
  return printableLines(text).slice(-12).some(isCursorBusyLine);
}

module.exports = {
  ...baseDetector,
  id: 'cursor',
  // Cursor redraws prompt/footer regions periodically; hold the provider only
  // for meaningful activity frames, mirroring the OpenCode debounce.
  idleDebounceMs: 10000,

  isActiveChunk(data) {
    if (!baseDetector.isActiveChunk(data)) return false;
    const lines = printableLines(data);
    if (lines.length && lines.every(line => isCursorPassiveLine(line) && !isCursorBusyLine(line))) {
      return false;
    }
    return true;
  },

  isStatusOnlyChunk(data) {
    const lines = printableLines(data);
    return !!(lines.length && lines.every(line => isCursorPassiveLine(line) || isCursorBusyLine(line)));
  },

  isBusyStatusChunk(data) {
    const lines = printableLines(data);
    return lines.some(isCursorBusyLine);
  },

  detectWaitingInput,
  detectActivelyGenerating,
  _test: {
    printableLines,
    isCursorPassiveLine,
    isCursorBusyLine,
  },
};
