#!/usr/bin/env node
/**
 * brain-retro reviewer — direct Opus API fallback handler (Phase 3 Task 18).
 *
 * Spec §4.6: the primary reviewer is a Claude Code subagent
 * (`.claude/agents/reviewer-agent.md`) spawned via Task() from /brain-retro.
 * THIS module is the FALLBACK handler invoked by the controller when the
 * subagent crashes / times out: direct Opus API call with the same adaptive
 * review prompt (but no cross-episode reading, no skill invocations).
 *
 * Pure layer: buildReviewPrompt + parseReview (this file's tests). Network
 * layer: reviewViaDirectApi (zero-cost wrapper around router-classifier's
 * callAnthropicAPI; the controller decides when to call it).
 *
 * G16 — file did not exist before Phase 3 Task 18; created here.
 */

import { REVIEWER_MODEL } from './router-config.mjs';

const REQUIRED_REVIEW_FIELDS = [
  'node_quality',
  'chain_quality',
  'gap_assessment',
  'agent_self_assessment_accuracy',
  'error_root_cause',
  'outcome_reviewed',
  'reasoning',
];

/**
 * Build the adaptive review prompt as { system, user } blocks for Anthropic
 * prompt caching (ephemeral 5m TTL). The `system` block contains static
 * instructions + 8-dim cues + schema-version-conditional notes; it is
 * byte-identical across all episodes with the same schema_version and gets
 * billed at ~10% rate after the first call within a 5-minute window. The
 * `user` block carries the per-episode JSON (volatile).
 *
 * Cache-eligibility caveat: Anthropic's minimum cacheable prefix is
 * model-dependent — 4096 tokens on Opus 4.7 / 4.6, 2048 on Sonnet 4.6. The
 * static block here is ~300-400 tokens, so on Opus 4.7 cache writes silently
 * no-op until either the static block grows or the model switches to Sonnet.
 * The split is still applied so the moment either condition flips, caching
 * activates with zero further code changes.
 */
export function buildReviewPromptStructured(episode) {
  const v = Number(episode?.schema_version) || 0;
  const cues = [
    'node_quality: correct | wrong_node | overkill | underkill | disputable',
    'chain_quality: correct | missing_step | extra_step | wrong_order | n/a',
    'gap_assessment: acceptable | mistake_should_complete | mistake_should_not_start | n/a',
    'agent_self_assessment_accuracy: accurate | over_confident | under_confident | no_self_assessment',
    'error_root_cause: wrong_skill | wrong_tool | wrong_chain_order | external_failure | n/a',
    'alternative_better: <node_id> | null',
    'outcome_reviewed: success | soft_success | rework | blocked',
    'reasoning: 1-3 sentences',
  ];

  const adaptiveNotes = [];
  if (v >= 3) {
    adaptiveNotes.push('Episode is v3+: primary_rationale carries triggers/candidates/boundaries.');
  }
  if (v >= 4) {
    adaptiveNotes.push('Episode is v4: classifier_output.alternatives_considered tells you what the classifier weighed.');
    adaptiveNotes.push('self_assessment (if present and not pending) is the agent\'s post-hoc judgement — compare honesty.');
    adaptiveNotes.push('execution_trace.chain_gaps shows whether the recommended chain ran in full.');
  }

  const system = [
    'You are the independent reviewer of routing decisions for the Лидерра brain-governance experiment.',
    'Return ONLY a JSON object with the 8 fields below. No prose, no code fences.',
    '',
    'Fields:',
    ...cues.map((c) => '  - ' + c),
    '',
    adaptiveNotes.length ? 'Notes for this schema version:' : '',
    ...adaptiveNotes.map((n) => '  - ' + n),
  ].filter(Boolean).join('\n');

  const user = [
    'Episode (JSON):',
    JSON.stringify(episode, null, 2),
    '',
    'Output JSON only.',
  ].join('\n');

  return { system, user };
}

/**
 * Backward-compat wrapper — returns the concatenated single-string prompt for
 * tests and any caller that hasn't switched to the structured form.
 *
 * Adaptive prompt template (spec §4.6):
 *   - v4 → full prompt including alternatives_considered, self_assessment,
 *     chain_gaps cues.
 *   - v3 → omits alternatives_considered.
 *   - v2 → omits both alternatives_considered and self_assessment.
 *   - v1 → skipped upstream (caller filters them out).
 */
export function buildReviewPrompt(episode) {
  const { system, user } = buildReviewPromptStructured(episode);
  return `${system}\n\n${user}`;
}

/**
 * Parse the Opus reviewer response. Pure. Returns null on malformed JSON or
 * when a required 8-dim field is missing. Passes through `reviewer_error`
 * escalations from the subagent.
 */
export function parseReview(text) {
  if (!text) return null;
  const stripped = String(text).trim()
    .replace(/^```(?:json)?\s*\n?/, '')
    .replace(/\n?```$/, '')
    .trim();
  let parsed;
  try { parsed = JSON.parse(stripped); }
  catch { return null; }
  if (!parsed || typeof parsed !== 'object') return null;

  // Reviewer-agent escalation: pass through verbatim.
  if (typeof parsed.reviewer_error === 'string') return parsed;

  for (const f of REQUIRED_REVIEW_FIELDS) {
    if (parsed[f] === undefined) return null;
  }
  return parsed;
}

/**
 * Direct Opus API call. Wraps callAnthropicAPI from router-classifier with
 * the reviewer model. Caller (controller inside /brain-retro) is responsible
 * for decision (subagent first, this on failure).
 *
 * Returns the parsed review object or null on transport / parse failure.
 */
export async function reviewViaDirectApi(episode, options = {}) {
  const { callAnthropicAPI } = await import('./router-classifier.mjs');
  const apiKey = options.apiKey ?? process.env.ROUTER_LLM_KEY;
  if (!apiKey) return null;
  const structured = buildReviewPromptStructured(episode);
  try {
    const text = await callAnthropicAPI(structured, {
      apiKey,
      baseUrl: options.baseUrl ?? process.env.ROUTER_LLM_BASE_URL ?? undefined,
      model: options.model ?? REVIEWER_MODEL,
      onUsage: options.onUsage,
    });
    return parseReview(text);
  } catch {
    return null;
  }
}
