#!/usr/bin/env node
/**
 * Intelligence Layer (ADR-050)
 *
 * Closes the intelligence loop by wiring PageRank-ranked memory into
 * the hook system. Pure CJS — no ESM imports of @claude-flow/memory.
 *
 * Data files (all under .claude-flow/data/):
 *   auto-memory-store.json  — written by auto-memory-hook.mjs
 *   graph-state.json        — serialized graph (nodes + edges + pageRanks)
 *   ranked-context.json     — pre-computed ranked entries for fast lookup
 *   pending-insights.jsonl  — append-only edit/task log
 */

'use strict';

const fs = require('fs');
const path = require('path');

const DATA_DIR = path.join(process.cwd(), '.claude-flow', 'data');
const STORE_PATH = path.join(DATA_DIR, 'auto-memory-store.json');
const GRAPH_PATH = path.join(DATA_DIR, 'graph-state.json');
const RANKED_PATH = path.join(DATA_DIR, 'ranked-context.json');
const PENDING_PATH = path.join(DATA_DIR, 'pending-insights.jsonl');
const SESSION_DIR = path.join(process.cwd(), '.claude-flow', 'sessions');
const SESSION_FILE = path.join(SESSION_DIR, 'current.json');

// ── Safety limits (fixes #1530, #1531) ─────────────────────────────────────
const MAX_DATA_FILE_SIZE = 10 * 1024 * 1024; // 10 MB — skip files larger than this
const MAX_GRAPH_NODES = 5000;                 // skip PageRank if graph exceeds this

// ── Stop words for trigram matching ──────────────────────────────────────────

const STOP_WORDS = new Set([
  'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
  'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
  'should', 'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'for',
  'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
  'before', 'after', 'and', 'but', 'or', 'nor', 'not', 'so', 'yet',
  'both', 'either', 'neither', 'each', 'every', 'all', 'any', 'few',
  'more', 'most', 'other', 'some', 'such', 'no', 'only', 'own', 'same',
  'than', 'too', 'very', 'just', 'because', 'if', 'when', 'which',
  'who', 'whom', 'this', 'that', 'these', 'those', 'it', 'its',
]);

// ── Helpers ──────────────────────────────────────────────────────────────────

function ensureDataDir() {
  if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
}

function readJSON(filePath) {
  // Safety: skip files exceeding MAX_DATA_FILE_SIZE (#1531)
  try {
    const stat = fs.statSync(filePath);
    if (stat.size > MAX_DATA_FILE_SIZE) {
      process.stderr.write("[INTELLIGENCE] WARN: Skipping " + path.basename(filePath) + " (" + Math.round(stat.size / 1048576) + "MB exceeds 10MB limit)\n");
      return null;
    }
  } catch { /* file may not exist yet */ }
  try {
    if (fs.existsSync(filePath)) return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
  } catch { /* corrupt file — start fresh */ }
  return null;
}

function writeJSON(filePath, data) {
  ensureDataDir();
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
}

function tokenize(text) {
  if (!text) return [];
  return text.toLowerCase()
    .replace(/[^a-z0-9\s-]/g, ' ')
    .split(/\s+/)
    .filter(w => w.length > 2 && !STOP_WORDS.has(w));
}

function trigrams(words) {
  const t = new Set();
  for (const w of words) {
    for (let i = 0; i <= w.length - 3; i++) t.add(w.slice(i, i + 3));
  }
  return t;
}

function jaccardSimilarity(setA, setB) {
  if (setA.size === 0 && setB.size === 0) return 0;
  let intersection = 0;
  for (const item of setA) { if (setB.has(item)) intersection++; }
  return intersection / (setA.size + setB.size - intersection);
}

// ── Deduplication helper (fixes #1518) ──────────────────────────────────────

function deduplicateById(entries) {
  if (!entries || !Array.isArray(entries)) return entries;
  const seen = new Map();
  for (const entry of entries) {
    const id = entry.id || entry.key;
    if (id) {
      seen.set(id, entry);
    } else {
      seen.set(`__no_id_${seen.size}`, entry);
    }
  }
  return Array.from(seen.values());
}

// ADR-095 G6 — content-hash dedup. The April audit measured 5,706 entries
// in the auto-memory store with only ~20 unique by content; 5,686 dupes
// were the same MEMORY.md sections imported from sibling project dirs
// with different IDs. deduplicateById can't catch these (the IDs really
// are different); we need a content fingerprint.
//
// Fast non-cryptographic fingerprint — collisions on 64-bit FNV-1a are
// vanishingly rare for human prose at the scale of an auto-memory store.
// Whitespace-normalized so trivially-different formatting doesn't bypass dedup.
function fingerprintContent(text) {
  if (typeof text !== 'string' || text.length === 0) return '0';
  const norm = text.replace(/\s+/g, ' ').trim().toLowerCase();
  // FNV-1a 64-bit (split into 32-bit halves to stay within Number safe int)
  let h1 = 0x811c9dc5, h2 = 0xcbf29ce4;
  for (let i = 0; i < norm.length; i++) {
    const c = norm.charCodeAt(i);
    h1 ^= c; h1 = Math.imul(h1, 0x01000193) >>> 0;
    h2 ^= c; h2 = Math.imul(h2, 0x100000001b3 & 0xffffffff) >>> 0;
  }
  return `${h1.toString(16)}_${h2.toString(16)}_${norm.length}`;
}

function deduplicateByContent(entries) {
  if (!entries || !Array.isArray(entries)) return entries;
  const seen = new Map();
  for (const entry of entries) {
    const content = entry.content || entry.summary || entry.value || '';
    const fp = fingerprintContent(typeof content === 'string' ? content : JSON.stringify(content));
    if (!seen.has(fp)) {
      seen.set(fp, entry);
    } else {
      // Keep the entry with the higher accessCount or earlier createdAt
      const existing = seen.get(fp);
      const existingAccess = existing.accessCount || 0;
      const candidateAccess = entry.accessCount || 0;
      if (candidateAccess > existingAccess) seen.set(fp, entry);
    }
  }
  return Array.from(seen.values());
}

// ── Session state helpers ────────────────────────────────────────────────────

function sessionGet(key) {
  try {
    if (!fs.existsSync(SESSION_FILE)) return null;
    const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
    return key ? (session.context || {})[key] : session.context;
  } catch { return null; }
}

function sessionSet(key, value) {
  try {
    if (!fs.existsSync(SESSION_DIR)) fs.mkdirSync(SESSION_DIR, { recursive: true });
    let session = {};
    if (fs.existsSync(SESSION_FILE)) {
      session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
    }
    if (!session.context) session.context = {};
    session.context[key] = value;
    session.updatedAt = new Date().toISOString();
    fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), 'utf-8');
  } catch { /* best effort */ }
}

// ── PageRank ─────────────────────────────────────────────────────────────────

function computePageRank(nodes, edges, damping, maxIter) {
  damping = damping || 0.85;
  maxIter = maxIter || 30;

  const ids = Object.keys(nodes);
  const n = ids.length;
  if (n === 0) return {};

  // Build adjacency: outgoing edges per node
  const outLinks = {};
  const inLinks = {};
  for (const id of ids) { outLinks[id] = []; inLinks[id] = []; }
  for (const edge of edges) {
    if (outLinks[edge.sourceId]) outLinks[edge.sourceId].push(edge.targetId);
    if (inLinks[edge.targetId]) inLinks[edge.targetId].push(edge.sourceId);
  }

  // Initialize ranks
  const ranks = {};
  for (const id of ids) ranks[id] = 1 / n;

  // Power iteration (with dangling node redistribution)
  for (let iter = 0; iter < maxIter; iter++) {
    const newRanks = {};
    let diff = 0;

    // Collect rank from dangling nodes (no outgoing edges)
    let danglingSum = 0;
    for (const id of ids) {
      if (outLinks[id].length === 0) danglingSum += ranks[id];
    }

    for (const id of ids) {
      let sum = 0;
      for (const src of inLinks[id]) {
        const outCount = outLinks[src].length;
        if (outCount > 0) sum += ranks[src] / outCount;
      }
      // Dangling rank distributed evenly + teleport
      newRanks[id] = (1 - damping) / n + damping * (sum + danglingSum / n);
      diff += Math.abs(newRanks[id] - ranks[id]);
    }

    for (const id of ids) ranks[id] = newRanks[id];
    if (diff < 1e-6) break; // converged
  }

  return ranks;
}

// ── Edge building ────────────────────────────────────────────────────────────

function buildEdges(entries) {
  const edges = [];
  const byCategory = {};

  for (const entry of entries) {
    const cat = entry.category || entry.namespace || 'default';
    if (!byCategory[cat]) byCategory[cat] = [];
    byCategory[cat].push(entry);
  }

  // Temporal edges: entries from same sourceFile
  const byFile = {};
  for (const entry of entries) {
    const file = (entry.metadata && entry.metadata.sourceFile) || null;
    if (file) {
      if (!byFile[file]) byFile[file] = [];
      byFile[file].push(entry);
    }
  }
  for (const file of Object.keys(byFile)) {
    const group = byFile[file];
    for (let i = 0; i < group.length - 1; i++) {
      edges.push({
        sourceId: group[i].id,
        targetId: group[i + 1].id,
        type: 'temporal',
        weight: 0.5,
      });
    }
  }

  // Similarity edges within categories (Jaccard > 0.3).
  // ADR-095 G6 perf: hoist the trigram computation outside the inner
  // loop. Previously we re-tokenized + re-trigrammed group[j] for every
  // i — O(n²) extra work for nothing. Now compute once per entry.
  for (const cat of Object.keys(byCategory)) {
    const group = byCategory[cat];
    if (group.length < 2) continue;

    // Cache trigram sets for every entry in the group.
    const triCache = new Array(group.length);
    for (let i = 0; i < group.length; i++) {
      triCache[i] = trigrams(tokenize(group[i].content || group[i].summary || ''));
    }

    for (let i = 0; i < group.length; i++) {
      const triA = triCache[i];
      for (let j = i + 1; j < group.length; j++) {
        const sim = jaccardSimilarity(triA, triCache[j]);
        if (sim > 0.3) {
          edges.push({
            sourceId: group[i].id,
            targetId: group[j].id,
            type: 'similar',
            weight: sim,
          });
        }
      }
    }
  }

  return edges;
}

// ── Bootstrap from MEMORY.md files ───────────────────────────────────────────

/**
 * If auto-memory-store.json is empty, bootstrap by parsing MEMORY.md and
 * topic files from the auto-memory directory. This removes the dependency
 * on @claude-flow/memory for the initial seed.
 */
function bootstrapFromMemoryFiles() {
  const entries = [];
  const cwd = process.cwd();

  // Search for auto-memory directories
  const candidates = [
    // Claude Code auto-memory (project-scoped)
    path.join(require('os').homedir(), '.claude', 'projects'),
    // Local project memory
    path.join(cwd, '.claude-flow', 'memory'),
    path.join(cwd, '.claude', 'memory'),
  ];

  // Find MEMORY.md in project-scoped dirs
  for (const base of candidates) {
    if (!fs.existsSync(base)) continue;

    // For the projects dir, scope to CURRENT project only (not all 51+ dirs)
    if (base.endsWith('projects')) {
      try {
        // Match Claude Code's project-dir slug: every non-alphanumeric char -> '-'
        // (e.g. "G:\\My Drive\\TJ_Vault" -> "G--My-Drive-TJ-Vault"). The old version
        // only handled POSIX '/', so on Windows the slug kept ':' and '\\' and never
        // matched the real <projects>/<slug>/memory dir — bootstrap found nothing (FIX 5).
        const projectSlug = cwd.replace(/[^a-zA-Z0-9]/g, '-');
        const memDir = path.join(base, projectSlug, 'memory');
        if (fs.existsSync(memDir)) {
          parseMemoryDir(memDir, entries);
        }
      } catch { /* skip */ }
    } else if (fs.existsSync(base)) {
      parseMemoryDir(base, entries);
    }
  }

  return entries;
}

// Truncation transparency (FIX 4): mark the cut with an ellipsis and warn under
// debug, so later reasoning isn't silently built on severed text.
const CLIP_DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
function clip(text, max, label) {
  text = text == null ? '' : String(text);
  if (text.length <= max) return text;
  if (CLIP_DEBUG) process.stderr.write(`[INTELLIGENCE] WARN: truncated ${label || 'value'} from ${text.length} to ${max} chars\n`);
  return text.slice(0, max - 1) + '…';
}

function parseMemoryDir(dir, entries) {
  try {
    const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
    for (const file of files) {
      const filePath = path.join(dir, file);
      const content = fs.readFileSync(filePath, 'utf-8');
      if (!content.trim()) continue;

      // Parse markdown sections as separate entries
      const sections = content.split(/^##?\s+/m).filter(Boolean);
      for (let sIdx = 0; sIdx < sections.length; sIdx++) {
        const section = sections[sIdx];
        const lines = section.trim().split('\n');
        const title = lines[0].trim();
        const body = lines.slice(1).join('\n').trim();
        if (!body || body.length < 10) continue;

        const id = `mem-${file.replace('.md', '')}-${title.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 30)}-${sIdx}`;
        entries.push({
          id,
          key: title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50),
          content: clip(body, 500, 'memory content'),
          summary: title,
          namespace: file === 'MEMORY.md' ? 'core' : file.replace('.md', ''),
          type: 'semantic',
          metadata: { sourceFile: filePath, bootstrapped: true },
          createdAt: Date.now(),
        });
      }
    }
  } catch { /* skip unreadable dirs */ }
}

// ── Exported functions ───────────────────────────────────────────────────────

/**
 * init() — Called from session-restore. Budget: <200ms.
 * Reads auto-memory-store.json, builds graph, computes PageRank, writes caches.
 * If store is empty, bootstraps from MEMORY.md files directly.
 */
function init() {
  ensureDataDir();

  // Check if graph-state.json is fresh (within 60s of store)
  const graphState = readJSON(GRAPH_PATH);
  let store = readJSON(STORE_PATH);

  // Bootstrap from MEMORY.md files if store is empty
  if (!store || !Array.isArray(store) || store.length === 0) {
    const bootstrapped = bootstrapFromMemoryFiles();
    if (bootstrapped.length > 0) {
      store = bootstrapped;
      writeJSON(STORE_PATH, store);
    } else {
      return { nodes: 0, edges: 0, message: 'No memory entries to index' };
    }
  }

  // Deduplicate store entries by ID (fixes #1518 — 194MB → ~79KB)
  let deduped = deduplicateById(store);
  // ADR-095 G6: also dedupe by content fingerprint. The April audit
  // measured 5,706 entries with only ~20 unique by content because the
  // same MEMORY.md sections get imported from sibling project dirs with
  // different IDs. deduplicateById can't catch that; deduplicateByContent
  // can. Cuts the graph from O(n²) over near-identical duplicates down
  // to O(unique²), which is the difference between a 100MB graph-state
  // and a kilobytes-scale one for typical workloads.
  const beforeContentDedup = deduped.length;
  deduped = deduplicateByContent(deduped);
  if (deduped.length < store.length) {
    process.stderr.write(
      `[INTELLIGENCE] Deduped store: ${store.length} -> ${deduped.length} entries ` +
      `(by-id: ${store.length - beforeContentDedup} dropped, by-content: ${beforeContentDedup - deduped.length} dropped)\n`
    );
    writeJSON(STORE_PATH, deduped);
  }

  // Skip rebuild if graph is fresh and store hasn't changed
  if (graphState && graphState.nodeCount === deduped.length) {
    const age = Date.now() - (graphState.updatedAt || 0);
    if (age < 60000) {
      return {
        nodes: graphState.nodeCount || Object.keys(graphState.nodes || {}).length,
        edges: (graphState.edges || []).length,
        message: 'Graph cache hit',
      };
    }
  }

  // Build nodes from deduped entries
  const nodes = {};
  for (const entry of deduped) {
    const id = entry.id || entry.key || `entry-${Math.random().toString(36).slice(2, 8)}`;
    nodes[id] = {
      id,
      category: entry.namespace || entry.type || 'default',
      confidence: (entry.metadata && entry.metadata.confidence) || 0.5,
      accessCount: (entry.metadata && entry.metadata.accessCount) || 0,
      createdAt: entry.createdAt || Date.now(),
    };
    // Ensure entry has id for edge building
    entry.id = id;
  }

  // Build edges
  const edges = buildEdges(deduped);

  // Compute PageRank (skip if graph too large — #1531)
  const nodeCount = Object.keys(nodes).length;
  let pageRanks = {};
  if (nodeCount > MAX_GRAPH_NODES) {
    process.stderr.write("[INTELLIGENCE] WARN: Graph has " + nodeCount + " nodes (>" + MAX_GRAPH_NODES + "), skipping PageRank\n");
    for (const id of Object.keys(nodes)) pageRanks[id] = 1 / nodeCount;
  } else {
    pageRanks = computePageRank(nodes, edges, 0.85, 30);
  }

  // Write graph state
  const graph = {
    version: 1,
    updatedAt: Date.now(),
    nodeCount: Object.keys(nodes).length,
    nodes,
    edges,
    pageRanks,
  };
  writeJSON(GRAPH_PATH, graph);

  // Build ranked context for fast lookup
  const rankedEntries = deduped.map(entry => {
    const id = entry.id;
    const content = entry.content || entry.value || '';
    const summary = entry.summary || entry.key || '';
    const words = tokenize(content + ' ' + summary);
    return {
      id,
      content,
      summary,
      category: entry.namespace || entry.type || 'default',
      confidence: nodes[id] ? nodes[id].confidence : 0.5,
      pageRank: pageRanks[id] || 0,
      accessCount: nodes[id] ? nodes[id].accessCount : 0,
      words,
    };
  }).sort((a, b) => {
    const scoreA = 0.6 * a.pageRank + 0.4 * a.confidence;
    const scoreB = 0.6 * b.pageRank + 0.4 * b.confidence;
    return scoreB - scoreA;
  });

  const ranked = {
    version: 1,
    computedAt: Date.now(),
    entries: rankedEntries,
  };
  writeJSON(RANKED_PATH, ranked);

  return {
    nodes: Object.keys(nodes).length,
    edges: edges.length,
    message: 'Graph built and ranked',
  };
}

/**
 * getContext(prompt) — Called from route. Budget: <15ms.
 * Matches prompt to ranked entries, returns top-5 formatted context.
 */
function getContext(prompt) {
  if (!prompt) return null;

  const ranked = readJSON(RANKED_PATH);
  if (!ranked || !ranked.entries || ranked.entries.length === 0) return null;

  const promptWords = tokenize(prompt);
  if (promptWords.length === 0) return null;
  const promptTrigrams = trigrams(promptWords);

  const ALPHA = 0.6; // content match weight
  const MIN_THRESHOLD = 0.05;
  const TOP_K = 5;

  // Score each entry
  const scored = [];
  for (const entry of ranked.entries) {
    const entryTrigrams = trigrams(entry.words || []);
    const contentMatch = jaccardSimilarity(promptTrigrams, entryTrigrams);
    const score = ALPHA * contentMatch + (1 - ALPHA) * (entry.pageRank || 0);
    if (score >= MIN_THRESHOLD) {
      scored.push({ ...entry, score });
    }
  }

  if (scored.length === 0) return null;

  // Sort by score descending, take top-K
  scored.sort((a, b) => b.score - a.score);
  const topEntries = scored.slice(0, TOP_K);

  // Boost previously matched patterns (implicit success: user continued working)
  const prevMatched = sessionGet('lastMatchedPatterns');

  // Store NEW matched IDs in session state for feedback
  const matchedIds = topEntries.map(e => e.id);
  sessionSet('lastMatchedPatterns', matchedIds);

  // Only boost previous if they differ from current (avoid double-boosting)
  if (prevMatched && Array.isArray(prevMatched)) {
    const newSet = new Set(matchedIds);
    const toBoost = prevMatched.filter(id => !newSet.has(id));
    if (toBoost.length > 0) boostConfidence(toBoost, 0.03);
  }

  // Format output
  const lines = ['[INTELLIGENCE] Relevant patterns for this task:'];
  for (let i = 0; i < topEntries.length; i++) {
    const e = topEntries[i];
    const display = (e.summary || e.content || '').slice(0, 80);
    const accessed = e.accessCount || 0;
    lines.push(`  * (${e.score.toFixed(2)}) ${display} [rank #${i + 1}, ${accessed}x accessed]`);
  }

  return lines.join('\n');
}

/**
 * recordEdit(file) — Called from post-edit. Budget: <2ms.
 * Appends to pending-insights.jsonl.
 */
function recordEdit(file) {
  ensureDataDir();
  const entry = JSON.stringify({
    type: 'edit',
    file: file || 'unknown',
    timestamp: Date.now(),
    sessionId: sessionGet('sessionId') || null,
  });
  fs.appendFileSync(PENDING_PATH, entry + '\n', 'utf-8');
}

/**
 * feedback(success) — Called from post-task. Budget: <10ms.
 * Boosts or decays confidence for last-matched patterns.
 */
function feedback(success) {
  const matchedIds = sessionGet('lastMatchedPatterns');
  if (!matchedIds || !Array.isArray(matchedIds)) return;

  const amount = success ? 0.05 : -0.02;
  boostConfidence(matchedIds, amount);
}

function boostConfidence(ids, amount) {
  const ranked = readJSON(RANKED_PATH);
  if (!ranked || !ranked.entries) return;

  let changed = false;
  for (const entry of ranked.entries) {
    if (ids.includes(entry.id)) {
      entry.confidence = Math.max(0, Math.min(1, (entry.confidence || 0.5) + amount));
      entry.accessCount = (entry.accessCount || 0) + 1;
      changed = true;
    }
  }

  if (changed) writeJSON(RANKED_PATH, ranked);

  // Also update graph-state confidence
  const graph = readJSON(GRAPH_PATH);
  if (graph && graph.nodes) {
    for (const id of ids) {
      if (graph.nodes[id]) {
        graph.nodes[id].confidence = Math.max(0, Math.min(1, (graph.nodes[id].confidence || 0.5) + amount));
        graph.nodes[id].accessCount = (graph.nodes[id].accessCount || 0) + 1;
      }
    }
    writeJSON(GRAPH_PATH, graph);
  }
}

/**
 * consolidate() — Called from session-end. Budget: <500ms.
 * Processes pending insights, rebuilds edges, recomputes PageRank.
 */
function consolidate() {
  ensureDataDir();

  let store = readJSON(STORE_PATH);
  if (!store || !Array.isArray(store)) {
    return { entries: 0, edges: 0, newEntries: 0, message: 'No store to consolidate' };
  }

  // Deduplicate store entries by ID before processing (fixes #1518)
  const preDedupCount = store.length;
  store = deduplicateById(store);

  // 1. Process pending insights
  let newEntries = 0;
  if (fs.existsSync(PENDING_PATH)) {
    const lines = fs.readFileSync(PENDING_PATH, 'utf-8').trim().split('\n').filter(Boolean);
    const editCounts = {};
    for (const line of lines) {
      try {
        const insight = JSON.parse(line);
        if (insight.file) {
          editCounts[insight.file] = (editCounts[insight.file] || 0) + 1;
        }
      } catch { /* skip malformed */ }
    }

    // Create entries for frequently-edited files (3+ edits)
    for (const [file, count] of Object.entries(editCounts)) {
      if (count >= 3) {
        const exists = store.some(e =>
          (e.metadata && e.metadata.sourceFile === file && e.metadata.autoGenerated)
        );
        if (!exists) {
          store.push({
            id: `insight-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
            key: `frequent-edit-${path.basename(file)}`,
            content: `File ${file} was edited ${count} times this session — likely a hot path worth monitoring.`,
            summary: `Frequently edited: ${path.basename(file)} (${count}x)`,
            namespace: 'insights',
            type: 'procedural',
            metadata: { sourceFile: file, editCount: count, autoGenerated: true },
            createdAt: Date.now(),
          });
          newEntries++;
        }
      }
    }

    // Clear pending
    fs.writeFileSync(PENDING_PATH, '', 'utf-8');
  }

  // 2. Confidence decay for unaccessed entries
  const graph = readJSON(GRAPH_PATH);
  if (graph && graph.nodes) {
    const now = Date.now();
    for (const id of Object.keys(graph.nodes)) {
      const node = graph.nodes[id];
      const hoursSinceCreation = (now - (node.createdAt || now)) / (1000 * 60 * 60);
      if (node.accessCount === 0 && hoursSinceCreation > 24) {
        node.confidence = Math.max(0.05, (node.confidence || 0.5) - 0.005 * Math.floor(hoursSinceCreation / 24));
      }
    }
  }

  // 3. Rebuild edges with updated store
  for (const entry of store) {
    if (!entry.id) entry.id = `entry-${Math.random().toString(36).slice(2, 8)}`;
  }
  const edges = buildEdges(store);

  // 4. Build updated nodes
  const nodes = {};
  for (const entry of store) {
    nodes[entry.id] = {
      id: entry.id,
      category: entry.namespace || entry.type || 'default',
      confidence: (graph && graph.nodes && graph.nodes[entry.id])
        ? graph.nodes[entry.id].confidence
        : (entry.metadata && entry.metadata.confidence) || 0.5,
      accessCount: (graph && graph.nodes && graph.nodes[entry.id])
        ? graph.nodes[entry.id].accessCount
        : (entry.metadata && entry.metadata.accessCount) || 0,
      createdAt: entry.createdAt || Date.now(),
    };
  }

  // 5. Recompute PageRank (skip if graph too large — #1531)
  const nodeCount = Object.keys(nodes).length;
  let pageRanks = {};
  if (nodeCount > MAX_GRAPH_NODES) {
    process.stderr.write("[INTELLIGENCE] WARN: Graph has " + nodeCount + " nodes (>" + MAX_GRAPH_NODES + "), skipping PageRank in consolidate\n");
    for (const id of Object.keys(nodes)) pageRanks[id] = 1 / nodeCount;
  } else {
    pageRanks = computePageRank(nodes, edges, 0.85, 30);
  }

  // 6. Write updated graph
  writeJSON(GRAPH_PATH, {
    version: 1,
    updatedAt: Date.now(),
    nodeCount: Object.keys(nodes).length,
    nodes,
    edges,
    pageRanks,
  });

  // 7. Write updated ranked context
  const rankedEntries = store.map(entry => {
    const id = entry.id;
    const content = entry.content || entry.value || '';
    const summary = entry.summary || entry.key || '';
    const words = tokenize(content + ' ' + summary);
    return {
      id,
      content,
      summary,
      category: entry.namespace || entry.type || 'default',
      confidence: nodes[id] ? nodes[id].confidence : 0.5,
      pageRank: pageRanks[id] || 0,
      accessCount: nodes[id] ? nodes[id].accessCount : 0,
      words,
    };
  }).sort((a, b) => {
    const scoreA = 0.6 * a.pageRank + 0.4 * a.confidence;
    const scoreB = 0.6 * b.pageRank + 0.4 * b.confidence;
    return scoreB - scoreA;
  });

  writeJSON(RANKED_PATH, {
    version: 1,
    computedAt: Date.now(),
    entries: rankedEntries,
  });

  // 8. Persist updated store (deduped or with new insight entries)
  if (newEntries > 0 || store.length < preDedupCount) writeJSON(STORE_PATH, store);

  // 9. Save snapshot for delta tracking
  const updatedGraph = readJSON(GRAPH_PATH);
  const updatedRanked = readJSON(RANKED_PATH);
  saveSnapshot(updatedGraph, updatedRanked);

  return {
    entries: store.length,
    edges: edges.length,
    newEntries,
    message: 'Consolidated',
  };
}

// ── Snapshot for delta tracking ─────────────────────────────────────────────

const SNAPSHOT_PATH = path.join(DATA_DIR, 'intelligence-snapshot.json');

function saveSnapshot(graph, ranked) {
  const snap = {
    timestamp: Date.now(),
    nodes: graph ? Object.keys(graph.nodes || {}).length : 0,
    edges: graph ? (graph.edges || []).length : 0,
    pageRankSum: 0,
    confidences: [],
    accessCounts: [],
    topPatterns: [],
  };

  if (graph && graph.pageRanks) {
    for (const v of Object.values(graph.pageRanks)) snap.pageRankSum += v;
  }
  if (graph && graph.nodes) {
    for (const n of Object.values(graph.nodes)) {
      snap.confidences.push(n.confidence || 0.5);
      snap.accessCounts.push(n.accessCount || 0);
    }
  }
  if (ranked && ranked.entries) {
    snap.topPatterns = ranked.entries.slice(0, 10).map(e => ({
      id: e.id,
      summary: (e.summary || '').slice(0, 60),
      confidence: e.confidence || 0.5,
      pageRank: e.pageRank || 0,
      accessCount: e.accessCount || 0,
    }));
  }

  // Keep history: append to array, cap at 50
  let history = readJSON(SNAPSHOT_PATH);
  if (!Array.isArray(history)) history = [];
  history.push(snap);
  if (history.length > 50) history = history.slice(-50);
  writeJSON(SNAPSHOT_PATH, history);
}

/**
 * stats() — Diagnostic report showing intelligence health and improvement.
 * Can be called as: node intelligence.cjs stats [--json]
 */
function stats(outputJson) {
  const graph = readJSON(GRAPH_PATH);
  const ranked = readJSON(RANKED_PATH);
  const history = readJSON(SNAPSHOT_PATH) || [];
  const pending = fs.existsSync(PENDING_PATH)
    ? fs.readFileSync(PENDING_PATH, 'utf-8').trim().split('\n').filter(Boolean).length
    : 0;

  // Current state
  const nodes = graph ? Object.keys(graph.nodes || {}).length : 0;
  const edges = graph ? (graph.edges || []).length : 0;
  const density = nodes > 1 ? (2 * edges) / (nodes * (nodes - 1)) : 0;

  // Confidence distribution
  const confidences = [];
  const accessCounts = [];
  if (graph && graph.nodes) {
    for (const n of Object.values(graph.nodes)) {
      confidences.push(n.confidence || 0.5);
      accessCounts.push(n.accessCount || 0);
    }
  }
  confidences.sort((a, b) => a - b);
  const confMin = confidences.length ? confidences[0] : 0;
  const confMax = confidences.length ? confidences[confidences.length - 1] : 0;
  const confMean = confidences.length ? confidences.reduce((s, c) => s + c, 0) / confidences.length : 0;
  const confMedian = confidences.length ? confidences[Math.floor(confidences.length / 2)] : 0;

  // Access stats
  const totalAccess = accessCounts.reduce((s, c) => s + c, 0);
  const accessedCount = accessCounts.filter(c => c > 0).length;

  // PageRank stats
  let prSum = 0, prMax = 0, prMaxId = '';
  if (graph && graph.pageRanks) {
    for (const [id, pr] of Object.entries(graph.pageRanks)) {
      prSum += pr;
      if (pr > prMax) { prMax = pr; prMaxId = id; }
    }
  }

  // Top patterns by composite score
  const topPatterns = (ranked && ranked.entries || []).slice(0, 10).map((e, i) => ({
    rank: i + 1,
    summary: (e.summary || '').slice(0, 60),
    confidence: (e.confidence || 0.5).toFixed(3),
    pageRank: (e.pageRank || 0).toFixed(4),
    accessed: e.accessCount || 0,
    score: (0.6 * (e.pageRank || 0) + 0.4 * (e.confidence || 0.5)).toFixed(4),
  }));

  // Edge type breakdown
  const edgeTypes = {};
  if (graph && graph.edges) {
    for (const e of graph.edges) {
      edgeTypes[e.type || 'unknown'] = (edgeTypes[e.type || 'unknown'] || 0) + 1;
    }
  }

  // Delta from previous snapshot
  let delta = null;
  if (history.length >= 2) {
    const prev = history[history.length - 2];
    const curr = history[history.length - 1];
    const elapsed = (curr.timestamp - prev.timestamp) / 1000;
    const prevConfMean = prev.confidences.length
      ? prev.confidences.reduce((s, c) => s + c, 0) / prev.confidences.length : 0;
    const currConfMean = curr.confidences.length
      ? curr.confidences.reduce((s, c) => s + c, 0) / curr.confidences.length : 0;
    const prevAccess = prev.accessCounts.reduce((s, c) => s + c, 0);
    const currAccess = curr.accessCounts.reduce((s, c) => s + c, 0);

    delta = {
      elapsed: elapsed < 3600 ? `${Math.round(elapsed / 60)}m` : `${(elapsed / 3600).toFixed(1)}h`,
      nodes: curr.nodes - prev.nodes,
      edges: curr.edges - prev.edges,
      confidenceMean: currConfMean - prevConfMean,
      totalAccess: currAccess - prevAccess,
    };
  }

  // Trend over all history
  let trend = null;
  if (history.length >= 3) {
    const first = history[0];
    const last = history[history.length - 1];
    const sessions = history.length;
    const firstConfMean = first.confidences.length
      ? first.confidences.reduce((s, c) => s + c, 0) / first.confidences.length : 0;
    const lastConfMean = last.confidences.length
      ? last.confidences.reduce((s, c) => s + c, 0) / last.confidences.length : 0;
    trend = {
      sessions,
      nodeGrowth: last.nodes - first.nodes,
      edgeGrowth: last.edges - first.edges,
      confidenceDrift: lastConfMean - firstConfMean,
      direction: lastConfMean > firstConfMean ? 'improving' :
                 lastConfMean < firstConfMean ? 'declining' : 'stable',
    };
  }

  const report = {
    graph: { nodes, edges, density: +density.toFixed(4) },
    confidence: {
      min: +confMin.toFixed(3), max: +confMax.toFixed(3),
      mean: +confMean.toFixed(3), median: +confMedian.toFixed(3),
    },
    access: { total: totalAccess, patternsAccessed: accessedCount, patternsNeverAccessed: nodes - accessedCount },
    pageRank: { sum: +prSum.toFixed(4), topNode: prMaxId, topNodeRank: +prMax.toFixed(4) },
    edgeTypes,
    pendingInsights: pending,
    snapshots: history.length,
    topPatterns,
    delta,
    trend,
  };

  if (outputJson) {
    console.log(JSON.stringify(report, null, 2));
    return report;
  }

  // Human-readable output
  const bar = '+' + '-'.repeat(62) + '+';
  console.log(bar);
  console.log('|' + '  Intelligence Diagnostics (ADR-050)'.padEnd(62) + '|');
  console.log(bar);
  console.log('');

  console.log('  Graph');
  console.log(`    Nodes:    ${nodes}`);
  console.log(`    Edges:    ${edges} (${Object.entries(edgeTypes).map(([t,c]) => `${c} ${t}`).join(', ') || 'none'})`);
  console.log(`    Density:  ${(density * 100).toFixed(1)}%`);
  console.log('');

  console.log('  Confidence');
  console.log(`    Min:      ${confMin.toFixed(3)}`);
  console.log(`    Max:      ${confMax.toFixed(3)}`);
  console.log(`    Mean:     ${confMean.toFixed(3)}`);
  console.log(`    Median:   ${confMedian.toFixed(3)}`);
  console.log('');

  console.log('  Access');
  console.log(`    Total accesses:     ${totalAccess}`);
  console.log(`    Patterns used:      ${accessedCount}/${nodes}`);
  console.log(`    Never accessed:     ${nodes - accessedCount}`);
  console.log(`    Pending insights:   ${pending}`);
  console.log('');

  console.log('  PageRank');
  console.log(`    Sum:      ${prSum.toFixed(4)} (should be ~1.0)`);
  console.log(`    Top node: ${prMaxId || '(none)'} (${prMax.toFixed(4)})`);
  console.log('');

  if (topPatterns.length > 0) {
    console.log('  Top Patterns (by composite score)');
    console.log('  ' + '-'.repeat(60));
    for (const p of topPatterns) {
      console.log(`    #${p.rank}  ${p.summary}`);
      console.log(`         conf=${p.confidence}  pr=${p.pageRank}  score=${p.score}  accessed=${p.accessed}x`);
    }
    console.log('');
  }

  if (delta) {
    console.log(`  Last Delta (${delta.elapsed} ago)`);
    const sign = v => v > 0 ? `+${v}` : `${v}`;
    console.log(`    Nodes:      ${sign(delta.nodes)}`);
    console.log(`    Edges:      ${sign(delta.edges)}`);
    console.log(`    Confidence: ${delta.confidenceMean >= 0 ? '+' : ''}${delta.confidenceMean.toFixed(4)}`);
    console.log(`    Accesses:   ${sign(delta.totalAccess)}`);
    console.log('');
  }

  if (trend) {
    console.log(`  Trend (${trend.sessions} snapshots)`);
    console.log(`    Node growth:       ${trend.nodeGrowth >= 0 ? '+' : ''}${trend.nodeGrowth}`);
    console.log(`    Edge growth:       ${trend.edgeGrowth >= 0 ? '+' : ''}${trend.edgeGrowth}`);
    console.log(`    Confidence drift:  ${trend.confidenceDrift >= 0 ? '+' : ''}${trend.confidenceDrift.toFixed(4)}`);
    console.log(`    Direction:         ${trend.direction.toUpperCase()}`);
    console.log('');
  }

  if (!delta && !trend) {
    console.log('  No history yet — run more sessions to see deltas and trends.');
    console.log('');
  }

  console.log(bar);
  return report;
}

module.exports = { init, getContext, recordEdit, feedback, consolidate, stats };

// ── CLI entrypoint ──────────────────────────────────────────────────────────
if (require.main === module) {
  const cmd = process.argv[2];
  const jsonFlag = process.argv.includes('--json');

  const cmds = {
    init: () => { const r = init(); console.log(JSON.stringify(r)); },
    stats: () => { stats(jsonFlag); },
    consolidate: () => { const r = consolidate(); console.log(JSON.stringify(r)); },
  };

  if (cmd && cmds[cmd]) {
    cmds[cmd]();
  } else {
    console.log('Usage: intelligence.cjs <stats|init|consolidate> [--json]');
    console.log('');
    console.log('  stats         Show intelligence diagnostics and trends');
    console.log('  stats --json  Output as JSON for programmatic use');
    console.log('  init          Build graph and rank entries');
    console.log('  consolidate   Process pending insights and recompute');
  }
}
