#!/usr/bin/env node
/**
 * One-time migration for kb#153 / A2A-21 (DESIGN-a2a-agents.md v2.1 §5b):
 * splits the single legacy "adolf" Hindsight bank into the per-human bank
 * layout the hindsight-memory plugin now expects (see that plugin's
 * index.js / resolveBanksForSender).
 *
 * WHY A STRAIGHT COPY, NOT A alvis-vs-household CLASSIFIER:
 * The live "adolf" bank's memories/list `context` field (chatLabel, set by
 * the plugin's pre-kb#153 code) shows exactly ONE Matrix DM room across all
 * 381 facts (`chat_qxknyifrguyghhvzdb_mtx_alogins_net` /
 * `chat_room_qxknyifrguyghhvzdb_mtx_alogins_net`) plus a handful of
 * non-Matrix contexts (`chat_webchat`, blank, and manual dev-seeded labels
 * like "goals"/"work"/"kb#84 smoke test"). None of it is attributable to
 * elizaveta (she was only just added to the DM allowlist) and there is no
 * reliable signal in the data for "this fact is household, not personal" —
 * that is a content judgment call, and DESIGN §5b's hard rule is that
 * promotion from a private bank to the shared bank happens ONLY by the
 * owning human's explicit action/approval task, never automatically. So the
 * correct, safe migration is: everything goes to adolf-alvis (matching "the
 * default is H's private bank"); nothing is auto-promoted to adolf-shared.
 * alvis can promote individual household facts to adolf-shared later,
 * through whatever explicit approval flow gets built for that (kb#153's
 * report flags this as follow-up work, not done by this script).
 *
 * MECHANISM: Hindsight has no bulk "copy raw fact between banks" endpoint
 * (verified against the live OpenAPI schema — /export and /import are bank
 * TEMPLATE manifests: config/mental-models/directives, not memory data).
 * The only write path is POST .../memories (RetainRequest), which re-runs
 * server-side extraction on each item's `content` text. Since source items
 * are already atomic single facts (Hindsight's own extraction output), this
 * script feeds each fact's already-clean `text` back through retain into
 * the destination bank, carrying over `context` and `timestamp` (`date`)
 * for provenance. Re-extraction on an already-atomic fact is expected to
 * reproduce it closely, not fragment it further, but this is a genuine
 * re-processing step (a live LLM call per item via hindsight-llm), not a
 * byte-for-byte copy — verify counts after running.
 *
 * SAFETY: dry-run by default. Requires --execute to write. Refuses to
 * target the source bank as its own destination. Does NOT delete or modify
 * the source bank — this script only ever reads it.
 *
 * Usage:
 *   node migrate-adolf-memory-banks.mjs --source adolf --dest adolf-alvis [--execute]
 *   node migrate-adolf-memory-banks.mjs --source adolf --dest adolf-alvis --async --execute
 *
 * Tested (kb#153) against a throwaway destination bank with the full live
 * "adolf" source in dry-run + a partial real write, then that throwaway
 * bank was deleted — this script has NOT been run against adolf-alvis. That
 * final execution against the real destination is the live-migration step
 * kb#153 explicitly hands off rather than running unattended.
 */

const args = process.argv.slice(2);
function argVal(name, def) {
  const i = args.indexOf(`--${name}`);
  return i !== -1 && args[i + 1] !== undefined ? args[i + 1] : def;
}
const flag = (name) => args.includes(`--${name}`);

const HINDSIGHT_URL = argVal("hindsight-url", "http://localhost:8888").replace(/\/+$/, "");
const SOURCE = argVal("source", "adolf");
const DEST = argVal("dest", "adolf-alvis");
const EXECUTE = flag("execute");
const ASYNC = flag("async");
const PAGE_SIZE = Number(argVal("page-size", "50"));
const DELAY_MS = Number(argVal("delay-ms", ASYNC ? "150" : "1500"));
// Testing/smoke-test aid only — omit to migrate everything.
const LIMIT = argVal("limit", undefined);

if (SOURCE === DEST) {
  console.error(`Refusing: --source and --dest are both "${SOURCE}".`);
  process.exit(1);
}

function bankPath(bank) {
  return `${HINDSIGHT_URL}/v1/default/banks/${encodeURIComponent(bank)}`;
}

async function listAll(bank) {
  const items = [];
  let offset = 0;
  for (;;) {
    const res = await fetch(`${bankPath(bank)}/memories/list?limit=${PAGE_SIZE}&offset=${offset}`);
    if (!res.ok) throw new Error(`list ${bank} failed: ${res.status}`);
    const data = await res.json();
    const batch = Array.isArray(data.items) ? data.items : [];
    items.push(...batch);
    offset += batch.length;
    if (batch.length === 0 || offset >= (data.total ?? offset)) break;
  }
  return items;
}

async function retainOne(bank, item) {
  const memoryItem = {
    content: item.text,
    context: item.context || "migrated_from_adolf",
    timestamp: item.date || item.mentioned_at || undefined,
  };
  const res = await fetch(`${bankPath(bank)}/memories`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ async: ASYNC, items: [memoryItem] }),
  });
  if (!res.ok) {
    const body = await res.text().catch(() => "");
    throw new Error(`retain into ${bank} failed: ${res.status} ${body.slice(0, 200)}`);
  }
  return res.json();
}

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function main() {
  console.log(`Source: ${SOURCE}  Dest: ${DEST}  Mode: ${EXECUTE ? "EXECUTE" : "DRY-RUN"}  async retain: ${ASYNC}`);
  let items = await listAll(SOURCE);
  console.log(`Fetched ${items.length} memory items from "${SOURCE}".`);
  if (LIMIT) {
    items = items.slice(0, Number(LIMIT));
    console.log(`--limit set: only processing first ${items.length} items (testing aid).`);
  }
  if (items.length === 0) {
    console.log("Nothing to migrate.");
    return;
  }

  console.log("Sample of first 3 items to be migrated:");
  for (const it of items.slice(0, 3)) {
    console.log(`  [${it.fact_type}] ${it.text.slice(0, 100)}${it.text.length > 100 ? "…" : ""} (context=${it.context || "-"})`);
  }

  if (!EXECUTE) {
    console.log(`\nDry-run only — no writes made. Re-run with --execute to retain all ${items.length} items into "${DEST}".`);
    return;
  }

  let ok = 0;
  let failed = 0;
  for (const [i, item] of items.entries()) {
    try {
      await retainOne(DEST, item);
      ok++;
    } catch (e) {
      failed++;
      console.error(`  [${i + 1}/${items.length}] FAILED: ${e.message}`);
    }
    if ((i + 1) % 10 === 0 || i === items.length - 1) {
      console.log(`  ${i + 1}/${items.length} processed (ok=${ok}, failed=${failed})`);
    }
    await sleep(DELAY_MS);
  }
  console.log(`\nDone. ok=${ok} failed=${failed} out of ${items.length}.`);
  console.log(`Verify with: GET ${bankPath(DEST)}/stats`);
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
