// scripts/combat/verify.js
// Read combat log (JSON array of lean events). Check rotation correctness.
// DM-facing output: combat name, rounds, per-round turn list, checks pass/fail.
//
// No internal jargon in output. "Someone acted twice", "someone skipped",
// "turn passed wrong way" — not "double_act", "real_skip", "wrong_advance".
//
// CHECKS (rules combat must obey):
//   1. Round count go up correctly (no skip/jump/backward)
//   2. Turn pass to right person each step
//   3. Nobody act twice in same round
//   4. Nobody get skipped who was active whole round
//   5. Turn order stay stable (no random reshuffle)
//   6. Initiative slot order hold (replay logs only — needs full roster)
//
// Return: 0 clean, 1 bugs found.

'use strict';

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

// snake_case log type → camelCase fn (checks match both shapes)
function normalizeFn(fn) {
  if (!fn) return fn;
  if (!fn.includes('_')) return fn;
  return fn.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
}

// Parse text → array of step-arrays (one per combat run).
// Input: JSON array of lean events (app download OR combat replay output).
// Split: by encounterId, then sub-split on start_encounter (restart boundary).
// First start after setup stays with setup (not bogus "encounter 1").
function loadSteps(text) {
  const trimmed = text.trim();
  if (!trimmed) return [];

  const raw = JSON.parse(trimmed);
  const arr = Array.isArray(raw) ? raw : [raw];
  const groups = new Map();
  for (const e of arr) {
    const key = e.encounterId || '_none_';
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(e);
  }

  const toSteps = (evs) => evs.map((e, i) => ({
    step: i + 1, ts: e.ts || 0, type: e.type,
    fn: normalizeFn(e.type),
    args: null,
    pre: null,
    post: e.snapshot ? {
      round: e.snapshot.round,
      currentTurnParticipantId: e.snapshot.currentTurnParticipantId,
      isStarted: true, isPaused: false,
      turnOrderIds: e.snapshot.turnOrderIds || [],
      activeIds: e.snapshot.activeIds || [],
      participants: null,
    } : null,
    message: e.message || '',
    error: null,
  }));

  const out = [];
  for (const evs of groups.values()) {
    let cur = [];
    let started = false;
    const flush = () => { if (cur.length) { out.push(toSteps(cur)); cur = []; } started = false; };
    for (const e of evs) {
      if (e.type === 'start_encounter' && started) flush();
      if (e.type === 'start_encounter') started = true;
      cur.push(e);
    }
    flush();
  }
  return out;
}

// ---------- name map ----------

const nameMap = new Map();
function learnNames(steps) {
  for (const s of steps) {
    // snapshot has no roster; use participantName field on events + messages
    if (s.message) {
      // no-op; names come from post.currentTurnParticipantId via message ctx
    }
  }
}
// fallback: ids unknown → short. (replay logs carry encounterName; app logs
// carry participantName. We use message text + the turnOrderIds as-is.)
function nm(id) { return id ? (nameMap.get(id) || id.slice(0, 8)) : '(none)'; }

// rebuild name map from raw events (participantName field)
function learnNamesFromEvents(evs) {
  for (const e of evs) {
    if (e.participantId && e.participantName) nameMap.set(e.participantId, e.participantName);
  }
}

// ---------- expected advance ----------

function expectedAdvance(order, fromPos, isActive) {
  const n = order.length;
  if (n === 0) return { nextId: null, wrapped: false };
  for (let step = 1; step < n; step++) {
    const idx = (fromPos + step) % n;
    const id = order[idx];
    if (isActive(id)) return { nextId: id, wrapped: idx <= fromPos };
  }
  return { nextId: null, wrapped: false };
}

// ---------- checks ----------

function analyze(steps) {
  const issues = [];
  const rounds = new Map();
  function ensureRound(r) {
    if (!rounds.has(r)) rounds.set(r, { turnCount: 0, actors: [] });
    return rounds.get(r);
  }

  for (let i = 0; i < steps.length; i++) {
    const s = steps[i];
    const pre = s.pre || (i > 0 ? steps[i - 1].post : null);
    const post = s.post;
    if (!post) continue;

    const isNextTurn = s.fn === 'nextTurn' || s.type === 'next_turn';
    const isStart = s.fn === 'startEncounter' || s.type === 'start_encounter';
    const isEnd = s.fn === 'endEncounter' || s.type === 'end_encounter';

    if (isStart) {
      ensureRound(post.round || 1).actors.push(post.currentTurnParticipantId);
      continue;
    }
    if (isEnd) continue;
    if (isNextTurn) {
      const r = ensureRound(post.round || 0);
      r.turnCount++;
      r.actors.push(post.currentTurnParticipantId);
      if (!pre) continue;

      const order = pre.turnOrderIds || [];
      const fromPos = order.indexOf(pre.currentTurnParticipantId);
      const isActive = id => (pre.activeIds || []).includes(id);
      const exp = expectedAdvance(order, fromPos, isActive);
      const actual = post.currentTurnParticipantId;

      if (exp.nextId && actual && actual !== exp.nextId) {
        issues.push({ step: s.step, round: post.round, kind: 'wrong_turn',
          detail: `Turn passed to ${nm(actual)}, should have been ${nm(exp.nextId)}` });
      }
      if (pre.round !== undefined && post.round !== undefined) {
        if (post.round < pre.round)
          issues.push({ step: s.step, kind: 'round_backward',
            detail: `Round went ${pre.round} → ${post.round}` });
        if (post.round > pre.round + 1)
          issues.push({ step: s.step, kind: 'round_jump',
            detail: `Round jumped ${pre.round} → ${post.round}` });
        if (post.round === pre.round + 1 && !exp.wrapped)
          issues.push({ step: s.step, kind: 'round_phantom',
            detail: `Round went ${pre.round} → ${post.round} but turn didn't wrap` });
      }
      continue;
    }

    if (pre && post && !orderChangedByRosterOrReorder(s.fn)) {
      const before = JSON.stringify(pre.turnOrderIds || []);
      const after = JSON.stringify(post.turnOrderIds || []);
      if (before !== after && pre.turnOrderIds && pre.turnOrderIds.length) {
        issues.push({ step: s.step, kind: 'order_shift',
          detail: `Turn order changed during ${s.fn}` });
      }
    }
  }

  issues.push(...checkCycles(steps, rounds));
  return { issues, rounds };
}

function checkCycles(steps, rounds) {
  const out = [];
  let cycleActive = new Set();
  let cycleActed = new Set();
  let cycleStarter = null;
  let cycleRound = null;
  let started = false;

  function finalize(endStep) {
    if (!started) return;
    const skipped = [...cycleActive].filter(id => !cycleActed.has(id));
    if (skipped.length) {
      out.push({ step: endStep, round: cycleRound, kind: 'skipped',
        detail: `Never acted in round ${cycleRound}: ${skipped.map(nm).join(', ')}` });
    }
  }

  for (let i = 0; i < steps.length; i++) {
    const s = steps[i];
    const pre = s.pre || (i > 0 ? steps[i - 1].post : null);
    const post = s.post;
    if (!post) continue;

    const isNextTurn = s.fn === 'nextTurn' || s.type === 'next_turn';
    const isStart = s.fn === 'startEncounter' || s.type === 'start_encounter';
    const isEnd = s.fn === 'endEncounter' || s.type === 'end_encounter';

    if (isStart) {
      finalize(s.step);
      cycleRound = post.round || 1;
      cycleActive = new Set(post.activeIds || []);
      cycleActed = new Set(post.currentTurnParticipantId ? [post.currentTurnParticipantId] : []);
      cycleStarter = post.currentTurnParticipantId;
      started = true;
      continue;
    }
    if (isEnd) { started = false; continue; }

    if (started && pre && post.currentTurnParticipantId &&
        pre.currentTurnParticipantId !== post.currentTurnParticipantId) {
      const wrapped = pre.round !== undefined && post.round !== undefined && post.round !== pre.round;
      if (wrapped) {
        // drain removed/inactive before finalizing so removed actors
        // aren't flagged as skipped.
        if (pre && pre.activeIds && post.activeIds) {
          const postSet = new Set(post.activeIds);
          for (const id of [...cycleActive]) {
            if (!postSet.has(id)) {
              cycleActive.delete(id);
              cycleActed.delete(id);
            }
          }
        }
        finalize(s.step);
        cycleRound = post.round;
        cycleActive = new Set(post.activeIds || []);
        cycleActed = new Set(post.currentTurnParticipantId ? [post.currentTurnParticipantId] : []);
        cycleStarter = post.currentTurnParticipantId;
      } else {
        const c = post.currentTurnParticipantId;
        if (cycleActed.has(c) && c !== cycleStarter) {
          out.push({ step: s.step, round: post.round, kind: 'acted_twice',
            detail: `${nm(c)} acted twice in round ${post.round}` });
        }
        cycleActed.add(c);
      }
    }

    if (isNextTurn) continue;

    if (pre && post && pre.activeIds && post.activeIds) {
      const postSet = new Set(post.activeIds);
      for (const id of [...cycleActive]) {
        if (!postSet.has(id)) {
          cycleActive.delete(id);
          cycleActed.delete(id);
        }
      }
    }
  }
  return out;
}

function orderChangedByRosterOrReorder(fn) {
  return [
    'addParticipant','addParticipants','removeParticipant','reorderParticipants',
    'startEncounter','endEncounter','setup_encounter','setup_campaign',
    'add_participant','add_participants','remove_participant','reorder',
    'start_encounter','end_encounter',
  ].includes(fn);
}

// ---------- reporting (DM-facing) ----------

const KIND_LABEL = {
  wrong_turn: 'Turn passed to wrong person',
  round_backward: 'Round count went backward',
  round_jump: 'Round count jumped',
  round_phantom: 'Round count changed without turn wrapping',
  order_shift: 'Turn order changed unexpectedly',
  skipped: 'Someone got skipped',
  acted_twice: 'Someone acted twice',
  slot_violation: 'Initiative order violated',
};

function reportOne(label, steps, rawEventsForNames, opts = {}) {
  const log = opts.log || console.log;
  if (rawEventsForNames) learnNamesFromEvents(rawEventsForNames);
  const { issues, rounds } = analyze(steps);

  // combat name
  const name = (rawEventsForNames && rawEventsForNames[0] && rawEventsForNames[0].encounterName) || 'Combat';
  log(`=== ${label}: ${name} ===`);

  // Per-round turn list only in verbose mode. Long replays can print thousands
  // of names and swamp useful result output.
  const sortedRounds = [...rounds.keys()].sort((a, b) => a - b);
  if (opts.verbose) {
    for (const r of sortedRounds) {
      const info = rounds.get(r);
      log(`  Round ${r}: ${info.actors.map(nm).join(' → ')}`);
    }
  }
  log(`  (${steps.length} events, ${rounds.size} rounds${opts.verbose ? '' : ', use -v for per-round turns'})`);

  if (issues.length === 0) {
    log('  ✓ PASS — combat rotated correctly');
    return 0;
  }
  log(`  ✗ FAIL — ${issues.length} problem(s):`);
  const byKind = {};
  for (const it of issues) byKind[it.kind] = (byKind[it.kind] || 0) + 1;
  for (const [k, n] of Object.entries(byKind)) {
    log(`    ${KIND_LABEL[k] || k}: ${n}`);
  }
  for (const it of issues.slice(0, 15)) {
    const where = it.round != null ? `R${it.round} ` : '';
    log(`    [${where}event ${it.step}] ${it.detail}`);
  }
  if (issues.length > 15) log(`    ... +${issues.length - 15} more`);
  return issues.length;
}

module.exports = function verify(text, opts = {}) {
  const log = opts.log || console.log;
  const allSteps = loadSteps(text);
  if (!allSteps.length) {
    log('No combat events found.');
    return 0;
  }

  // raw events grouped same way for name learning
  const raw = JSON.parse(text.trim());
  const arr = Array.isArray(raw) ? raw : [raw];
  const rawGroups = new Map();
  for (const e of arr) {
    const key = e.encounterId || '_none_';
    if (!rawGroups.has(key)) rawGroups.set(key, []);
    rawGroups.get(key).push(e);
  }
  const rawList = [...rawGroups.values()];

  let total = 0;
  for (let i = 0; i < allSteps.length; i++) {
    const label = allSteps.length > 1 ? `[combat ${i + 1}/${allSteps.length}]` : 'Combat';
    if (i > 0) log('');
    total += reportOne(label, allSteps[i], rawList[i], opts);
  }
  log('');
  log(total === 0 ? 'CLEAN' : `${total} problem(s) found`);
  return total === 0 ? 0 : 1;
};
