/**
 * engine/harness.js — Tri-agent harness mode for long-running missions.
 *
 * Inspired by Anthropic's Harness Design (April 2026), this module implements
 * the "tri_agent" mission mode: a schedule firing produces three coordinated
 * work items — Planner → Generator → Evaluator — that operate against a
 * shared artifact on disk. The Evaluator scores the artifact against a rubric
 * declared in the schedule config; if it scores below the threshold (default
 * 0.7), the engine spawns another Generator+Evaluator iteration with the
 * Evaluator's feedback injected. The cycle terminates on pass or after
 * `harness_max_iterations` (default 5) iterations — whichever comes first.
 *
 * Why a separate module:
 *   - Keeps scheduler.js focused on cron parsing + run-dedup.
 *   - The iteration loop lives in lifecycle.js so the engine's existing
 *     post-completion hook chain owns retry orchestration.
 *   - Pure helpers here (no side effects beyond reading shared.MINIONS_DIR)
 *     are easy to test in isolation.
 *
 * Schedule config shape (additive on top of the cron schedule schema):
 *   {
 *     id: 'daily-research',
 *     cron: '0 9 * *',
 *     title: 'Daily research pass',
 *     description: 'Summarize new arxiv papers',
 *     project: 'minions',                    // optional
 *     type: 'ask',                            // generator work-type (default 'ask')
 *     harness_mode: 'tri_agent',              // REQUIRED to enable harness mode
 *     harness_rubric: 'Must cite ≥3 papers', // REQUIRED — passed to Evaluator
 *     harness_threshold: 0.7,                 // default 0.7, must be (0, 1]
 *     harness_max_iterations: 5,              // default 5, integer in [1, 20]
 *   }
 *
 * Per-item _harness meta (carried through dispatch + lifecycle):
 *   {
 *     role: 'planner' | 'generator' | 'evaluator',
 *     iteration: 1,                  // generators/evaluators bump this on retry
 *     missionId: 'sched-id-1700-abc',
 *     artifactPath: '<MINIONS_DIR>/engine/harness/<missionId>/artifact.md',
 *     rubric: '...',                 // verbatim from schedule
 *     threshold: 0.7,
 *     maxIterations: 5,
 *     generatorType: 'ask',          // remembered so iteration N+1 reuses it
 *   }
 *
 * Zero dependencies beyond Node built-ins + engine/shared.
 */

'use strict';

const path = require('path');
const shared = require('./shared');
const { MINIONS_DIR, WI_STATUS, ts } = shared;

const HARNESS_MODE = Object.freeze({
  TRI_AGENT: 'tri_agent',
});

const HARNESS_ROLE = Object.freeze({
  PLANNER: 'planner',
  GENERATOR: 'generator',
  EVALUATOR: 'evaluator',
});

const HARNESS_DEFAULTS = Object.freeze({
  threshold: 0.7,
  maxIterations: 5,
  // Used when sched.type is absent. Ask is read-only and produces no PR, which
  // matches the "research / synthesis" use case the harness was designed for.
  generatorType: 'ask',
});

const HARNESS_MAX_ITERATIONS_CAP = 20;

// Filesystem layout for harness artifacts. Each mission gets its own dir so
// concurrent missions don't stomp each other's artifacts.
function harnessRootDir() {
  return path.join(MINIONS_DIR, 'engine', 'harness');
}

function harnessMissionDir(missionId) {
  return path.join(harnessRootDir(), missionId);
}

function harnessArtifactPath(missionId) {
  return path.join(harnessMissionDir(missionId), 'artifact.md');
}

/**
 * Validate a schedule's tri-agent harness configuration.
 * Returns { valid: boolean, errors: string[], resolved?: { threshold, maxIterations, generatorType } }.
 * The `resolved` object reflects the defaults that will be applied (only populated when valid=true).
 */
function validateHarnessConfig(sched) {
  const errors = [];
  if (!sched || typeof sched !== 'object') {
    return { valid: false, errors: ['schedule must be an object'] };
  }

  if (sched.harness_mode !== HARNESS_MODE.TRI_AGENT) {
    errors.push(`harness_mode must be "${HARNESS_MODE.TRI_AGENT}" (got ${JSON.stringify(sched.harness_mode)})`);
  }

  if (typeof sched.harness_rubric !== 'string' || sched.harness_rubric.trim().length === 0) {
    errors.push('harness_rubric is required (non-empty string)');
  }

  let threshold = HARNESS_DEFAULTS.threshold;
  if (sched.harness_threshold !== undefined && sched.harness_threshold !== null) {
    if (typeof sched.harness_threshold !== 'number' || !Number.isFinite(sched.harness_threshold)
        || sched.harness_threshold <= 0 || sched.harness_threshold > 1) {
      errors.push(`harness_threshold must be a number in (0, 1] (got ${JSON.stringify(sched.harness_threshold)})`);
    } else {
      threshold = sched.harness_threshold;
    }
  }

  let maxIterations = HARNESS_DEFAULTS.maxIterations;
  if (sched.harness_max_iterations !== undefined && sched.harness_max_iterations !== null) {
    const n = sched.harness_max_iterations;
    if (!Number.isInteger(n) || n < 1 || n > HARNESS_MAX_ITERATIONS_CAP) {
      errors.push(`harness_max_iterations must be a positive integer ≤ ${HARNESS_MAX_ITERATIONS_CAP} (got ${JSON.stringify(n)})`);
    } else {
      maxIterations = n;
    }
  }

  if (errors.length > 0) return { valid: false, errors };
  return {
    valid: true,
    errors: [],
    resolved: {
      threshold,
      maxIterations,
      generatorType: typeof sched.type === 'string' && sched.type.trim() ? sched.type.trim() : HARNESS_DEFAULTS.generatorType,
    },
  };
}

// ─── ID + path helpers ──────────────────────────────────────────────────────

function _shortRand() {
  // 6 hex chars from current ms entropy — collisions are vanishingly rare for
  // distinct scheduler ticks and harness has no cross-process write contention
  // (each mission's artifact dir is freshly created).
  return Math.random().toString(36).slice(2, 8);
}

function _buildMissionId(sched, nowMs) {
  const base = sched && typeof sched.id === 'string' ? sched.id : 'mission';
  return `${base}-${nowMs}-${_shortRand()}`;
}

function _buildItemId(scheduleId, role, iteration, nowMs) {
  return `sched-${scheduleId}-${role}-i${iteration}-${nowMs}-${_shortRand()}`;
}

// ─── Prompt builders ────────────────────────────────────────────────────────

const RUBRIC_HEADING = '## Rubric';
const ARTIFACT_HEADING = '## Shared Artifact';

function _buildPlannerDescription(sched, ctx) {
  const { artifactPath, missionId, threshold, maxIterations, rubric, iteration } = ctx;
  return [
    `# Tri-Agent Mission — Planner (iteration ${iteration})`,
    '',
    `**Mission:** ${sched.title || sched.id}`,
    '',
    `**Goal:** ${(sched.description || sched.title || '').trim()}`,
    '',
    'You are the **Planner** in a three-agent harness loop. Your job is to',
    'decompose the mission goal above into a numbered list of concrete subtasks',
    'that the Generator will execute next, then write the plan to the shared',
    'artifact below. Keep subtasks small and verifiable.',
    '',
    ARTIFACT_HEADING,
    '',
    `Write your plan to: \`${artifactPath}\``,
    '',
    'Structure the file as:',
    '```',
    `# Mission ${missionId}`,
    '',
    '## Plan',
    '1. <subtask 1>',
    '2. <subtask 2>',
    '...',
    '```',
    '',
    'Do not execute the subtasks yourself — that is the Generator\'s job.',
    'Create the artifact directory if it does not exist.',
    '',
    RUBRIC_HEADING,
    '',
    'The Evaluator will eventually score the completed artifact against this',
    'rubric. Plan with the rubric in mind:',
    '',
    '> ' + rubric.split('\n').join('\n> '),
    '',
    `Threshold: ${threshold} · Max iterations: ${maxIterations}`,
    '',
    `Mission ID: \`${missionId}\``,
  ].join('\n');
}

function _buildGeneratorDescription(sched, ctx, opts) {
  const { artifactPath, missionId, threshold, maxIterations, rubric, iteration } = ctx;
  const previousFeedback = opts && opts.previousFeedback ? String(opts.previousFeedback).trim() : '';
  const lines = [
    `# Tri-Agent Mission — Generator (iteration ${iteration})`,
    '',
    `**Mission:** ${sched.title || sched.id}`,
    '',
    `**Goal:** ${(sched.description || sched.title || '').trim()}`,
    '',
    'You are the **Generator** in a three-agent harness loop. Read the Planner\'s',
    'subtask list from the shared artifact, execute each subtask in order, and',
    'append your outputs to the artifact under a clearly-labelled section.',
    '',
    ARTIFACT_HEADING,
    '',
    `Shared artifact: \`${artifactPath}\``,
    '',
    `Append a section titled \`## Generator Output (iteration ${iteration})\` to the`,
    'artifact. Within it, address each numbered subtask from the plan.',
    '',
    'Do NOT delete or rewrite earlier sections — append only.',
  ];

  if (previousFeedback) {
    lines.push(
      '',
      `## Previous Evaluator Feedback (iteration ${iteration - 1})`,
      '',
      'The previous iteration failed the rubric. The Evaluator provided this feedback:',
      '',
      '> ' + previousFeedback.split('\n').join('\n> '),
      '',
      'Address this feedback explicitly in your new output.',
    );
  }

  lines.push(
    '',
    RUBRIC_HEADING,
    '',
    'The Evaluator will score your output against this rubric:',
    '',
    '> ' + rubric.split('\n').join('\n> '),
    '',
    `Threshold: ${threshold} · Max iterations: ${maxIterations}`,
    '',
    `Mission ID: \`${missionId}\``,
  );
  return lines.join('\n');
}

function _buildEvaluatorDescription(sched, ctx) {
  const { artifactPath, missionId, threshold, maxIterations, rubric, iteration } = ctx;
  return [
    `# Tri-Agent Mission — Evaluator (iteration ${iteration})`,
    '',
    `**Mission:** ${sched.title || sched.id}`,
    '',
    'You are the **Evaluator** in a three-agent harness loop. Read the shared',
    'artifact (including the Planner\'s plan and the Generator\'s output) and',
    'score it against the rubric below.',
    '',
    ARTIFACT_HEADING,
    '',
    `Shared artifact: \`${artifactPath}\``,
    '',
    'Append a section titled `## Evaluation (iteration ' + iteration + ')` containing:',
    '- A numeric score in `[0, 1]` formatted as `Score: 0.NN`',
    '- A `PASS` or `FAIL` verdict on its own line',
    '- Concrete feedback under `### Feedback` explaining strengths and gaps',
    '',
    RUBRIC_HEADING,
    '',
    '> ' + rubric.split('\n').join('\n> '),
    '',
    `**Threshold:** ${threshold} — a score < ${threshold} is a FAIL and triggers another Generator iteration`,
    `(up to ${maxIterations} total iterations).`,
    '',
    '## Completion Report',
    '',
    'In your JSON completion report include these fields so the engine can route',
    'the next iteration deterministically (in addition to the standard schema):',
    '```json',
    '{',
    '  "status": "success",',
    '  "summary": "<one-line verdict>",',
    '  "harness_score": 0.NN,',
    '  "harness_pass": true | false,',
    '  "harness_feedback": "<machine-readable feedback the next Generator should address>"',
    '}',
    '```',
    '',
    'If you cannot evaluate (artifact missing, malformed), set `harness_pass: false`,',
    '`harness_score: 0`, and explain in `harness_feedback`.',
    '',
    `Mission ID: \`${missionId}\``,
  ].join('\n');
}

// ─── Mission creation ───────────────────────────────────────────────────────

function _commonItemFields(sched, role, iteration) {
  return {
    title: `[harness:${role}:i${iteration}] ${sched.title || sched.id}`,
    priority: sched.priority || 'medium',
    status: WI_STATUS.PENDING,
    created: ts(),
    createdBy: 'scheduler:harness',
    project: sched.project || null,
    agent: null,
    _scheduleId: sched.id,
  };
}

function _buildHarnessMeta(missionId, role, iteration, resolved, sched, artifactPath) {
  return {
    role,
    iteration,
    missionId,
    artifactPath,
    rubric: sched.harness_rubric,
    threshold: resolved.threshold,
    maxIterations: resolved.maxIterations,
    generatorType: resolved.generatorType,
  };
}

/**
 * Build the initial Planner → Generator → Evaluator trio for a tri-agent
 * schedule firing. Throws if the schedule's harness config is invalid — the
 * caller is responsible for validating + logging upstream when desired.
 *
 * Returns { items, missionId, artifactPath } where items[0..2] are the three
 * work items in dispatch order, already linked by depends_on.
 */
function createTriAgentMission(sched, opts) {
  const { valid, errors, resolved } = validateHarnessConfig(sched);
  if (!valid) throw new Error(`tri_agent harness config invalid for schedule ${sched && sched.id}: ${errors.join('; ')}`);

  const nowMs = opts && Number.isFinite(opts.now) ? opts.now : Date.now();
  const missionId = (opts && typeof opts.missionId === 'string' && opts.missionId) || _buildMissionId(sched, nowMs);
  const artifactPath = harnessArtifactPath(missionId);
  const iteration = 1;
  const ctx = {
    artifactPath, missionId, iteration,
    threshold: resolved.threshold,
    maxIterations: resolved.maxIterations,
    rubric: sched.harness_rubric,
  };

  const plannerId = _buildItemId(sched.id, HARNESS_ROLE.PLANNER, iteration, nowMs);
  const generatorId = _buildItemId(sched.id, HARNESS_ROLE.GENERATOR, iteration, nowMs);
  const evaluatorId = _buildItemId(sched.id, HARNESS_ROLE.EVALUATOR, iteration, nowMs);

  // Planner + Evaluator are read-only (ask) by design — they don't mutate
  // project code, they only read/write the shared harness artifact. The
  // generator inherits sched.type (default 'ask').
  const planner = {
    id: plannerId,
    type: 'ask',
    description: _buildPlannerDescription(sched, ctx),
    depends_on: [],
    ..._commonItemFields(sched, HARNESS_ROLE.PLANNER, iteration),
    _missionId: missionId,
    _harness: _buildHarnessMeta(missionId, HARNESS_ROLE.PLANNER, iteration, resolved, sched, artifactPath),
  };

  const generator = {
    id: generatorId,
    type: resolved.generatorType,
    description: _buildGeneratorDescription(sched, ctx, {}),
    depends_on: [plannerId],
    ..._commonItemFields(sched, HARNESS_ROLE.GENERATOR, iteration),
    _missionId: missionId,
    _harness: _buildHarnessMeta(missionId, HARNESS_ROLE.GENERATOR, iteration, resolved, sched, artifactPath),
  };

  const evaluator = {
    id: evaluatorId,
    type: 'ask',
    description: _buildEvaluatorDescription(sched, ctx),
    depends_on: [generatorId],
    ..._commonItemFields(sched, HARNESS_ROLE.EVALUATOR, iteration),
    _missionId: missionId,
    _harness: _buildHarnessMeta(missionId, HARNESS_ROLE.EVALUATOR, iteration, resolved, sched, artifactPath),
  };

  return { items: [planner, generator, evaluator], missionId, artifactPath };
}

/**
 * Build the Generator+Evaluator pair for iteration N+1 after the Evaluator
 * fails the rubric. The Planner only runs once per mission — its plan is
 * already in the shared artifact.
 *
 * `prevEvaluatorItem` is the work item that just completed (must carry
 * `_harness` meta with role='evaluator'). The new generator depends on it so
 * the engine's dispatch loop won't fire it until the artifact is fully written.
 */
function createIterationWorkItems(prevEvaluatorItem, verdict, opts) {
  if (!prevEvaluatorItem || !prevEvaluatorItem._harness) {
    throw new Error('createIterationWorkItems: prevEvaluatorItem missing _harness meta');
  }
  const prevMeta = prevEvaluatorItem._harness;
  if (prevMeta.role !== HARNESS_ROLE.EVALUATOR) {
    throw new Error(`createIterationWorkItems: prev item must be an evaluator (got role=${prevMeta.role})`);
  }
  const iteration = (Number(prevMeta.iteration) || 1) + 1;
  const nowMs = opts && Number.isFinite(opts.now) ? opts.now : Date.now();

  const sched = {
    id: prevEvaluatorItem._scheduleId,
    title: prevEvaluatorItem.title || prevMeta.missionId,
    description: '', // Carried via artifact + feedback, not re-rendered.
    harness_rubric: prevMeta.rubric,
    project: prevEvaluatorItem.project || null,
    priority: prevEvaluatorItem.priority || 'medium',
  };
  const ctx = {
    artifactPath: prevMeta.artifactPath,
    missionId: prevMeta.missionId,
    iteration,
    threshold: prevMeta.threshold,
    maxIterations: prevMeta.maxIterations,
    rubric: prevMeta.rubric,
  };
  const resolved = {
    threshold: prevMeta.threshold,
    maxIterations: prevMeta.maxIterations,
    generatorType: prevMeta.generatorType || HARNESS_DEFAULTS.generatorType,
  };

  const generatorId = _buildItemId(sched.id || 'mission', HARNESS_ROLE.GENERATOR, iteration, nowMs);
  const evaluatorId = _buildItemId(sched.id || 'mission', HARNESS_ROLE.EVALUATOR, iteration, nowMs);
  const feedback = verdict && verdict.feedback ? verdict.feedback : '(no feedback supplied)';

  const generator = {
    id: generatorId,
    type: resolved.generatorType,
    title: `[harness:generator:i${iteration}] ${sched.title}`,
    description: _buildGeneratorDescription(sched, ctx, { previousFeedback: feedback }),
    depends_on: [prevEvaluatorItem.id],
    priority: sched.priority,
    status: WI_STATUS.PENDING,
    created: ts(),
    createdBy: 'harness:iterate',
    project: sched.project,
    agent: null,
    _scheduleId: sched.id,
    _missionId: prevMeta.missionId,
    _harness: _buildHarnessMeta(prevMeta.missionId, HARNESS_ROLE.GENERATOR, iteration, resolved, sched, prevMeta.artifactPath),
  };
  const evaluator = {
    id: evaluatorId,
    type: 'ask',
    title: `[harness:evaluator:i${iteration}] ${sched.title}`,
    description: _buildEvaluatorDescription(sched, ctx),
    depends_on: [generatorId],
    priority: sched.priority,
    status: WI_STATUS.PENDING,
    created: ts(),
    createdBy: 'harness:iterate',
    project: sched.project,
    agent: null,
    _scheduleId: sched.id,
    _missionId: prevMeta.missionId,
    _harness: _buildHarnessMeta(prevMeta.missionId, HARNESS_ROLE.EVALUATOR, iteration, resolved, sched, prevMeta.artifactPath),
  };

  return [generator, evaluator];
}

// ─── Verdict parsing + iteration gate ───────────────────────────────────────

const SCORE_RE = /(?:^|\W)Score\s*[:=]\s*([0-1](?:\.\d+)?|\.\d+)/i;
const PASS_RE = /(?:^|[^\w])(PASS|✅\s*PASS|verdict\s*[:=]\s*pass)\b/i;
const FAIL_RE = /(?:^|[^\w])(FAIL|❌\s*FAIL|verdict\s*[:=]\s*fail)\b/i;

/**
 * Extract { score, pass, feedback } from the Evaluator's completion report and
 * stdout. Structured fields in the completion report win when present.
 *
 * Returns:
 *   { score: number | null, pass: boolean | null, feedback: string }
 * `score=null` and `pass=null` together mean "no signal" — the caller should
 * treat this as inconclusive (do NOT retry blindly).
 */
function parseEvaluatorVerdict(stdout, structuredCompletion) {
  let score = null;
  let pass = null;
  let feedback = '';

  // Structured fields take precedence — they're the documented contract in
  // the evaluator prompt and not vulnerable to text-format drift.
  if (structuredCompletion && typeof structuredCompletion === 'object') {
    if (typeof structuredCompletion.harness_score === 'number' && Number.isFinite(structuredCompletion.harness_score)) {
      score = Math.max(0, Math.min(1, structuredCompletion.harness_score));
    }
    if (typeof structuredCompletion.harness_pass === 'boolean') {
      pass = structuredCompletion.harness_pass;
    }
    if (typeof structuredCompletion.harness_feedback === 'string' && structuredCompletion.harness_feedback.trim()) {
      feedback = structuredCompletion.harness_feedback.trim();
    } else if (typeof structuredCompletion.summary === 'string' && structuredCompletion.summary.trim()) {
      feedback = structuredCompletion.summary.trim();
    }
  }

  // Text fallback — only fill in fields the structured report did not provide.
  if ((score === null || pass === null || !feedback) && typeof stdout === 'string' && stdout.length > 0) {
    if (score === null) {
      const m = SCORE_RE.exec(stdout);
      if (m) {
        const n = parseFloat(m[1]);
        if (Number.isFinite(n)) score = Math.max(0, Math.min(1, n));
      }
    }
    if (pass === null) {
      const failMatch = FAIL_RE.exec(stdout);
      const passMatch = PASS_RE.exec(stdout);
      // FAIL takes precedence over PASS when both appear (the evaluator's
      // explanation of failure may mention 'pass criteria' etc).
      if (failMatch) pass = false;
      else if (passMatch) pass = true;
    }
    if (!feedback) {
      // Best-effort: take the last non-empty line as the feedback summary.
      const lines = stdout.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
      if (lines.length > 0) feedback = lines[lines.length - 1].slice(0, 2000);
    }
  }

  // If score is set but pass is not, infer pass from the threshold caller
  // (lifecycle.shouldIterateAgain) — but leave pass=null here so the caller
  // can apply the per-mission threshold rather than baking in a default.
  return { score, pass, feedback };
}

/**
 * Decide whether to spawn another Generator+Evaluator iteration.
 *
 * Rules (in order):
 *   1. If verdict.pass === true, stop (mission succeeded).
 *   2. If iteration >= maxIterations, stop (cap reached).
 *   3. If we have a numeric score AND score >= threshold, treat as pass and stop.
 *   4. If we have a numeric score AND score <  threshold, iterate.
 *   5. If verdict.pass === false explicitly (no score), iterate.
 *   6. Otherwise (no score and no pass signal), STOP — silent agents would
 *      loop forever; require explicit failure to retry.
 */
function shouldIterateAgain(harnessMeta, verdict) {
  if (!harnessMeta || !verdict) return false;
  const iteration = Number(harnessMeta.iteration) || 1;
  const maxIterations = Number(harnessMeta.maxIterations) || HARNESS_DEFAULTS.maxIterations;
  const threshold = Number(harnessMeta.threshold);
  const t = Number.isFinite(threshold) ? threshold : HARNESS_DEFAULTS.threshold;

  if (verdict.pass === true) return false;
  if (iteration >= maxIterations) return false;

  if (typeof verdict.score === 'number' && Number.isFinite(verdict.score)) {
    return verdict.score < t;
  }
  if (verdict.pass === false) return true;
  return false;
}

module.exports = {
  HARNESS_MODE,
  HARNESS_ROLE,
  HARNESS_DEFAULTS,
  HARNESS_MAX_ITERATIONS_CAP,
  harnessRootDir,
  harnessMissionDir,
  harnessArtifactPath,
  validateHarnessConfig,
  createTriAgentMission,
  createIterationWorkItems,
  parseEvaluatorVerdict,
  shouldIterateAgain,
  // Exported for direct unit tests (per docs/skills.md skill 'export-internal-helpers-for-direct-unit-tests').
  _buildPlannerDescription,
  _buildGeneratorDescription,
  _buildEvaluatorDescription,
};
