'use strict';

// AI accuracy backstop for "what is this terminal frame?" — a prompt-INTENT classifier
// (approval / choice / none) that the server consults ONLY when the structural detectors
// conflict (assessAmbiguity), deduped per frame (frameFingerprint), with a pure hold
// decision (resolveIntentDecision) the server drives. The LLM call itself is
// classifyPromptIntent (added in Task 2). Pure parts here so they unit-test without I/O.

const { evaluateWaitState, _fnv1a } = require('./wait-state');
const terminalChoice = require('./terminal-choice');
const { callBackgroundLlm } = require('./background-llm');
let approvalAgent = null;
try { approvalAgent = require('../approval-agent'); } catch { approvalAgent = null; }

// Stable per-frame key shared by the reconcile and approver paths so a frame is
// classified ONCE regardless of which sees it first. Hash of the cleaned tail.
function frameFingerprint(viewportText) {
  const s = String(viewportText || '').replace(/[ \t]+$/gm, '');
  return 'intent:' + _fnv1a(s.slice(-600));
}

// First token of a real shell command. A parsed "command" whose head isn't a plausible
// binary/builtin (or that reads like prose) signals a low-confidence parse.
const _COMMAND_HEAD_RE = /^[\w][\w.\/-]*$/; // \w = [A-Za-z0-9_] — allow digit-leading binaries (1password, 2to3)
function _titleLooksLikeCommand(title) {
  const t = String(title || '').trim();
  if (!t) return false;
  // Prose markers a one-line command would not contain (sentence punctuation + filler).
  if (/[.?!]\s/.test(t) || /\b(but|and|the|allow-list|permission)\b/i.test(t)) return false;
  const head = t.split(/\s+/)[0] || '';
  return _COMMAND_HEAD_RE.test(head);
}

// Pure ambiguity assessment over a settled frame. Returns { ambiguous, reasons }.
// ambiguous:true ONLY when structural signals conflict / are low-confidence — a
// confident frame returns false and the AI is never called.
function assessAmbiguity({ providerId, viewportText } = {}) {
  const text = String(viewportText || '');
  const reasons = [];
  if (!text.trim()) return { ambiguous: false, reasons };

  const ws = evaluateWaitState({ providerId, viewportText: text });
  const liveApproval = !!(approvalAgent && typeof approvalAgent.isLiveApprovalPrompt === 'function'
    && approvalAgent.isLiveApprovalPrompt(text));

  // 1) Approver gate matched a live prompt, but the canonical evaluator sees no widget
  //    (none/input) — a shape one detector recognizes and the other does not.
  if (liveApproval && (ws.kind === 'none' || ws.kind === 'input')) {
    reasons.push('approver-live-but-evaluator-' + ws.kind);
  }

  // 2) Parsed an approval context whose derived title does not look like a command
  //    (Unknown tool or scraped prose) — low-confidence parse.
  if (approvalAgent && typeof approvalAgent.parseApprovalContext === 'function') {
    let ctx = null;
    try { ctx = approvalAgent.parseApprovalContext(text, providerId); } catch { ctx = null; }
    if (ctx) {
      let title = '';
      try { title = (approvalAgent.escalationCommandParts(ctx).title) || ''; } catch { title = ''; }
      if (ctx.toolName === 'Unknown' || !_titleLooksLikeCommand(title)) reasons.push('low-confidence-parse');
    }
  }

  // 3) A numbered menu with NON-yes/no options while an approval-style question is on
  //    screen — the approval<->choice tie (AskUserQuestion that reads like a prompt).
  let menu = null;
  try { menu = terminalChoice.extractTerminalChoice(text); } catch { menu = null; }
  if (menu && Array.isArray(menu.options) && menu.options.length >= 2) {
    const first = String((menu.options[0] && menu.options[0].label) || '').trim();
    const firstYesNo = /^(yes|no|allow|deny|approve|reject|proceed|cancel|accept)\b/i.test(first);
    const askApproval = /\bdo you want to\b/i.test(text) || /\?\s*$/m.test(text);
    if (!firstYesNo && askApproval) reasons.push('nonyesno-menu-with-approval-question');
  }

  return { ambiguous: reasons.length > 0, reasons };
}

const INTENT_SYSTEM = `You classify what an AI coding CLI's terminal screen is currently asking for. Reply with ONLY JSON: {"intent":"approval"|"choice"|"none","reason":"<=1 sentence"}.

Definitions:
- "approval": the CLI is asking PERMISSION to run a TOOL or SHELL COMMAND it proposed (e.g. "Do you want to proceed?" with Yes/No, an edit/command confirmation). Acting on it means pressing yes/approve.
- "choice": the AGENT is asking the USER to answer a question or pick among several non-yes/no options (e.g. an AskUserQuestion menu: "Which approach?" 1/2/3/4). There is nothing to auto-approve; the user must choose.
- "none": no live interactive prompt — the agent is working/thinking, or the screen is prose/output.

When unsure between "approval" and "choice", answer "choice" — never treat a user-facing question as an approval.`;

const INTENT_TIMEOUT_MS = 5000;

// Async LLM classification. Returns { intent:'approval'|'choice'|'none'|'unknown',
// reason, durationMs, error? }. ANY failure (disabled provider, timeout, bad JSON,
// bad value) -> intent:'unknown' so the caller falls back to the structural verdict.
async function classifyPromptIntent(viewportText, { reasons, goal, cwd } = {}, { callModel, timeoutMs } = {}) {
  const started = Date.now();
  const call = callModel || callBackgroundLlm;
  const screen = String(viewportText || '').replace(/[ \t]+$/gm, '').slice(-1600);
  const ctxLine = (goal || cwd)
    ? `Session goal: ${String(goal || '').slice(0, 200)}\nCwd: ${String(cwd || '').slice(0, 200)}\n`
    : '';
  const why = Array.isArray(reasons) && reasons.length ? `Detectors disagreed: ${reasons.join(', ')}.\n` : '';
  const prompt = `${ctxLine}${why}Terminal screen (bottom of viewport):\n"""\n${screen}\n"""\n\nReturn ONLY: {"intent":"approval|choice|none","reason":"..."}`;

  let response;
  try {
    response = await call(prompt, { system: INTENT_SYSTEM, maxTokens: 120, temperature: 0, timeoutMs: timeoutMs || INTENT_TIMEOUT_MS });
  } catch (e) {
    return { intent: 'unknown', reason: '', error: e.reason || e.message || String(e), durationMs: Date.now() - started };
  }
  const text = (response && (response.text ?? response)) || '';
  const match = String(text).match(/\{[\s\S]*\}/);
  if (!match) return { intent: 'unknown', reason: '', error: 'no JSON in response', durationMs: Date.now() - started };
  let parsed;
  try { parsed = JSON.parse(match[0]); } catch (e) { return { intent: 'unknown', reason: '', error: `parse: ${e.message}`, durationMs: Date.now() - started }; }
  const intent = ['approval', 'choice', 'none'].includes(parsed.intent) ? parsed.intent : 'unknown';
  const reason = typeof parsed.reason === 'string' ? parsed.reason.slice(0, 200) : '';
  if (intent === 'unknown') return { intent, reason, error: 'bad intent value', durationMs: Date.now() - started };
  return { intent, reason, durationMs: Date.now() - started };
}

// Pure decision the server drives for the brief hold. Inputs are the current cache /
// pending entries for the session plus the frame fp and clock. Output tells the server
// what to do this tick. (cachedIntent === null means a prior classification fell back.)
function resolveIntentDecision({ cachedFp, cachedIntent, pendingFp, pendingDeadline, fp, now, holdMs } = {}) {
  if (cachedFp === fp) {
    return cachedIntent ? { action: 'use', intent: cachedIntent } : { action: 'fallback' };
  }
  if (pendingFp === fp) {
    return now < pendingDeadline ? { action: 'hold' } : { action: 'fallback' };
  }
  return { action: 'start', deadline: now + (holdMs || INTENT_TIMEOUT_MS) };
}

module.exports = { frameFingerprint, assessAmbiguity, classifyPromptIntent, resolveIntentDecision, _titleLooksLikeCommand, INTENT_TIMEOUT_MS };
