'use strict';

// Canonical "what interactive state is this session in right now?" evaluator.
//
// One classifier, used by the live card path, the warm reconcile, and restart, so
// they can never disagree (the historical cause of "the approval card only shows
// after a restart": four detectors with four gates, and only the restart path
// happened to re-evaluate a clean frame). It composes the existing building blocks:
//   - provider state detectors (workers/state-detectors) — detectWaitingInput is the
//     authoritative, position-aware approval/choice signal (footer text does NOT veto;
//     a bare composer below the widget does).
//   - terminal-choice — highlighted numbered/form selection menus.
//   - approval-agent — to derive a human label (the real command, not box junk).
//
// Pure: text in -> classification out. The live/stale discrimination that needs
// runtime knowledge (is the session output-quiet / not running?) is the CALLER's
// job (the reconcile only trusts this on a settled, non-running frame).

const { getStateDetector } = require('../workers/state-detectors');
const terminalChoice = require('./terminal-choice');
let approvalAgent = null;
try { approvalAgent = require('../approval-agent'); } catch { approvalAgent = null; }

// FNV-1a over a string — stable, dependency-free fingerprint for set/clear/dismissal.
function _fnv1a(str) {
  let hash = 0x811c9dc5;
  const s = String(str || '');
  for (let i = 0; i < s.length; i += 1) {
    hash ^= s.charCodeAt(i);
    hash = Math.imul(hash, 0x01000193) >>> 0;
  }
  return hash.toString(16).padStart(8, '0');
}

function _approvalLabel(providerId, viewportText, rawTail) {
  try {
    if (approvalAgent && typeof approvalAgent.escalationCommandParts === 'function') {
      const parts = approvalAgent.escalationCommandParts(rawTail || viewportText, providerId);
      const title = parts && (parts.title || parts.command);
      if (title && String(title).trim()) return String(title).trim().slice(0, 200);
    }
  } catch { /* label is best-effort */ }
  return '';
}

function _stripAnsiLocal(s) {
  return String(s || '').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
}

// A numbered option row of a selection menu: optional highlight cursor, a number,
// then "." or ")", then text. e.g. "❯ 1. Kimi / Moonshot", "  2. DeepSeek".
const _CHOICE_OPTION_ROW_RE = /^[>❯▸›▶◉○\s]*\d+[.)]\s+\S/;
// Header rows that are NOT the question: box borders/dividers, the AskUserQuestion
// tab row ("Model  Approach  Submit →"), keyboard-hint footers, "Read N file" notes.
const _CHOICE_HEADER_JUNK_RE = /^(?:[\s─━═│┃┌┐└┘├┤┬┴┼╭╮╰╯▀▄█▌▐░▒▓•·*\-_=.]+|(?:esc to|press enter|enter to|tab\b|tab to|arrow keys?|ctrl\+|shift\+|↑|↓|↵).*|read\s+\d+\s+files?\b.*|.*\bto\s+(?:select|navigate|cancel|submit|expand)\b.*|.*\b(?:submit|next|back)\s*[→›▶].*)$/i;

// Derive a human label for a `choice` prompt (e.g. a Claude AskUserQuestion or a
// numbered selection menu). evaluateWaitState only knows the option SET, not the
// QUESTION, so the banner read blank/divider for choices. We recover the question
// text from the rows ABOVE the options: take the question line nearest the options
// (the one with a "?"), reassembling any wrapped continuation rows, and clip at the
// "?". Falls back to the last meaningful header line, then "". Pure: text -> label.
function deriveChoiceLabel(viewportText) {
  // Strip surrounding box-frame borders ("│ How broadly…? │") per line so a boxed
  // widget's question is recovered cleanly (and its option rows match below), the
  // same normalization terminal-choice applies to option rows.
  const lines = _stripAnsiLocal(viewportText).split('\n')
    .map((l) => l.replace(/\s+$/, '').replace(/^[│┃║▏▕|]\s?/, '').replace(/\s?[│┃║▏▕|]$/, ''));
  let firstOpt = lines.length;
  for (let i = 0; i < lines.length; i += 1) {
    if (_CHOICE_OPTION_ROW_RE.test(lines[i])) { firstOpt = i; break; }
  }
  const header = lines.slice(0, firstOpt).map((l) => l.trim());
  // The question is the "?"-bearing line closest to the options (scan upward), so a
  // "?" in the agent's analysis prose higher up never wins over the real prompt.
  let qIdx = -1;
  for (let i = header.length - 1; i >= 0; i -= 1) {
    if (header[i].includes('?') && !_CHOICE_HEADER_JUNK_RE.test(header[i])) { qIdx = i; break; }
  }
  if (qIdx >= 0) {
    // Reassemble wrapped continuation rows: walk up while the previous row is prose
    // that did not already end a sentence (so a long question split across terminal
    // rows is rejoined), stopping at a junk/divider/tab row or a sentence boundary.
    let start = qIdx;
    while (start > 0) {
      const prev = header[start - 1];
      if (!prev || _CHOICE_HEADER_JUNK_RE.test(prev) || /[.?!:]$|[→›▶]$/.test(prev)) break;
      start -= 1;
    }
    const para = header.slice(start, qIdx + 1).join(' ').replace(/\s+/g, ' ').trim();
    const cut = para.indexOf('?');
    const q = (cut >= 0 ? para.slice(0, cut + 1) : para).trim();
    if (q.length > 1) return q.slice(0, 120);
  }
  // No question mark: the last meaningful (non-junk) header line is the best label.
  for (let i = header.length - 1; i >= 0; i -= 1) {
    const t = header[i];
    if (t && !_CHOICE_HEADER_JUNK_RE.test(t)) return t.replace(/\s+/g, ' ').slice(0, 120);
  }
  return '';
}

const NONE = Object.freeze({ kind: 'none', live: false, label: '', fingerprint: '', gateReason: '' });

// `viewportText` = the settled headless viewport (authoritative). `rawTail` = a wider
// ANSI-stripped buffer, used ONLY to recover a label for a command box clipped by the
// fixed headless grid — never for liveness (it can contain stale scrollback).
function evaluateWaitState({ providerId, viewportText, rawTail } = {}) {
  const text = String(viewportText || '');
  if (!text.trim()) return NONE;

  const detector = getStateDetector(providerId);
  let wi = null;
  try { wi = typeof detector.detectWaitingInput === 'function' ? detector.detectWaitingInput(text) : null; } catch { wi = null; }
  const reason = wi && wi.reason;

  // 1) A real approval (Yes/No + "Do you want to…"/confirm) is first-class — it must
  //    win over the generic menu detector, which would otherwise classify the Yes/No
  //    selection as a plain choice. Position-aware in the detector (footer text does
  //    NOT veto; a bare composer below the widget does).
  if (reason === 'approval') {
    const label = _approvalLabel(providerId, text, rawTail);
    return { kind: 'approval', live: true, label, fingerprint: 'approval:' + _fnv1a(label || text.slice(-400)), gateReason: '' };
  }

  // 2) Idle composer awaiting free text, or a finished turn — not a blocker. Returning
  //    here also stops the menu detector from resurrecting a stale selection list that
  //    sits above the now-active composer.
  if (reason === 'input') return { kind: 'input', live: false, label: '', fingerprint: '', gateReason: '' };
  let finished = false;
  try { finished = typeof detector.detectTurnFinished === 'function' && !!detector.detectTurnFinished(text); } catch { finished = false; }
  if (finished) return { kind: 'finished', live: false, label: '', fingerprint: '', gateReason: '' };

  // 3) Highlighted numbered / form selection menu (e.g. Claude AskUserQuestion).
  try {
    const menu = terminalChoice.extractTerminalChoice(text);
    if (menu && (menu.kind === 'numbered' || menu.kind === 'form')) {
      return { kind: 'choice', live: true, label: deriveChoiceLabel(text), fingerprint: 'choice:' + menu.fingerprint, gateReason: '' };
    }
  } catch { /* fall through */ }

  // 4) Provider-reported choice (selection cursor without a generic menu shape).
  if (reason === 'choice') {
    return { kind: 'choice', live: true, label: deriveChoiceLabel(text), fingerprint: 'choice:' + _fnv1a(text.slice(-400)), gateReason: '' };
  }
  return NONE;
}

// Decide whether the warm reconcile should CLEAR an existing approval/choice banner
// when the settled live frame no longer classifies as approval/choice. Reconcile is
// the single clearer, but it must clear ONLY with positive "moved-on" evidence — a
// genuine escalation whose live widget the provider detector can't structurally
// confirm (Codex's detectWaitingInput is narrower than the approver's gate) must NOT
// be dismissed while it is still on screen. Pure so it can be unit-tested.
//   - movedOn: the detector saw the ready free-text composer (input) or a finished
//     turn — a live widget would have replaced these, so the prompt is gone.
//   - freshOutput: the agent rendered substantial output AFTER the card was raised —
//     a blocked agent emits nothing until the prompt resolves, so it moved past it.
//   - reconcileOwnsAndSeesNothing: a reconcile-raised card whose own detector now
//     sees nothing is authoritative for itself; an escalation-owned `none` is kept.
function shouldClearWaitCard({ wsKind, prevOwner, lastSignificantOutput, raisedAt } = {}) {
  const movedOn = wsKind === 'input' || wsKind === 'finished';
  const freshOutput = Number(lastSignificantOutput || 0) > Number(raisedAt || 0);
  const reconcileOwnsAndSeesNothing = prevOwner === 'reconcile' && wsKind === 'none';
  const clear = !!(movedOn || freshOutput || reconcileOwnsAndSeesNothing);
  const reason = !clear ? ''
    : movedOn ? `moved-on:${wsKind}`
    : freshOutput ? 'fresh-output'
    : 'reconcile-owned-none';
  return { clear, reason };
}

// Reconcile, while a frame is still repainting (inside the quiet-gate), must drop a
// stale ESCALATION-owned banner the moment the agent moves to a choice/question menu:
// it is now asking the USER to pick, not awaiting the escalated command's approval, so
// the old "Approval needed: …" banner (with the verifier's free-text reasoning) is
// moot and must not linger on top of the question. A navigated menu keeps output
// flowing, so the settled-frame choice branch may never run while the user interacts
// with it — hence this in-quiet-window supersede. Pure so server and tests share it.
function shouldSupersedeEscalation({ prevOwner, wsKind } = {}) {
  return prevOwner === 'escalation' && wsKind === 'choice';
}

module.exports = { evaluateWaitState, _fnv1a, shouldClearWaitCard, deriveChoiceLabel, shouldSupersedeEscalation };
