'use strict';

// OpenAI Codex CLI approval prompt provider.
// Detection signals based on codex-rs/tui/src/bottom_pane/approval_overlay.rs.

const QUESTION_RE = /Would you like to (run the following command|make the following edits|grant these permissions)|Do you want to approve network access|.+ needs your approval\./;
const ANCHOR_RE = /Press enter to confirm or esc to cancel/;
const YES_RE = /^\s*[❯›> ]?\s*1\.\s*Yes\b/i;
const OPTION_RE = /^\s*[›❯> ]?\s*(\d+)\.\s*(Yes|No)\b/i;
const REASON_RE = /^Reason:\s*(.*)$/i;
const APPROVE_ALL_RE = /don't ask again|for this session|for these files|allow this host|for this conversation|grant these permissions for this session|always allow/i;
const SHORTCUT_RE = /\(([a-z])\)\s*$/i;

function extractShortcut(line, fallback = null) {
  const match = String(line || '').match(SHORTCUT_RE);
  return match ? match[1].toLowerCase() : fallback;
}

module.exports = {
  id: 'codex',
  name: 'OpenAI Codex',

  detect(text) {
    const allLines = text.split('\n');
    const nonEmpty = allLines.filter(l => l.trim());
    if (nonEmpty.length < 3) return false;

    // Anchor: "Press enter to confirm or esc to cancel" in last 5 non-empty lines
    const tail = nonEmpty.slice(-5);
    const hasAnchor = tail.some(l => ANCHOR_RE.test(l));
    if (!hasAnchor) return false;

    // Must have either a Yes option or a question
    const hasYes = nonEmpty.some(l => YES_RE.test(l));
    const hasQuestion = nonEmpty.some(l => QUESTION_RE.test(l));

    return hasYes || hasQuestion;
  },

  // parse(text) is tolerant of "viewport-overflow" Codex prompts. A typical
  // Codex approval has [question] / [reason] / [command block] / [option list] /
  // [anchor] stacked top-to-bottom, but a multi-line command (heredoc, diff, etc.)
  // can push the question + reason + command above the captured window. When
  // that happens we still parse the option list + anchor and emit a partial
  // context (toolName='Bash command', empty command/warning) so the upstream
  // approver can decide based on its general rules. Returning null here would
  // silently drop the prompt — which is exactly the bug this guards against.
  parse(text) {
    const lines = text.split('\n').map(l => l.trim()).filter(Boolean);

    // questionIdx may stay -1 for tall prompts whose question scrolled out.
    let questionIdx = -1;
    for (let i = lines.length - 1; i >= 0; i--) {
      if (QUESTION_RE.test(lines[i])) { questionIdx = i; break; }
    }

    // Locate the Yes option. When the question is visible we anchor the
    // search to it; otherwise we scan the whole captured text — for very
    // tall prompts (e.g. node heredoc) the question may have aged out of
    // scrollback while the option list is still on screen.
    let firstYesIdx = -1;
    const yesStart = questionIdx >= 0 ? questionIdx + 1 : 0;
    const yesEnd = questionIdx >= 0
      ? Math.min(questionIdx + 80, lines.length)
      : lines.length;
    for (let i = yesStart; i < yesEnd; i++) {
      if (YES_RE.test(lines[i])) { firstYesIdx = i; break; }
      if (questionIdx >= 0 && ANCHOR_RE.test(lines[i])) break;
    }
    if (firstYesIdx < 0) return null;

    // When the question scrolled off, require the anchor as a second
    // signal so we don't accidentally fire on stale or partial widgets.
    if (questionIdx < 0 && !lines.some(l => ANCHOR_RE.test(l))) return null;

    // Default toolName when scrolled off. Codex shell-exec ("Would you like
    // to run the following command?") is the dominant approval path, so this
    // is the safest guess for caller rule-matching. A wrong default still
    // produces a parsed context (better than null), letting blocklists and
    // safety heuristics inspect whatever header text survived in fullContext.
    let toolName = questionIdx < 0 ? 'Bash command' : 'Unknown';
    if (questionIdx >= 0) {
      const q = lines[questionIdx];
      if (/run the following command/.test(q)) toolName = 'Bash command';
      else if (/make the following edits/.test(q)) toolName = 'Edit';
      else if (/grant these permissions/.test(q)) toolName = 'Permissions';
      else if (/approve network access/.test(q)) toolName = 'Network';
      else if (/needs your approval\./.test(q)) toolName = 'MCP';
    }

    // Codex renders the header as:
    //   [question]
    //   Reason: ...
    //   $ command / diff / permission summary
    // so reason must be extracted from the post-question header block, not only
    // from lines above the question.
    let warning = '';
    const headerLines = [];
    for (let i = questionIdx + 1; i < firstYesIdx; i++) {
      const line = lines[i];
      if (ANCHOR_RE.test(line)) break;
      if (!warning && REASON_RE.test(line)) {
        warning = line;
        continue;
      }
      headerLines.push(line);
    }

    // Backward-compat fallback for earlier synthetic fixtures that placed
    // Reason above the question.
    if (!warning) {
      for (let i = questionIdx - 1; i >= Math.max(0, questionIdx - 5); i--) {
        if (REASON_RE.test(lines[i])) {
          warning = lines[i];
          break;
        }
      }
    }

    // Prefer the real post-question header block. If it is unexpectedly empty,
    // fall back to command lines above the question for compatibility.
    const cmdLines = [];
    for (const line of headerLines) {
      if (!line || REASON_RE.test(line)) continue;
      cmdLines.push(line.replace(/^\$\s*/, ''));
    }
    if (cmdLines.length === 0) {
      for (let i = questionIdx - 1; i >= Math.max(0, questionIdx - 15); i--) {
        const line = lines[i];
        if (/^\$\s/.test(line)) {
          cmdLines.unshift(line.replace(/^\$\s*/, ''));
        } else if (cmdLines.length > 0) {
          break;
        }
      }
    }
    const command = cmdLines.join('\n').trim();

    const approveShortcut = extractShortcut(lines[firstYesIdx], 'y');
    let approveAllShortcut = null;
    let hasAllowAll = false;
    for (let i = firstYesIdx + 1; i < Math.min(firstYesIdx + 8, lines.length); i++) {
      const line = lines[i];
      if (!OPTION_RE.test(line)) continue;
      if (/^\s*[›❯> ]?\s*1\./.test(line)) continue;
      if (!/\bYes\b/i.test(line)) continue;
      if (!APPROVE_ALL_RE.test(line)) continue;
      hasAllowAll = true;  // Found allow-all option
      approveAllShortcut = extractShortcut(line, null);
      break;
    }

    // Build full context — anchor on the question if visible, otherwise on
    // the Yes option line so the snippet is still meaningful when the
    // prompt has scrolled.
    const ctxAnchor = questionIdx >= 0 ? questionIdx : firstYesIdx;
    const ctxStart = Math.max(0, ctxAnchor - 5);
    const ctxEnd = Math.min(lines.length, ctxAnchor + 10);
    const fullContext = lines.slice(ctxStart, ctxEnd).join('\n');

    return {
      toolName,
      command: command.slice(0, 2000),
      warning,
      fullContext: fullContext.slice(0, 2000),
      hasAllowAll,
      approveShortcut,
      approveAllShortcut,
      providerId: 'codex',
    };
  },

  approveKeystroke(ctx) {
    // If allow-all option exists, prefer it (use shortcut if available, else '2')
    if (ctx.hasAllowAll) {
      return ctx.approveAllShortcut || '2';
    }
    return ctx.approveShortcut || 'y';
  },

  approveAllKeystroke(ctx) {
    return ctx.approveAllShortcut || null;
  },

  requiresEnter: false,
};
