const fs = require('fs');
const path = require('path');
const { getMemoryDir } = require('./paths');
const { normalizePersonalityState, isValidPersonalityState, personalityKey } = require('./personality');
const { isValidMutation, normalizeMutation } = require('./mutation');

function ensureDir(dir) {
  try {
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  } catch (e) {}
}

function stableHash(input) {
  const s = String(input || '');
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return (h >>> 0).toString(16).padStart(8, '0');
}

function nowIso() {
  return new Date().toISOString();
}

function normalizeErrorSignature(text) {
  const s = String(text || '').trim();
  if (!s) return null;
  return (
    s
      .toLowerCase()
      // normalize Windows paths
      .replace(/[a-z]:\\[^ \n\r\t]+/gi, '<path>')
      // normalize Unix paths
      .replace(/\/[^ \n\r\t]+/g, '<path>')
      // normalize hex and numbers
      .replace(/\b0x[0-9a-f]+\b/gi, '<hex>')
      .replace(/\b\d+\b/g, '<n>')
      // normalize whitespace
      .replace(/\s+/g, ' ')
      .slice(0, 220)
  );
}

function normalizeSignalsForMatching(signals) {
  const list = Array.isArray(signals) ? signals : [];
  const out = [];
  for (const s of list) {
    const str = String(s || '').trim();
    if (!str) continue;
    if (str.startsWith('errsig:')) {
      const norm = normalizeErrorSignature(str.slice('errsig:'.length));
      if (norm) out.push(`errsig_norm:${stableHash(norm)}`);
      continue;
    }
    out.push(str);
  }
  return out;
}

function computeSignalKey(signals) {
  // Key must be stable across runs; normalize noisy signatures (paths, numbers).
  const list = normalizeSignalsForMatching(signals);
  const uniq = Array.from(new Set(list.filter(Boolean))).sort();
  return uniq.join('|') || '(none)';
}

function extractErrorSignatureFromSignals(signals) {
  // Convention: signals can include "errsig:<raw>" emitted by signals extractor.
  const list = Array.isArray(signals) ? signals : [];
  for (const s of list) {
    const str = String(s || '');
    if (str.startsWith('errsig:')) return normalizeErrorSignature(str.slice('errsig:'.length));
  }
  return null;
}

function memoryGraphPath() {
  const { getEvolutionDir } = require('./paths');
  const evoDir = getEvolutionDir();
  return process.env.MEMORY_GRAPH_PATH || path.join(evoDir, 'memory_graph.jsonl');
}

function memoryGraphStatePath() {
  const { getEvolutionDir } = require('./paths');
  return path.join(getEvolutionDir(), 'memory_graph_state.json');
}

function appendJsonl(filePath, obj) {
  const dir = path.dirname(filePath);
  ensureDir(dir);
  fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', 'utf8');
}

function readJsonIfExists(filePath, fallback) {
  try {
    if (!fs.existsSync(filePath)) return fallback;
    const raw = fs.readFileSync(filePath, 'utf8');
    if (!raw.trim()) return fallback;
    return JSON.parse(raw);
  } catch (e) {
    return fallback;
  }
}

function writeJsonAtomic(filePath, obj) {
  const dir = path.dirname(filePath);
  ensureDir(dir);
  const tmp = `${filePath}.tmp`;
  fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
  fs.renameSync(tmp, filePath);
}

function tryReadMemoryGraphEvents(limitLines = 2000) {
  try {
    const p = memoryGraphPath();
    if (!fs.existsSync(p)) return [];
    const raw = fs.readFileSync(p, 'utf8');
    const lines = raw
      .split('\n')
      .map(l => l.trim())
      .filter(Boolean);
    const recent = lines.slice(Math.max(0, lines.length - limitLines));
    return recent
      .map(l => {
        try {
          return JSON.parse(l);
        } catch (e) {
          return null;
        }
      })
      .filter(Boolean);
  } catch (e) {
    return [];
  }
}

function jaccard(aList, bList) {
  const aNorm = normalizeSignalsForMatching(aList);
  const bNorm = normalizeSignalsForMatching(bList);
  const a = new Set((Array.isArray(aNorm) ? aNorm : []).map(String));
  const b = new Set((Array.isArray(bNorm) ? bNorm : []).map(String));
  if (a.size === 0 && b.size === 0) return 1;
  if (a.size === 0 || b.size === 0) return 0;
  let inter = 0;
  for (const x of a) if (b.has(x)) inter++;
  const union = a.size + b.size - inter;
  return union === 0 ? 0 : inter / union;
}

function decayWeight(updatedAtIso, halfLifeDays) {
  const hl = Number(halfLifeDays);
  if (!Number.isFinite(hl) || hl <= 0) return 1;
  const t = Date.parse(updatedAtIso);
  if (!Number.isFinite(t)) return 1;
  const ageDays = (Date.now() - t) / (1000 * 60 * 60 * 24);
  if (!Number.isFinite(ageDays) || ageDays <= 0) return 1;
  // Exponential half-life decay: weight = 0.5^(age/hl)
  return Math.pow(0.5, ageDays / hl);
}

function aggregateEdges(events) {
  // Aggregate by (signal_key, gene_id) from outcome events.
  // Laplace smoothing to avoid 0/1 extremes.
  const map = new Map();
  for (const ev of events) {
    if (!ev || ev.type !== 'MemoryGraphEvent') continue;
    if (ev.kind !== 'outcome') continue;
    const signalKey = ev.signal && ev.signal.key ? String(ev.signal.key) : '(none)';
    const geneId = ev.gene && ev.gene.id ? String(ev.gene.id) : null;
    if (!geneId) continue;

    const k = `${signalKey}::${geneId}`;
    const cur = map.get(k) || { signalKey, geneId, success: 0, fail: 0, last_ts: null, last_score: null };
    const status = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
    if (status === 'success') cur.success += 1;
    else if (status === 'failed') cur.fail += 1;

    const ts = ev.ts || ev.created_at || ev.at;
    if (ts && (!cur.last_ts || Date.parse(ts) > Date.parse(cur.last_ts))) {
      cur.last_ts = ts;
      cur.last_score =
        ev.outcome && Number.isFinite(Number(ev.outcome.score)) ? Number(ev.outcome.score) : cur.last_score;
    }
    map.set(k, cur);
  }
  return map;
}

function aggregateGeneOutcomes(events) {
  // Aggregate by gene_id from outcome events (gene -> outcome success probability).
  const map = new Map();
  for (const ev of events) {
    if (!ev || ev.type !== 'MemoryGraphEvent') continue;
    if (ev.kind !== 'outcome') continue;
    const geneId = ev.gene && ev.gene.id ? String(ev.gene.id) : null;
    if (!geneId) continue;
    const cur = map.get(geneId) || { geneId, success: 0, fail: 0, last_ts: null, last_score: null };
    const status = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
    if (status === 'success') cur.success += 1;
    else if (status === 'failed') cur.fail += 1;
    const ts = ev.ts || ev.created_at || ev.at;
    if (ts && (!cur.last_ts || Date.parse(ts) > Date.parse(cur.last_ts))) {
      cur.last_ts = ts;
      cur.last_score =
        ev.outcome && Number.isFinite(Number(ev.outcome.score)) ? Number(ev.outcome.score) : cur.last_score;
    }
    map.set(geneId, cur);
  }
  return map;
}

function edgeExpectedSuccess(edge, opts) {
  const e = edge || { success: 0, fail: 0, last_ts: null };
  const succ = Number(e.success) || 0;
  const fail = Number(e.fail) || 0;
  const total = succ + fail;
  const p = (succ + 1) / (total + 2); // Laplace smoothing
  const halfLifeDays = opts && Number.isFinite(Number(opts.half_life_days)) ? Number(opts.half_life_days) : 30;
  const w = decayWeight(e.last_ts || '', halfLifeDays);
  return { p, w, total, value: p * w };
}

function getMemoryAdvice({ signals, genes, driftEnabled }) {
  const events = tryReadMemoryGraphEvents(2000);
  const edges = aggregateEdges(events);
  const geneOutcomes = aggregateGeneOutcomes(events);
  const curSignals = Array.isArray(signals) ? signals : [];
  const curKey = computeSignalKey(curSignals);

  const bannedGeneIds = new Set();
  const scoredGeneIds = [];

  // Similarity: consider exact key first, then any key with overlap.
  const seenKeys = new Set();
  const candidateKeys = [];
  candidateKeys.push({ key: curKey, sim: 1 });
  seenKeys.add(curKey);

  for (const ev of events) {
    if (!ev || ev.type !== 'MemoryGraphEvent') continue;
    const k = ev.signal && ev.signal.key ? String(ev.signal.key) : '(none)';
    if (seenKeys.has(k)) continue;
    const sigs = ev.signal && Array.isArray(ev.signal.signals) ? ev.signal.signals : [];
    const sim = jaccard(curSignals, sigs);
    if (sim >= 0.34) {
      candidateKeys.push({ key: k, sim });
      seenKeys.add(k);
    }
  }

  const byGene = new Map();
  for (const ck of candidateKeys) {
    for (const g of Array.isArray(genes) ? genes : []) {
      if (!g || g.type !== 'Gene' || !g.id) continue;
      const k = `${ck.key}::${g.id}`;
      const edge = edges.get(k);
      const cur = byGene.get(g.id) || { geneId: g.id, best: 0, attempts: 0, prior: 0, prior_attempts: 0 };

      // Signal->Gene edge score (if available)
      if (edge) {
        const ex = edgeExpectedSuccess(edge, { half_life_days: 30 });
        const weighted = ex.value * ck.sim;
        if (weighted > cur.best) cur.best = weighted;
        cur.attempts = Math.max(cur.attempts, ex.total);
      }

      // Gene->Outcome prior (independent of signal): stabilizer when signal edges are sparse.
      const gEdge = geneOutcomes.get(String(g.id));
      if (gEdge) {
        const gx = edgeExpectedSuccess(gEdge, { half_life_days: 45 });
        cur.prior = Math.max(cur.prior, gx.value);
        cur.prior_attempts = Math.max(cur.prior_attempts, gx.total);
      }

      byGene.set(g.id, cur);
    }
  }

  for (const [geneId, info] of byGene.entries()) {
    const combined = info.best > 0 ? info.best + info.prior * 0.12 : info.prior * 0.4;
    scoredGeneIds.push({ geneId, score: combined, attempts: info.attempts, prior: info.prior });
    // Low-efficiency path suppression (unless drift is explicit).
    if (!driftEnabled && info.attempts >= 2 && info.best < 0.18) {
      bannedGeneIds.add(geneId);
    }
    // Also suppress genes with consistently poor global outcomes when signal edges are sparse.
    if (!driftEnabled && info.attempts < 2 && info.prior_attempts >= 3 && info.prior < 0.12) {
      bannedGeneIds.add(geneId);
    }
  }

  scoredGeneIds.sort((a, b) => b.score - a.score);
  const preferredGeneId = scoredGeneIds.length ? scoredGeneIds[0].geneId : null;

  const explanation = [];
  if (preferredGeneId) explanation.push(`memory_prefer:${preferredGeneId}`);
  if (bannedGeneIds.size) explanation.push(`memory_ban:${Array.from(bannedGeneIds).slice(0, 6).join(',')}`);
  if (preferredGeneId) {
    const top = scoredGeneIds.find(x => x && x.geneId === preferredGeneId);
    if (top && Number.isFinite(Number(top.prior)) && top.prior > 0) explanation.push(`gene_prior:${top.prior.toFixed(3)}`);
  }
  if (driftEnabled) explanation.push('random_drift:enabled');

  return {
    currentSignalKey: curKey,
    preferredGeneId,
    bannedGeneIds,
    explanation,
  };
}

function recordSignalSnapshot({ signals, observations }) {
  const signalKey = computeSignalKey(signals);
  const ts = nowIso();
  const errsig = extractErrorSignatureFromSignals(signals);
  const ev = {
    type: 'MemoryGraphEvent',
    kind: 'signal',
    id: `mge_${Date.now()}_${stableHash(`${signalKey}|signal|${ts}`)}`,
    ts,
    signal: {
      key: signalKey,
      signals: Array.isArray(signals) ? signals : [],
      error_signature: errsig || null,
    },
    observed: observations && typeof observations === 'object' ? observations : null,
  };
  appendJsonl(memoryGraphPath(), ev);
  return ev;
}

function buildHypothesisText({ signalKey, signals, geneId, geneCategory, driftEnabled }) {
  const sigCount = Array.isArray(signals) ? signals.length : 0;
  const drift = driftEnabled ? 'drift' : 'directed';
  const g = geneId ? `${geneId}${geneCategory ? `(${geneCategory})` : ''}` : '(none)';
  return `Given signal_key=${signalKey} with ${sigCount} signals, selecting gene=${g} under mode=${drift} is expected to reduce repeated errors and improve stability.`;
}

function recordHypothesis({
  signals,
  mutation,
  personality_state,
  selectedGene,
  selector,
  driftEnabled,
  selectedBy,
  capsulesUsed,
  observations,
}) {
  const signalKey = computeSignalKey(signals);
  const geneId = selectedGene && selectedGene.id ? String(selectedGene.id) : null;
  const geneCategory = selectedGene && selectedGene.category ? String(selectedGene.category) : null;
  const ts = nowIso();
  const errsig = extractErrorSignatureFromSignals(signals);
  const hypothesisId = `hyp_${Date.now()}_${stableHash(`${signalKey}|${geneId || 'none'}|${ts}`)}`;
  const personalityState = personality_state || null;
  const mutNorm = mutation && isValidMutation(mutation) ? normalizeMutation(mutation) : null;
  const psNorm = personalityState && isValidPersonalityState(personalityState) ? normalizePersonalityState(personalityState) : null;
  const ev = {
    type: 'MemoryGraphEvent',
    kind: 'hypothesis',
    id: `mge_${Date.now()}_${stableHash(`${hypothesisId}|${ts}`)}`,
    ts,
    signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [], error_signature: errsig || null },
    hypothesis: {
      id: hypothesisId,
      text: buildHypothesisText({ signalKey, signals, geneId, geneCategory, driftEnabled }),
      predicted_outcome: { status: null, score: null },
    },
    mutation: mutNorm
      ? {
          id: mutNorm.id,
          category: mutNorm.category,
          trigger_signals: mutNorm.trigger_signals,
          target: mutNorm.target,
          expected_effect: mutNorm.expected_effect,
          risk_level: mutNorm.risk_level,
        }
      : null,
    personality: psNorm
      ? {
          key: personalityKey(psNorm),
          state: psNorm,
        }
      : null,
    gene: { id: geneId, category: geneCategory },
    action: {
      drift: !!driftEnabled,
      selected_by: selectedBy || 'selector',
      selector: selector || null,
    },
    capsules: {
      used: Array.isArray(capsulesUsed) ? capsulesUsed.map(String).filter(Boolean) : [],
    },
    observed: observations && typeof observations === 'object' ? observations : null,
  };
  appendJsonl(memoryGraphPath(), ev);
  return { hypothesisId, signalKey };
}

function hasErrorSignal(signals) {
  const list = Array.isArray(signals) ? signals : [];
  return list.includes('log_error');
}

function recordAttempt({
  signals,
  mutation,
  personality_state,
  selectedGene,
  selector,
  driftEnabled,
  selectedBy,
  hypothesisId,
  capsulesUsed,
  observations,
}) {
  const signalKey = computeSignalKey(signals);
  const geneId = selectedGene && selectedGene.id ? String(selectedGene.id) : null;
  const geneCategory = selectedGene && selectedGene.category ? String(selectedGene.category) : null;
  const ts = nowIso();
  const errsig = extractErrorSignatureFromSignals(signals);
  const actionId = `act_${Date.now()}_${stableHash(`${signalKey}|${geneId || 'none'}|${ts}`)}`;
  const personalityState = personality_state || null;
  const mutNorm = mutation && isValidMutation(mutation) ? normalizeMutation(mutation) : null;
  const psNorm = personalityState && isValidPersonalityState(personalityState) ? normalizePersonalityState(personalityState) : null;
  const ev = {
    type: 'MemoryGraphEvent',
    kind: 'attempt',
    id: `mge_${Date.now()}_${stableHash(actionId)}`,
    ts,
    signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [], error_signature: errsig || null },
    mutation: mutNorm
      ? {
          id: mutNorm.id,
          category: mutNorm.category,
          trigger_signals: mutNorm.trigger_signals,
          target: mutNorm.target,
          expected_effect: mutNorm.expected_effect,
          risk_level: mutNorm.risk_level,
        }
      : null,
    personality: psNorm
      ? {
          key: personalityKey(psNorm),
          state: psNorm,
        }
      : null,
    gene: { id: geneId, category: geneCategory },
    hypothesis: hypothesisId ? { id: String(hypothesisId) } : null,
    action: {
      id: actionId,
      drift: !!driftEnabled,
      selected_by: selectedBy || 'selector',
      selector: selector || null,
    },
    capsules: {
      used: Array.isArray(capsulesUsed) ? capsulesUsed.map(String).filter(Boolean) : [],
    },
    observed: observations && typeof observations === 'object' ? observations : null,
  };

  appendJsonl(memoryGraphPath(), ev);

  // State is mutable; graph is append-only.
  const statePath = memoryGraphStatePath();
  const state = readJsonIfExists(statePath, { last_action: null });
  state.last_action = {
    action_id: actionId,
    signal_key: signalKey,
    signals: Array.isArray(signals) ? signals : [],
    mutation_id: mutNorm ? mutNorm.id : null,
    mutation_category: mutNorm ? mutNorm.category : null,
    mutation_risk_level: mutNorm ? mutNorm.risk_level : null,
    personality_key: psNorm ? personalityKey(psNorm) : null,
    personality_state: psNorm || null,
    gene_id: geneId,
    gene_category: geneCategory,
    hypothesis_id: hypothesisId ? String(hypothesisId) : null,
    capsules_used: Array.isArray(capsulesUsed) ? capsulesUsed.map(String).filter(Boolean) : [],
    had_error: hasErrorSignal(signals),
    created_at: ts,
    outcome_recorded: false,
    baseline_observed: observations && typeof observations === 'object' ? observations : null,
  };
  writeJsonAtomic(statePath, state);

  return { actionId, signalKey };
}

function inferOutcomeFromSignals({ prevHadError, currentHasError }) {
  if (prevHadError && !currentHasError) return { status: 'success', score: 0.85, note: 'error_cleared' };
  if (prevHadError && currentHasError) return { status: 'failed', score: 0.2, note: 'error_persisted' };
  if (!prevHadError && currentHasError) return { status: 'failed', score: 0.15, note: 'new_error_appeared' };
  return { status: 'success', score: 0.6, note: 'stable_no_error' };
}

function clamp01(x) {
  const n = Number(x);
  if (!Number.isFinite(n)) return 0;
  return Math.max(0, Math.min(1, n));
}

function tryParseLastEvolutionEventOutcome(evidenceText) {
  // Scan tail text for an EvolutionEvent JSON line and extract its outcome.
  const s = String(evidenceText || '');
  if (!s) return null;
  const lines = s.split('\n').slice(-400);
  for (let i = lines.length - 1; i >= 0; i--) {
    const line = lines[i].trim();
    if (!line) continue;
    if (!line.includes('"type"') || !line.includes('EvolutionEvent')) continue;
    try {
      const obj = JSON.parse(line);
      if (!obj || obj.type !== 'EvolutionEvent') continue;
      const o = obj.outcome && typeof obj.outcome === 'object' ? obj.outcome : null;
      if (!o) continue;
      const status = o.status === 'success' || o.status === 'failed' ? o.status : null;
      const score = Number.isFinite(Number(o.score)) ? clamp01(Number(o.score)) : null;
      if (!status && score == null) continue;
      return {
        status: status || (score != null && score >= 0.5 ? 'success' : 'failed'),
        score: score != null ? score : status === 'success' ? 0.75 : 0.25,
        note: 'evolutionevent_observed',
      };
    } catch (e) {
      continue;
    }
  }
  return null;
}

function inferOutcomeEnhanced({ prevHadError, currentHasError, baselineObserved, currentObserved }) {
  const evidence =
    currentObserved &&
    currentObserved.evidence &&
    (currentObserved.evidence.recent_session_tail || currentObserved.evidence.today_log_tail)
      ? currentObserved.evidence
      : null;
  const combinedEvidence = evidence
    ? `${String(evidence.recent_session_tail || '')}\n${String(evidence.today_log_tail || '')}`
    : '';
  const observed = tryParseLastEvolutionEventOutcome(combinedEvidence);
  if (observed) return observed;

  const base = inferOutcomeFromSignals({ prevHadError, currentHasError });

  const prevErrCount =
    baselineObserved && Number.isFinite(Number(baselineObserved.recent_error_count))
      ? Number(baselineObserved.recent_error_count)
      : null;
  const curErrCount =
    currentObserved && Number.isFinite(Number(currentObserved.recent_error_count))
      ? Number(currentObserved.recent_error_count)
      : null;

  let score = base.score;
  if (prevErrCount != null && curErrCount != null) {
    const delta = prevErrCount - curErrCount;
    score += Math.max(-0.12, Math.min(0.12, delta / 50));
  }

  const prevScan =
    baselineObserved && Number.isFinite(Number(baselineObserved.scan_ms)) ? Number(baselineObserved.scan_ms) : null;
  const curScan =
    currentObserved && Number.isFinite(Number(currentObserved.scan_ms)) ? Number(currentObserved.scan_ms) : null;
  if (prevScan != null && curScan != null && prevScan > 0) {
    const ratio = (prevScan - curScan) / prevScan;
    score += Math.max(-0.06, Math.min(0.06, ratio));
  }

  return { status: base.status, score: clamp01(score), note: `${base.note}|heuristic_delta` };
}

function buildConfidenceEdgeEvent({ signalKey, signals, geneId, geneCategory, outcomeEventId, halfLifeDays }) {
  const events = tryReadMemoryGraphEvents(2000);
  const edges = aggregateEdges(events);
  const k = `${signalKey}::${geneId}`;
  const edge = edges.get(k) || { success: 0, fail: 0, last_ts: null };
  const ex = edgeExpectedSuccess(edge, { half_life_days: halfLifeDays });
  const ts = nowIso();
  return {
    type: 'MemoryGraphEvent',
    kind: 'confidence_edge',
    id: `mge_${Date.now()}_${stableHash(`${signalKey}|${geneId}|confidence|${ts}`)}`,
    ts,
    signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [] },
    gene: { id: geneId, category: geneCategory || null },
    edge: { signal_key: signalKey, gene_id: geneId },
    stats: {
      success: Number(edge.success) || 0,
      fail: Number(edge.fail) || 0,
      attempts: Number(ex.total) || 0,
      p: ex.p,
      decay_weight: ex.w,
      value: ex.value,
      half_life_days: halfLifeDays,
      updated_at: ts,
    },
    derived_from: { outcome_event_id: outcomeEventId || null },
  };
}

function buildGeneOutcomeConfidenceEvent({ geneId, geneCategory, outcomeEventId, halfLifeDays }) {
  const events = tryReadMemoryGraphEvents(2000);
  const geneOutcomes = aggregateGeneOutcomes(events);
  const edge = geneOutcomes.get(String(geneId)) || { success: 0, fail: 0, last_ts: null };
  const ex = edgeExpectedSuccess(edge, { half_life_days: halfLifeDays });
  const ts = nowIso();
  return {
    type: 'MemoryGraphEvent',
    kind: 'confidence_gene_outcome',
    id: `mge_${Date.now()}_${stableHash(`${geneId}|gene_outcome|confidence|${ts}`)}`,
    ts,
    gene: { id: String(geneId), category: geneCategory || null },
    edge: { gene_id: String(geneId) },
    stats: {
      success: Number(edge.success) || 0,
      fail: Number(edge.fail) || 0,
      attempts: Number(ex.total) || 0,
      p: ex.p,
      decay_weight: ex.w,
      value: ex.value,
      half_life_days: halfLifeDays,
      updated_at: ts,
    },
    derived_from: { outcome_event_id: outcomeEventId || null },
  };
}

function recordOutcomeFromState({ signals, observations }) {
  const statePath = memoryGraphStatePath();
  const state = readJsonIfExists(statePath, { last_action: null });
  const last = state && state.last_action ? state.last_action : null;
  if (!last || !last.action_id) return null;
  if (last.outcome_recorded) return null;

  const currentHasError = hasErrorSignal(signals);
  const inferred = inferOutcomeEnhanced({
    prevHadError: !!last.had_error,
    currentHasError,
    baselineObserved: last.baseline_observed || null,
    currentObserved: observations || null,
  });
  const ts = nowIso();
  const errsig = extractErrorSignatureFromSignals(signals);
  const ev = {
    type: 'MemoryGraphEvent',
    kind: 'outcome',
    id: `mge_${Date.now()}_${stableHash(`${last.action_id}|outcome|${ts}`)}`,
    ts,
    signal: {
      key: String(last.signal_key || '(none)'),
      signals: Array.isArray(last.signals) ? last.signals : [],
      error_signature: errsig || null,
    },
    mutation:
      last.mutation_id || last.mutation_category || last.mutation_risk_level
        ? {
            id: last.mutation_id || null,
            category: last.mutation_category || null,
            risk_level: last.mutation_risk_level || null,
          }
        : null,
    personality:
      last.personality_key || last.personality_state
        ? {
            key: last.personality_key || null,
            state: last.personality_state || null,
          }
        : null,
    gene: { id: last.gene_id || null, category: last.gene_category || null },
    action: { id: String(last.action_id) },
    hypothesis: last.hypothesis_id ? { id: String(last.hypothesis_id) } : null,
    outcome: {
      status: inferred.status,
      score: inferred.score,
      note: inferred.note,
      observed: { current_signals: Array.isArray(signals) ? signals : [] },
    },
    confidence: {
      // This is an interpretable, decayed success estimate derived from outcomes; aggregation is computed at read-time.
      half_life_days: 30,
    },
    observed: observations && typeof observations === 'object' ? observations : null,
    baseline: last.baseline_observed || null,
    capsules: {
      used: Array.isArray(last.capsules_used) ? last.capsules_used : [],
    },
  };

  appendJsonl(memoryGraphPath(), ev);

  // Persist explicit confidence snapshots (append-only) for auditability.
  try {
    if (last.gene_id) {
      const edgeEv = buildConfidenceEdgeEvent({
        signalKey: String(last.signal_key || '(none)'),
        signals: Array.isArray(last.signals) ? last.signals : [],
        geneId: String(last.gene_id),
        geneCategory: last.gene_category || null,
        outcomeEventId: ev.id,
        halfLifeDays: 30,
      });
      appendJsonl(memoryGraphPath(), edgeEv);

      const geneEv = buildGeneOutcomeConfidenceEvent({
        geneId: String(last.gene_id),
        geneCategory: last.gene_category || null,
        outcomeEventId: ev.id,
        halfLifeDays: 45,
      });
      appendJsonl(memoryGraphPath(), geneEv);
    }
  } catch (e) {}

  last.outcome_recorded = true;
  last.outcome_recorded_at = ts;
  state.last_action = last;
  writeJsonAtomic(statePath, state);

  return ev;
}

function recordExternalCandidate({ asset, source, signals }) {
  // Append-only annotation: external assets enter as candidates only.
  // This does not affect outcome aggregation (which only uses kind === 'outcome').
  const a = asset && typeof asset === 'object' ? asset : null;
  const type = a && a.type ? String(a.type) : null;
  const id = a && a.id ? String(a.id) : null;
  if (!type || !id) return null;

  const ts = nowIso();
  const signalKey = computeSignalKey(signals);
  const ev = {
    type: 'MemoryGraphEvent',
    kind: 'external_candidate',
    id: `mge_${Date.now()}_${stableHash(`${type}|${id}|external|${ts}`)}`,
    ts,
    signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [] },
    external: {
      source: source || 'external',
      received_at: ts,
    },
    asset: { type, id },
    candidate: {
      // Minimal hints for later local triggering/validation.
      trigger: type === 'Capsule' && Array.isArray(a.trigger) ? a.trigger : [],
      gene: type === 'Capsule' && a.gene ? String(a.gene) : null,
      confidence: type === 'Capsule' && Number.isFinite(Number(a.confidence)) ? Number(a.confidence) : null,
    },
  };

  appendJsonl(memoryGraphPath(), ev);
  return ev;
}

module.exports = {
  memoryGraphPath,
  computeSignalKey,
  tryReadMemoryGraphEvents,
  getMemoryAdvice,
  recordSignalSnapshot,
  recordHypothesis,
  recordAttempt,
  recordOutcomeFromState,
  recordExternalCandidate,
};

