/**
 * The bounded-iteration primitive behind per-unit retry ladders.
 *
 * The serial resolver (resolver.ts) is the primary control for gates.
 * converge() remains as the bounded per-unit temperature ladder + ceiling.
 *
 * Progress stall requires MEANINGFUL improvement — a loop that shaves 1
 * finding per pass (170→169→168…) must stall, not reset forever.
 */
// Deliberately config-free (config.ts is fs-coupled and this primitive runs
// in the Tier 1 browser bundle). Callers pass their own ceilings; the
// constants below mirror the canonical defaults.
const DEFAULT_MAX_PASSES = 20;
const DEFAULT_PROGRESS_STALL = 4;

export interface ConvergeOutcome<E> {
  /** Remaining errors; empty means the gate converged. */
  errors: E[];
  passes: number;
  /** True when stopped for lack of progress rather than the ceiling. */
  stalled: boolean;
}

const DEFAULT_STALL_LIMIT = 2;

/** Meaningful progress threshold relative to the window's starting best. */
export function meaningfulDelta(windowStartBest: number): number {
  return Math.max(3, Math.ceil(0.05 * windowStartBest));
}

export async function converge<E>(opts: {
  measure: () => E[] | Promise<E[]>;
  signature: (errs: E[]) => string;
  repair: (errs: E[], pass: number) => Promise<void>;
  maxPasses?: number;
  stallLimit?: number;
  /** Progress scalar, lower is better. Defaults to error count. */
  metric?: (errs: E[]) => number;
  /** Passes without meaningful new best before declaring stall. */
  progressStall?: number;
  /**
   * ENTROPY RESET hook. Invoked once when a stall is detected, BEFORE the
   * loop reports stalled. Return true if a fresh decorrelated draw was applied.
   */
  onStall?: (errs: E[], pass: number) => Promise<boolean>;
  /**
   * SET 20b — cooperative abort (e.g. parked crash). Checked after each repair
   * before the next measure; when true, exit with current errors (not stalled).
   */
  abort?: () => boolean;
}): Promise<ConvergeOutcome<E>> {
  const maxPasses = opts.maxPasses ?? DEFAULT_MAX_PASSES;
  const stallLimit = opts.stallLimit ?? DEFAULT_STALL_LIMIT;
  const progressStall = opts.progressStall ?? DEFAULT_PROGRESS_STALL;
  const metric = opts.metric ?? ((e: E[]) => e.length);

  let errors = await opts.measure();
  let lastSig = opts.signature(errors);
  let best = metric(errors);
  let windowStartBest = best;
  let sigRepeat = 0;
  let sinceImprovement = 0;
  let pass = 0;
  let entropyResetUsed = false;

  while (errors.length > 0 && pass < maxPasses) {
    if (opts.abort?.()) return { errors, passes: pass, stalled: false };
    pass++;
    await opts.repair(errors, pass);
    if (opts.abort?.()) {
      errors = await opts.measure();
      return { errors, passes: pass, stalled: false };
    }
    errors = await opts.measure();

    const sig = opts.signature(errors);
    sigRepeat = sig === lastSig ? sigRepeat + 1 : 0;
    lastSig = sig;

    const m = metric(errors);
    if (m < best) best = m;

    // Reset the no-progress counter only on MEANINGFUL improvement vs the
    // window start — not on any single-finding shave.
    const need = meaningfulDelta(windowStartBest);
    if (best <= windowStartBest - need) {
      windowStartBest = best;
      sinceImprovement = 0;
    } else {
      sinceImprovement++;
    }

    if (sigRepeat >= stallLimit || sinceImprovement >= progressStall) {
      if (opts.onStall && !entropyResetUsed) {
        entropyResetUsed = true;
        const recovered = await opts.onStall(errors, pass);
        if (recovered) {
          errors = await opts.measure();
          if (!errors.length) return { errors, passes: pass, stalled: false };
          lastSig = opts.signature(errors);
          best = metric(errors);
          windowStartBest = best;
          sigRepeat = 0;
          sinceImprovement = 0;
          continue;
        }
      }
      return { errors, passes: pass, stalled: true };
    }
  }
  return { errors, passes: pass, stalled: false };
}
