/**
 * Stall recovery — live producers that heartbeat but never land blocks.
 *
 * Offline skips (no heartbeat) already park via advanceSkipsIfNeeded. A live wallet
 * that misses every turn used to stay in the on-chain queue forever and freeze the tip
 * when it was the sole producer. This module parks after repeated missed slots at the
 * same chain height so other live devices can bootstrap production.
 *
 * Does not replace offline/sync skip paths — only augments the missed-slot branch.
 */

/** Missed full slots at a frozen tip before parking a live producer (default ~66s @ 33s blocks). */
const DEFAULT_MISSED_BEFORE_PARK = 2;

/**
 * @param {{ missedBeforePark?: number }} [options]
 */
function createStallRecovery(options = {}) {
  const missedBeforePark = Math.max(
    1,
    Number(options.missedBeforePark) || DEFAULT_MISSED_BEFORE_PARK
  );
  let streakTipHeight = -1;
  /** @type {Map<string, number>} */
  const missStreak = new Map();

  function recordTipAdvance(height) {
    const h = Number(height) || 0;
    if (h <= streakTipHeight) return;
    streakTipHeight = h;
    missStreak.clear();
  }

  /**
   * @param {string} producer
   * @param {number} tipHeight
   * @returns {{ action: "continue", consecutiveMisses: number } | { action: "park", producer: string, consecutiveMisses: number, tipHeight: number }}
   */
  function onMissedSlot(producer, tipHeight) {
    const addr = String(producer || "").trim();
    const tip = Number(tipHeight) || 0;
    if (!addr) {
      return { action: "continue", consecutiveMisses: 0 };
    }
    if (tip !== streakTipHeight) {
      streakTipHeight = tip;
      missStreak.clear();
    }
    const consecutiveMisses = (missStreak.get(addr) || 0) + 1;
    missStreak.set(addr, consecutiveMisses);
    if (consecutiveMisses < missedBeforePark) {
      return { action: "continue", consecutiveMisses };
    }
    missStreak.delete(addr);
    return { action: "park", producer: addr, consecutiveMisses, tipHeight: tip };
  }

  function reset() {
    streakTipHeight = -1;
    missStreak.clear();
  }

  return {
    recordTipAdvance,
    onMissedSlot,
    reset,
    /** @internal */
    _state: () => ({ streakTipHeight, missStreak: new Map(missStreak), missedBeforePark }),
  };
}

module.exports = {
  createStallRecovery,
  DEFAULT_MISSED_BEFORE_PARK,
};
