#!/usr/bin/env node
/**
 * ruflo H7 patch re-apply tool.
 *
 * H7: `ruflo memory store` routes through the AgentDB-v3 bridge
 * (memory-bridge.js:bridgeStoreEntry) which inserts into an in-RAM sql.js DB
 * and never flushes to disk — entries are lost between CLI invocations.
 *
 * Fix: force getBridge() in @claude-flow/cli's memory-initializer.js to
 * return null, so memory ops fall through to the raw sql.js path that DOES
 * persist (db.export() + writeFileRestricted).
 *
 * Both operations live in the GLOBAL npm install and are overwritten by
 * `ruflo update` — re-run this script after any ruflo upgrade.
 * Each apply/revert/check now covers BOTH the getBridge patch AND the
 * onnxruntime-node dedupe (disabling the nested @xenova/transformers copy).
 *
 * Usage:
 *   node tools/ruflo-h7-patch.mjs            apply both fixes (idempotent)
 *   node tools/ruflo-h7-patch.mjs --check    exit 0 if both applied, 1 if not
 *   node tools/ruflo-h7-patch.mjs --revert   remove both fixes
 *
 * See docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md §4
 */
import { execSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';

export const PATCH_MARKER = 'LIDERRA-H7-PATCH';
export const PATCH_LINE =
  '    return null; /* LIDERRA-H7-PATCH: AgentDB bridge writes in-RAM sql.js, never flushes — force raw persisting path */';
const ANCHOR = 'async function getBridge() {';

/** True if the file content already contains the patch. */
export function isPatched(content) {
  return content.includes(PATCH_MARKER);
}

/**
 * Insert the patch line as the first statement of getBridge().
 * Idempotent. Throws if the anchor is absent (ruflo upstream changed).
 */
export function applyPatch(content) {
  if (isPatched(content)) return { content, changed: false };
  const idx = content.indexOf(ANCHOR);
  if (idx === -1) {
    throw new Error(
      `anchor "${ANCHOR}" not found — ruflo upstream changed getBridge(); revise this patch manually`,
    );
  }
  const insertAt = idx + ANCHOR.length;
  const patched = content.slice(0, insertAt) + '\n' + PATCH_LINE + content.slice(insertAt);
  return { content: patched, changed: true };
}

/** Remove the patch line (the line carrying PATCH_MARKER). */
export function revertPatch(content) {
  if (!isPatched(content)) return { content, changed: false };
  const lines = content.split('\n');
  const kept = lines.filter((l) => !l.includes(PATCH_MARKER));
  return { content: kept.join('\n'), changed: true };
}

/**
 * Resolve the path to @claude-flow/cli's memory-initializer.js inside the
 * global npm tree. Handles both nested (under ruflo/) and hoisted layouts.
 */
export function resolveTargetPath(npmRoot) {
  const tail = ['@claude-flow', 'cli', 'dist', 'src', 'memory', 'memory-initializer.js'];
  const candidates = [
    join(npmRoot, 'ruflo', 'node_modules', ...tail),
    join(npmRoot, ...tail),
  ];
  return candidates.find((c) => existsSync(c)) ?? null;
}

// --- onnxruntime-node dedupe (H7 fix, part 2) ---
// ruflo's dependency tree carries two incompatible onnxruntime-node versions:
//   ruflo/node_modules/onnxruntime-node                                    (1.24.3)
//   ruflo/node_modules/@xenova/transformers/node_modules/onnxruntime-node   (1.14.0)
// Both ship onnxruntime.dll; loading both into one process => DLL name
// collision => ERR_DLOPEN_FAILED. Disabling the nested duplicate makes
// @xenova/transformers resolve to the single hoisted 1.24.3.
const DEDUPE_DISABLED_SUFFIX = '.h7disabled';

/** Resolve { active, disabled } paths for the duplicate nested onnxruntime-node. */
export function resolveDedupePaths(npmRoot) {
  const active = join(
    npmRoot, 'ruflo', 'node_modules', '@xenova', 'transformers',
    'node_modules', 'onnxruntime-node',
  );
  return { active, disabled: active + DEDUPE_DISABLED_SUFFIX };
}

/**
 * Dedupe status:
 *   'deduped'      — disabled dir exists, active does not (fix applied)
 *   'not-deduped'  — active dir exists, disabled does not (fix not applied)
 *   'absent'       — neither exists (no nested duplicate — nothing to dedupe)
 *   'inconsistent' — both exist (manual intervention required)
 */
export function dedupeStatus(active, disabled) {
  const a = existsSync(active);
  const d = existsSync(disabled);
  if (a && d) return 'inconsistent';
  if (d) return 'deduped';
  if (a) return 'not-deduped';
  return 'absent';
}

/** Disable the nested duplicate (rename active -> disabled). Idempotent. */
export function applyDedupe(active, disabled) {
  const status = dedupeStatus(active, disabled);
  if (status === 'deduped' || status === 'absent') return { changed: false, status };
  if (status === 'inconsistent') {
    throw new Error(
      `dedupe inconsistent — both "${active}" and "${disabled}" exist; resolve manually`,
    );
  }
  renameSync(active, disabled);
  return { changed: true, status: 'deduped' };
}

/** Re-enable the nested duplicate (rename disabled -> active). */
export function revertDedupe(active, disabled) {
  const status = dedupeStatus(active, disabled);
  if (status === 'not-deduped' || status === 'absent') return { changed: false, status };
  if (status === 'inconsistent') {
    throw new Error(
      `dedupe inconsistent — both "${active}" and "${disabled}" exist; resolve manually`,
    );
  }
  renameSync(disabled, active);
  return { changed: true, status: 'not-deduped' };
}

function main() {
  const mode = process.argv[2];
  const VALID_MODES = [undefined, '--check', '--revert'];
  if (!VALID_MODES.includes(mode)) {
    console.error(`[ruflo-h7-patch] unknown flag: ${mode}`);
    console.error('Usage: node tools/ruflo-h7-patch.mjs [--check | --revert]');
    process.exit(2);
  }
  let npmRoot;
  try {
    npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
  } catch (e) {
    console.error('[ruflo-h7-patch] failed to run `npm root -g`: ' + e.message);
    process.exit(2);
  }
  const target = resolveTargetPath(npmRoot);
  if (!target) {
    console.error('[ruflo-h7-patch] memory-initializer.js not found under ' + npmRoot);
    process.exit(2);
  }
  const { active, disabled } = resolveDedupePaths(npmRoot);

  if (mode === '--check') {
    const patched = isPatched(readFileSync(target, 'utf8'));
    const dStatus = dedupeStatus(active, disabled);
    const dedupeOk = dStatus === 'deduped' || dStatus === 'absent';
    console.log(`[ruflo-h7-patch] ${target}`);
    console.log(`  getBridge patch:    ${patched ? 'PATCHED' : 'UNPATCHED'}`);
    console.log(`  onnxruntime dedupe: ${dStatus}`);
    process.exit(patched && dedupeOk ? 0 : 1);
  }

  if (mode === '--revert') {
    const r1 = revertPatch(readFileSync(target, 'utf8'));
    if (r1.changed) writeFileSync(target, r1.content);
    console.log(`[ruflo-h7-patch] revert getBridge: ${r1.changed ? 'patch removed' : 'was not patched'}`);
    try {
      const r2 = revertDedupe(active, disabled);
      console.log(`[ruflo-h7-patch] revert dedupe: ${r2.changed ? 're-enabled nested onnxruntime-node' : r2.status}`);
    } catch (e) {
      console.error('[ruflo-h7-patch] ' + e.message);
      process.exit(3);
    }
    process.exit(0);
  }

  // default: apply
  try {
    const r1 = applyPatch(readFileSync(target, 'utf8'));
    if (r1.changed) writeFileSync(target, r1.content);
    console.log(`[ruflo-h7-patch] apply getBridge: ${r1.changed ? 'patched' : 'already patched'}`);
    const r2 = applyDedupe(active, disabled);
    console.log(`[ruflo-h7-patch] apply dedupe: ${r2.changed ? 'disabled nested onnxruntime-node' : r2.status}`);
    process.exit(0);
  } catch (e) {
    console.error('[ruflo-h7-patch] ' + e.message);
    process.exit(3);
  }
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  main();
}
