'use strict';

// Structural validation for approval prompts.
//
// Text-regex alone cannot distinguish a LIVE approval widget from text that
// happens to mention one (e.g. documentation pasted by the assistant, or
// scrollback from a previous prompt). This module inspects the xterm.js
// buffer cell-by-cell to detect structural properties a real widget always
// has and text mentions never do:
//
//   1. "1. Yes" line in BOTTOM N rows  (widget is at input position, not scrollback)
//   2. ANSI color or "❯" marker        (widget cells have attribute styling)
//
// Alt-screen is tracked as a soft signal but NOT required — after CTM restart,
// restored sessions' headless terminals lose alt-screen state even though the
// PTY is still in alt-screen mode.

// Bottom-row scan depth. Real Claude Code widgets occupy the last few rows.
const BOTTOM_ROW_SCAN = 10;

// When an anchor footer is found, scan up to this many rows above it for the
// Yes line. Codex's "Yes, and don't ask again ..." option can wrap to 10–15
// rows when the embedded command is long; 40 is comfortably wider while still
// bounding the work.
const ABOVE_ANCHOR_DEPTH = 40;

// Footer anchor that real widgets render at the bottom of the prompt.
// Claude Code: "Esc to cancel". Codex: "Press enter to confirm or esc to cancel".
// Cursor Agent: "Skip (esc or n)".
const ANCHOR_RE = /Esc to cancel|esc to cancel|Press enter to confirm|\(esc or n\)/;

// Approval-option pattern. Accepts an optional selection-arrow prefix in any of
// the forms different CLIs use: ❯ (Claude Code), ›/▶/▸ (Cursor/others), or
// plain ASCII > (Codex). Without this, Codex's "> 1. Yes, proceed (y)" would
// be skipped over and the validator would lock onto option 2 ("2. Yes, ...")
// — which is unstyled in Codex's renderer and trips no-widget-formatting.
const YES_RE = /^\s*(?:[❯›▶▸>]\s*)?\d+\.\s*(?:Yes|Allow)\b/i;

// Cursor Agent's "Run this command?" widget has no numbered options — the
// confirm row is a hotkey: "→ Run (once) (y)". Recognize it as the structural
// "Yes" row so the validator accepts Cursor prompts.
const CURSOR_RUN_RE = /^\s*(?:[▸❯›▶→>-]+\s*)?Run\s*\(once\)\s*\(y\)/i;

/**
 * Check if the terminal is currently displaying an active approval widget.
 *
 * @param {import('@xterm/headless').Terminal} term
 * @param {object} [opts]
 * @param {number} [opts.bottomRows=BOTTOM_ROW_SCAN]  How many bottom viewport rows to scan
 * @returns {{ valid: boolean, reason: string, yesRow?: number }}
 */
function validateWidget(term, opts = {}) {
  const bottomRows = opts.bottomRows || BOTTOM_ROW_SCAN;
  const buf = term.buffer.active;

  // Alt-screen is a SOFT signal, not a hard gate. Claude Code enters alt-screen
  // for its TUI, but after CTM restart (scrollback restore) or during certain
  // output phases, the headless terminal may not have alt-screen active even
  // though a real approval prompt is on screen. We track it for diagnostics
  // but don't reject on it — the structural checks below (Yes at bottom +
  // widget formatting) are sufficient to prevent false positives.
  const isAltScreen = term.buffer.active === term.buffer.alternate;

  // Phase 1 signal: "1. Yes" must be near the content cursor, not up in
  // scrollback. Real widgets are drawn at the bottom of the CLI's TUI alongside
  // an input cursor.
  //
  // Two scan modes (both deal with TUI quirks where cursorY is unreliable):
  //   (A) Anchor-based — when a footer like "Press enter to confirm or esc to
  //       cancel" or "Esc to cancel" is in the bottom 5 rows, scan UPWARD from
  //       the anchor up to ABOVE_ANCHOR_DEPTH rows. This handles widgets where
  //       multi-line option text (e.g. Codex's "Yes, and don't ask again ..."
  //       option that embeds the full command) pushes the Yes line many rows
  //       above the bottom — bottom-N alone is too narrow.
  //   (B) Last-non-blank fallback — when no footer is visible, scan the last
  //       `bottomRows` rows ending at the lowest non-blank row. This handles
  //       cursor-parked-high cases (input echo / partial redraws park cursorY
  //       mid-viewport while the widget is rendered below — see ctm.log
  //       gate-miss with widget at row 23, cursorY=5).
  //
  // Anchoring to cursorY breaks both shapes; the two modes above cover them.
  const rows = term.rows;

  let anchorRow = -1;
  for (let i = Math.max(0, rows - 5); i < rows; i++) {
    const line = buf.getLine(buf.viewportY + i);
    if (!line) continue;
    if (ANCHOR_RE.test(line.translateToString(true))) { anchorRow = i; break; }
  }

  let scanTop;
  let scanBottom;
  if (anchorRow >= 0) {
    scanBottom = anchorRow; // Yes must be ABOVE the anchor
    scanTop = Math.max(0, anchorRow - ABOVE_ANCHOR_DEPTH);
  } else {
    let lastContentRow = -1;
    for (let y = rows - 1; y >= 0; y--) {
      const line = buf.getLine(buf.viewportY + y);
      if (!line) continue;
      if (line.translateToString(true).trim()) { lastContentRow = y; break; }
    }
    if (lastContentRow < 0) {
      return { valid: false, reason: 'empty-viewport' };
    }
    scanBottom = lastContentRow + 1;
    scanTop = Math.max(0, scanBottom - bottomRows);
  }

  // Iterate top-to-bottom within the scan window so we find the PRIMARY
  // Yes option (numbered 1) before any "2. Yes, allow all" modifier option.
  let yesRow = -1;
  for (let i = scanTop; i < scanBottom; i++) {
    const line = buf.getLine(buf.viewportY + i);
    if (!line) continue;
    const text = line.translateToString(true);
    if (YES_RE.test(text) || CURSOR_RUN_RE.test(text)) { yesRow = i; break; }
  }
  if (yesRow < 0) {
    return { valid: false, reason: 'yes-not-at-bottom' };
  }

  // Phase 2 signal: widget formatting. A real widget has either:
  //   a) A "❯" selection marker in one of the bottom rows
  //   b) ANSI foreground color on the Yes-option line
  //
  // Documentation text in the main buffer is plain — no selection arrow,
  // no styling on the "1." text. This is the strongest discriminator.
  const hasWidgetFormatting = _hasWidgetFormatting(buf, yesRow, rows);
  if (!hasWidgetFormatting) {
    return { valid: false, reason: 'no-widget-formatting' };
  }

  return { valid: true, reason: isAltScreen ? 'ok' : 'ok-no-altscreen', yesRow };
}

/**
 * Inspect cell attributes to detect widget-like formatting.
 * Returns true if any cell in the bottom rows has a "❯" char OR
 * if the Yes-option line has at least one cell with foreground color.
 */
function _hasWidgetFormatting(buf, yesRow, totalRows) {
  // First check the selected Yes line itself. For tall Codex prompts, option 2
  // can wrap for many rows, pushing the selected "1. Yes" line well above the
  // bottom five rows while the footer remains at the bottom.
  const yesLine = buf.getLine(buf.viewportY + yesRow);
  if (!yesLine) return false;
  const yesText = yesLine.translateToString(true);
  if (/[❯›▶▸→]/.test(yesText)) return true;

  // Check for a selection marker near the approval options. Codex MCP forms can
  // select option 2 ("Allow for this session"), while option 1 is the first
  // approval-shaped line used for anchoring.
  for (let row = Math.max(0, yesRow - 1); row < Math.min(totalRows, yesRow + 8); row++) {
    const line = buf.getLine(buf.viewportY + row);
    if (!line) continue;
    const text = line.translateToString(true);
    // "❯" (U+276F), "›" (U+203A), "▶" (U+25B6), "→" (U+2192, Cursor), ">" spaced
    if (/[❯›▶▸→]/.test(text)) return true;
  }

  // Check for a selection marker anywhere in bottom 5 rows for prompts whose
  // option block is pushed down by long wrapped content.
  for (let row = Math.max(0, totalRows - 5); row < totalRows; row++) {
    const line = buf.getLine(buf.viewportY + row);
    if (!line) continue;
    const text = line.translateToString(true);
    if (/[❯›▶▸→]/.test(text)) return true;
  }

  // Check for ANSI foreground color on the Yes-option line.
  // xterm's BufferLine.getCell(x) returns an IBufferCell with .getFgColor()
  // (0 = default). Any non-default fg color = styled = widget.
  const lineLen = yesLine.length;
  for (let x = 0; x < lineLen; x++) {
    const cell = yesLine.getCell(x);
    if (!cell) continue;
    // getFgColorMode() returns 0 when default; any other mode = colored
    // getFgColor() returns -1 when default
    const fgColor = cell.getFgColor();
    const fgMode = cell.getFgColorMode();
    const bgColor = cell.getBgColor();
    const bgMode = cell.getBgColorMode();
    if ((fgMode !== 0 && fgColor !== -1) || (bgMode !== 0 && bgColor !== -1)) {
      return true;
    }
  }

  return false;
}

module.exports = { validateWidget, YES_RE, BOTTOM_ROW_SCAN };
