// Card-ready gating for the auto-scan loop (ADR replaces the fixed 280ms settle
// delay). After clicking a tile, instead of waiting a hardcoded interval and
// hoping the detail card has rendered, poll a cheap detail fingerprint until it
// has both (a) changed from the previously-read artifact and (b) stabilized
// across consecutive samples. This is faster on quick machines and correct on
// slow ones, and it is robust to particle/hover-glow animation: if the card
// never fully stabilizes within the budget it still proceeds once the content
// has changed, rather than looping forever on an animated frame.
//
// Pure except for the injected async sampler/clock, so it is unit testable.

export interface CardReadyOptions {
  /** Consecutive equal samples required to call the card stable. */
  minStableSamples?: number;
  /** Total time budget before giving up on full stability. */
  maxWaitMs?: number;
  /** Delay between samples. */
  pollIntervalMs?: number;
}

export interface CardReadyDeps {
  /** Fast capture -> detail fingerprint. */
  sampleFingerprint: () => Promise<string>;
  wait: (ms: number) => Promise<void>;
  now: () => number;
  /** Returns a non-empty reason to abort (ESC held, stop pressed, ...). */
  checkAbort?: () => Promise<string> | string;
}

export interface CardReadyResult {
  /** Safe to read the full artifact: content changed (and stabilized or budget spent). */
  ready: boolean;
  /** The detail differs from the previously-read artifact. */
  changed: boolean;
  /** Reached the required number of consecutive equal samples. */
  stable: boolean;
  /** Latest sampled fingerprint. */
  fingerprint: string;
  /** Non-empty when aborted via checkAbort. */
  abortReason: string;
  polls: number;
}

const DEFAULT_MIN_STABLE = 2;
const DEFAULT_MAX_WAIT_MS = 900;
const DEFAULT_POLL_MS = 90;

export async function waitForCardReady(
  deps: CardReadyDeps,
  previousFingerprint: string,
  options: CardReadyOptions = {},
): Promise<CardReadyResult> {
  const minStable = Math.max(1, options.minStableSamples ?? DEFAULT_MIN_STABLE);
  const maxWaitMs = Math.max(0, options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS);
  const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? DEFAULT_POLL_MS);

  const start = deps.now();
  let previousSample = "";
  let stableCount = 0;
  let latest = "";
  let polls = 0;

  for (;;) {
    if (deps.checkAbort) {
      const abortReason = await deps.checkAbort();
      if (abortReason) {
        return { ready: false, changed: false, stable: false, fingerprint: latest, abortReason, polls };
      }
    }

    latest = await deps.sampleFingerprint();
    polls++;

    stableCount = latest && latest === previousSample ? stableCount + 1 : 1;
    previousSample = latest;

    const changed = Boolean(latest) && latest !== previousFingerprint;
    const stable = stableCount >= minStable;

    if (changed && stable) {
      return { ready: true, changed: true, stable: true, fingerprint: latest, abortReason: "", polls };
    }

    if (deps.now() - start >= maxWaitMs) {
      // Budget spent. Proceed if the content has at least changed, even if it is
      // still animating (never fully stabilizes).
      return { ready: changed, changed, stable, fingerprint: latest, abortReason: "", polls };
    }

    await deps.wait(pollIntervalMs);
  }
}
