'use strict';
// Claude Code state detector.
// Claude Code emits a lot of cursor-addressed redraws (Ink rewriting the same
// lines every tick), but during long tool/thinking turns its meaningful task
// progress frames can be many seconds apart. The detector still suppresses
// spinner/footer-only redraws below; this debounce only stretches activity after
// meaningful frames so the sidebar does not flap idle between sparse updates.

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

const CLAUDE_STATUS_FRAGMENT_RE = /^(?:[✻✳✢✶◐◓◑◒⏺●○■□✔✓]+|Run|Runn|Runni|Runnin|Running\.{0,3}|unning|Thinking\.{0,3}|\(?ctrl\+o\s*to\s*expand\)?|>?\s*esc\s+to\s+interrupt|[⏵>]+\s*(?:accept edits|auto mode)\s+on\s+\(shift\+tab\s+to\s+cycle\)(?:\s*·\s*esc\s+to\s+interrupt)?(?:\s*·\s*ctrl\+t\s+to\s+(?:show|hide)\s+task)?|ctrl\+t\s+to\s+show\s+tasks?|\?\s+for\s+shortcuts|Native installation exists but .*? is not in your PATH\. Run:|echo\s+['"]export\s+PATH=.*)$/i;
const CLAUDE_PROMPT_LINE_RE = /^(?:›|❯|>)\s*(?:\S.*)?$/;
const CLAUDE_IDLE_FOOTER_LINE_RE = /(?:accept edits|auto mode)\s+on\s+\(shift\+tab\s+to\s+cycle\)|\besc\s+to\s+interrupt\b|\bctrl\+t\s+to\s+(?:show|hide)\s+tasks?\b|\bctrl\+o\s+to\s+expand\b|\?\s+for\s+shortcuts/i;
const CLAUDE_WORKED_LINE_RE = /^[*•]?\s*Worked for\s+\d/i;
const CLAUDE_PASSIVE_BORDER_LINE_RE = /^[\s─━═│┃┌┐└┘├┤┬┴┼╭╮╰╯]+$/u;

function isClaudeRedraw(data) {
  const s = String(data || '');
  return (
    /\x1b\[[0-9]+A/.test(s) && /\x1b\[[0-9;?]*J/.test(s)
  ) || (
    /\x1b\[[0-9]+;[0-9]+H/.test(s) && /\x1b\[[0-9;?]*[KJ]/.test(s)
  ) || /\x1b\[\?2026h/.test(s);
}

function stripAnsiPreserveLines(data) {
  return String(data || '')
    .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 isClaudePassivePromptLine(line) {
  const text = String(line || '').trim();
  if (!text) return true;
  return CLAUDE_PROMPT_LINE_RE.test(text) ||
    CLAUDE_IDLE_FOOTER_LINE_RE.test(text) ||
    CLAUDE_WORKED_LINE_RE.test(text) ||
    CLAUDE_STATUS_FRAGMENT_RE.test(text) ||
    CLAUDE_PASSIVE_BORDER_LINE_RE.test(text);
}

function isClaudeIdlePromptRedraw(data) {
  if (!isClaudeRedraw(data)) return false;
  const lines = printableLines(data);
  if (!lines.length) return false;
  const hasPrompt = lines.some(line => CLAUDE_PROMPT_LINE_RE.test(line));
  const hasFooter = lines.some(line => CLAUDE_IDLE_FOOTER_LINE_RE.test(line));
  if (!hasPrompt || !hasFooter) return false;
  return lines.every(isClaudePassivePromptLine);
}

// Live Claude approval/choice widget shapes. A real prompt highlights a
// selection row (❯ 1. Yes / ❯ Allow / ❯ Run) and/or asks an approval question
// with a confirm footer. These parallel approval-agent.js's LIVE_SELECTION_CURSOR_RE
// / LIVE_PROMPT_FOOTER_RE (which serve the auto-approver); kept here so the banner
// path's "at a live prompt?" answer comes from the provider detector, not from
// global content regexes tested over the whole tail.
// Leading box-frame border ("│ ❯ 1. …") — a menu rendered inside a bordered widget
// prefixes each row with a vertical rule. It is allowed (optional) before the cursor
// so a boxed AskUserQuestion reads like a borderless one; without it the menu rows
// fail to match and the whole widget classifies as `none`, stranding a stale banner.
const CLAUDE_BOX_BORDER = '(?:[│┃║▏▕|]\\s*)?';
const CLAUDE_SELECTION_CURSOR_RE = new RegExp('^\\s*' + CLAUDE_BOX_BORDER + '[❯▸›▶➤◆→]\\s*(?:\\d+[.)]\\s*)?(?:yes\\b|allow\\b|approve\\b|proceed\\b|accept\\b|run\\b|make edits|don\'?t ask|always)', 'i');
const CLAUDE_APPROVAL_QUESTION_RE = /\bDo you want to (?:proceed|make this edit|create|run|allow)\b/i;
const CLAUDE_CONFIRM_FOOTER_RE = /\benter to confirm\b|\besc to (?:cancel|reject|go back)\b|press enter to (?:confirm|continue)/i;
// Any numbered option row (cursor optional): "❯ 1. X", "  2. Y". Loose on its own —
// it also matches enumerated PROSE ("2. Double-stat (low): ..."), so it only counts
// toward a real menu alongside a highlight cursor or a menu nav footer (below).
const CLAUDE_NUMBERED_OPTION_RE = new RegExp('^\\s*' + CLAUDE_BOX_BORDER + '[❯▸›▶➤◆→]?\\s*\\d+[.)]\\s+\\S');
// A HIGHLIGHTED numbered option (selection cursor REQUIRED) — present in a real
// interactive menu ("❯ 1. Investigate"), absent from a prose findings list.
const CLAUDE_MENU_CURSOR_OPTION_RE = new RegExp('^\\s*' + CLAUDE_BOX_BORDER + '[❯▸▶➤◆→]\\s*\\d+[.)]\\s+\\S');
// A menu navigation footer ("Enter to select · ↑/↓ to navigate"): the other positive
// tell that the numbered rows are an interactive selection menu, not prose.
const CLAUDE_MENU_NAV_FOOTER_RE = /\bto\s+(?:select|navigate)\b|↑\s*\/\s*↓|↑\/↓/i;
// A yes/no-flavored option — the hallmark of a real yes/no APPROVAL menu (Yes / No /
// Allow / Deny / …) as opposed to an arbitrary multi-option CHOICE menu. The menu's
// OPTIONS, not its question text, decide approval-vs-choice: an AskUserQuestion-style
// menu can ask "...do you want to proceed?" (which matches the approval question regex)
// yet offer several non-yes/no answers, and it must read as a choice — otherwise the
// banner renders Approve/Deny (inject '1'/Esc) on it and the approval label path,
// which hunts for a command, comes back blank.
const CLAUDE_YESNO_OPTION_RE = /^\s*[❯▸›▶➤◆→]?\s*(?:\d+[.)]\s*)?(?:yes\b|no\b|allow\b|deny\b|approve\b|reject\b|proceed\b|cancel\b|accept\b|always\b|don'?t\b|keep planning\b)/i;
const CLAUDE_YN_RE = /\(Y\/n\)\s*$/i;
// The agent's free-text composer: a bare prompt line (›/❯/> with nothing typed).
// Its presence BELOW an approval/choice anchor is the real "idle, awaiting text"
// signal — the live approval widget REPLACES this composer. (The idle/working
// FOOTER — "esc to interrupt", "? for shortcuts" — can still be painted beneath a
// live approval, so footer presence alone must NOT veto. That blanket veto was the
// cause of approval cards never appearing live until a restart re-rendered a frame
// that happened to drop the footer from the captured tail.)
const CLAUDE_EMPTY_COMPOSER_RE = /^(?:›|❯|>)\s*$/;

// Negative gate: the idle/working composer footer (esc to interrupt, ? for
// shortcuts, accept edits on …) is on screen, which a live approval widget would
// have REPLACED — so the agent is at its ready composer, not blocked at a prompt.
function detectReadyComposer(text) {
  const lines = printableLines(text);
  if (!lines.length) return false;
  return lines.slice(-12).some(line => CLAUDE_IDLE_FOOTER_LINE_RE.test(line));
}

// "esc to interrupt" is the ONE footer phrase Claude shows exclusively while a
// turn is generating (or a tool/shell it spawned is still running) — the genuinely
// idle composer shows "? for shortcuts" / "auto mode on (shift+tab to cycle)" but
// never "esc to interrupt". So its presence in the tail is decisive evidence the
// agent is actively working, NOT idle awaiting input. server.js _idlePromptDetections
// uses this to suppress the bare-`❯`-composer 'input' idle-notify during a long,
// output-quiet tool turn (which otherwise mislabels the session "Needs You").
const CLAUDE_GENERATING_FOOTER_RE = /\besc\s+to\s+interrupt\b/i;
function detectActivelyGenerating(text) {
  const lines = printableLines(text);
  if (!lines.length) return false;
  return lines.slice(-12).some(line => CLAUDE_GENERATING_FOOTER_RE.test(line));
}

// Positive "blocked at a live approval/choice widget" signal. Returns
// {reason:'approval'|'choice'} only when a real widget is anchored in the active
// prompt region and the ready-composer footer is absent; null otherwise.
function detectWaitingInput(text) {
  const raw = String(text || '');
  if (!raw.trim()) return null;
  const lines = printableLines(raw);
  if (!lines.length) return null;
  const tail = lines.slice(-14);
  // Locate the anchors and the idle free-text composer by POSITION (closest to the
  // bottom = the active element). A widget is live only when it is not sitting above
  // a bare composer prompt (which would mean the agent already moved on and the
  // widget is stale scrollback). Footer text is intentionally NOT consulted here.
  let selIdx = -1, qIdx = -1, confIdx = -1, ynIdx = -1, composerIdx = -1;
  let numberedOptCount = 0, lastNumberedOptIdx = -1, hasYesNoOption = false;
  let hasMenuCursor = false, hasMenuNavFooter = false;
  for (let i = 0; i < tail.length; i++) {
    const line = tail[i];
    if (CLAUDE_SELECTION_CURSOR_RE.test(line)) selIdx = i;
    if (CLAUDE_APPROVAL_QUESTION_RE.test(line)) qIdx = i;
    if (CLAUDE_CONFIRM_FOOTER_RE.test(line)) confIdx = i;
    if (CLAUDE_YN_RE.test(line)) ynIdx = i;
    if (CLAUDE_EMPTY_COMPOSER_RE.test(line)) composerIdx = i;
    if (CLAUDE_MENU_NAV_FOOTER_RE.test(line)) hasMenuNavFooter = true;
    if (CLAUDE_NUMBERED_OPTION_RE.test(line)) {
      numberedOptCount += 1; lastNumberedOptIdx = i;
      if (CLAUDE_YESNO_OPTION_RE.test(line)) hasYesNoOption = true;
      if (CLAUDE_MENU_CURSOR_OPTION_RE.test(line)) hasMenuCursor = true;
    }
  }
  // A multi-option numbered menu whose options are NOT yes/no-flavored is a CHOICE,
  // even when its question matches the approval phrasing. A genuine yes/no approval
  // (Yes/No options) keeps hasYesNoOption true and stays an approval below. Require a
  // positive menu tell — a highlight cursor or a nav footer — so an enumerated PROSE
  // findings list ("2. ... 3. ... 4. ...") is NOT mistaken for an interactive menu.
  const multiChoiceMenu = numberedOptCount >= 2 && !hasYesNoOption &&
    (hasMenuCursor || hasMenuNavFooter);
  let approvalIdx = -1;
  if (selIdx >= 0 && (qIdx >= 0 || confIdx >= 0)) approvalIdx = Math.max(selIdx, qIdx, confIdx);
  else if (qIdx >= 0 && confIdx >= 0 && !multiChoiceMenu) approvalIdx = Math.max(qIdx, confIdx);
  else if (ynIdx >= 0 && !multiChoiceMenu) approvalIdx = ynIdx;
  // A bare composer BELOW the anchor ⇒ the widget is stale; the agent is idle at its
  // composer. No composer-below ⇒ the widget is the live, active prompt.
  if (approvalIdx >= 0 && !(composerIdx > approvalIdx)) return { reason: 'approval' };
  // Choice: a highlighted yes/no-style selection cursor (selIdx) OR a non-yes/no
  // numbered menu (anchored at its last option). Same composer-below staleness gate.
  const choiceIdx = selIdx >= 0 ? selIdx : (multiChoiceMenu ? lastNumberedOptIdx : -1);
  if (choiceIdx >= 0 && !(composerIdx > choiceIdx)) return { reason: 'choice' };
  return null;
}

function isClaudeStatusRedraw(data) {
  const stripped = stripAnsi(data).trim();
  return (
    stripped.length <= 160 &&
    CLAUDE_STATUS_FRAGMENT_RE.test(stripped) &&
    isClaudeRedraw(data)
  );
}

module.exports = {
  ...baseDetector,
  id: 'claude-code',
  idleDebounceMs: 15000,
  // Claude Code spinner uses ⠁⠂⠄⠈⠐⠠⡀⢀ braille dots + the "●" circle variants.
  // Base detector's TRIVIAL_CHUNK_RE already covers these.
  isActiveChunk(data) {
    if (!baseDetector.isActiveChunk(data)) return false;
    if (isClaudeIdlePromptRedraw(data) || isClaudeStatusRedraw(data)) return false;
    return true;
  },
  isStatusOnlyChunk(data) {
    return isClaudeIdlePromptRedraw(data) || isClaudeStatusRedraw(data);
  },
  isBusyStatusChunk() {
    return false;
  },
  isClaudeIdlePromptRedraw,
  detectWaitingInput,
  detectReadyComposer,
  detectActivelyGenerating,
  _test: {
    printableLines,
    isClaudePassivePromptLine,
  },
};
