// scripts/replay-from-logs.js
// Ingest canonical event JSON (downloaded app logs OR replay-combat --json-out)
// and replay forward patches in order. Verifies each event's snapshot matches
// the reconstructed encounter state — catches drift between logged intent and
// actual mutation.
//
// Usage:
//   node scripts/replay-from-logs.js <events.json>
//   node scripts/replay-from-logs.js <events.json> --encounter <id>   # filter
//   cat logs.json | node scripts/replay-from-logs.js                  # stdin
//
// Modes:
//   (default) in-memory replay + snapshot verification. No backend writes.
//   --write   apply each patch to LIVE backend (same adapter as replay-combat).
//             Creates fresh encounter under new campaign. Useful for cloning
//             a logged combat into a clean DB.
//
// Output: turn sequence, round progression, drift report (any snapshot mismatch).

'use strict';

const fs = require('fs');
const { normalizeEvent } = require('../shared/logEvent');

const WRITE_MODE = process.argv.includes('--write');
const ENC_FILTER = (() => {
  const i = process.argv.indexOf('--encounter');
  return i >= 0 ? process.argv[i + 1] : null;
})();

function readInput() {
  // positional .json arg, else stdin.
  const pos = process.argv[2];
  if (pos && !pos.startsWith('--')) return fs.readFileSync(pos, 'utf8');
  // stdin: require piped data (not interactive terminal). Avoid hang.
  if (process.stdin.isTTY) {
    console.error('Usage: node scripts/replay-from-logs.js <events.json> [--encounter <id>] [--write]');
    console.error('       cat events.json | node scripts/replay-from-logs.js');
    process.exit(2);
  }
  const data = fs.readFileSync(0, 'utf8');
  if (!data.trim()) {
    console.error('No input on stdin and no file arg.');
    process.exit(2);
  }
  return data;
}

// Shallow merge patch into encounter (mirrors storage updateDoc semantics).
function applyPatch(enc, patch) {
  if (!patch) return enc;
  const next = { ...enc };
  for (const [k, v] of Object.entries(patch)) {
    next[k] = v;
  }
  return next;
}

// Compare logged snapshot vs computed state. Returns list of field mismatches.
function diffSnapshot(enc, snap) {
  if (!snap) return [];
  const drift = [];
  const curRound = enc.round ?? 0;
  const curTurn = enc.currentTurnParticipantId ?? null;
  const order = enc.turnOrderIds || [];
  const active = (enc.participants || []).filter(p => p.isActive).map(p => p.id);
  if (snap.round !== curRound) drift.push(`round: logged=${snap.round} actual=${curRound}`);
  if (snap.currentTurnParticipantId !== curTurn) drift.push(`currentTurn: logged=${snap.currentTurnParticipantId} actual=${curTurn}`);
  if (JSON.stringify(snap.turnOrderIds || []) !== JSON.stringify(order)) drift.push(`turnOrderIds: logged=${JSON.stringify(snap.turnOrderIds || [])} actual=${JSON.stringify(order)}`);
  if (JSON.stringify(snap.activeIds || []) !== JSON.stringify(active)) drift.push(`activeIds: logged=${JSON.stringify(snap.activeIds || [])} actual=${JSON.stringify(active)}`);
  return drift;
}

function nameMap(enc) {
  const m = new Map();
  for (const p of (enc.participants || [])) if (p.id && p.name) m.set(p.id, p.name);
  return m;
}

async function main() {
  const text = readInput().trim();
  const raw = JSON.parse(text);
  const arr = (Array.isArray(raw) ? raw : [raw]).map(normalizeEvent).filter(Boolean);
  const events = ENC_FILTER ? arr.filter(e => e.encounterId === ENC_FILTER) : arr;

  console.log(`replay-from-logs: ${events.length} events${ENC_FILTER ? ` (encounter ${ENC_FILTER})` : ''}`);

  let enc = null;
  let encName = null;
  let encPath = null;
  let round = 0;
  let turnCount = 0;
  let appliedCount = 0;
  const drift = [];
  const turnSequence = [];

  for (const e of events) {
    // init on first event
    if (!enc) {
      enc = applyPatch({}, e.payload);
      encName = e.encounterName;
      encPath = e.encounterPath;
    } else {
      enc = applyPatch(enc, e.payload);
    }
    appliedCount++;

    if (e.snapshot) {
      const d = diffSnapshot(enc, e.snapshot);
      if (d.length) drift.push({ event: e, fields: d });
    }

    if (e.type === 'next_turn') {
      turnCount++;
      const names = nameMap(enc);
      const actor = e.snapshot?.currentTurnParticipantId;
      const actorName = names.get(actor) || actor || '?';
      const r = e.snapshot?.round ?? enc?.round ?? 0;
      if (r !== round) { round = r; }
      turnSequence.push(`R${r}: ${actorName}`);
    }
  }

  console.log(`encounter: ${encName || '?'}`);
  console.log(`events applied: ${appliedCount} / ${events.length}`);
  console.log(`turns replayed: ${turnCount}`);
  console.log(`final round: ${round}`);
  console.log(`participants at end: ${(enc?.participants || []).length}`);

  if (drift.length) {
    console.log(`\n--- SNAPSHOT DRIFT (${drift.length} events) ---`);
    for (const { event: e, fields } of drift.slice(0, 10)) {
      console.log(`  ${e.type} (${e.id || e.ts}): ${fields.join('; ')}`);
    }
    if (drift.length > 10) console.log(`  ... +${drift.length - 10} more`);
  } else {
    console.log('\nsnapshots: all match');
  }

  console.log(`\n=== ${turnCount} turns, ${drift.length} drift ===`);
  console.log(drift.length === 0 ? 'CLEAN — replay matches logged intent' : 'DRIFT — logged snapshots diverge from applied patches');

  process.exit(drift.length === 0 ? 0 : 1);
}

main().catch(err => { console.error('replay-from-logs failed:', err); process.exit(1); });
