/**
 * SET 32c/32g/32h/32i — the track-relay scheduler for IR fill.
 *
 * Doctrine (Andy's Wright-brothers framing): not enough rail for a long
 * runway → pull the section behind, truck it to the front, lay it ahead —
 * elongate the runway with the SAME finite parts. Frozen exemplars laid
 * behind the run are the rail sections, re-laid in front of the failed
 * feature. No new parts: no new machinery, no bigger model, same K-draw
 * batches. Fill exhaustion DEFERS instead of abandoning.
 *
 * Metaloop bracket — hard bound of 3 prosecutions per feature suite:
 *   1: normal sequential; exhausted features defer
 *   2: deferred set at end-of-phase, warm buffer, fresh anchor-injected plans
 *   3: ONE final bracket pass over every still-failing feature
 * After 3, abandonment is FINAL — fan-out per SET 32d.
 *
 * Changed-world precondition: re-prosecute only if the substrate CHANGED
 * since the feature's last attempt (new frozen/lessons/siblings) — variance
 * without new substrate is a free draw by another name.
 */
import type { FeatureNode } from '../core/types';

export type MetaloopPhase = 1 | 2 | 3;
export type FillStatus =
  | 'frozen'    // suite made it to frozenByFeature
  | 'deferred'  // fill exhaustion — try again next metaloop
  | 'abandoned' // metaloop 3 gave up; SET 32d fan-out applies
  | 'skipped';  // out-of-scope: all planned cases deferred (32f/32c cannot help)

/**
 * SET 32i — the substrate-change signature. Prosecuting the same feature
 * against the same substrate is a free draw dressed up as diligence; we
 * only re-run when at least one input has changed since last time.
 *
 * Inputs monitored:
 *   - number of frozen sibling suites (warm buffer size)
 *   - distinct lesson class keys available
 *   - sibling case count (accumulated exemplars)
 */
export interface WorldSignature {
  frozenCount: number;
  lessonCount: number;
  siblingCaseCount: number;
}

export function makeWorldSignature(input: {
  frozenByFeature: ReadonlyMap<string, string>;
  lessonKeys: ReadonlySet<string>;
  irPartsByFeature: ReadonlyMap<string, { cases: readonly unknown[] }>;
}): WorldSignature {
  let siblingCaseCount = 0;
  for (const parts of input.irPartsByFeature.values()) {
    siblingCaseCount += parts.cases.length;
  }
  return {
    frozenCount: input.frozenByFeature.size,
    lessonCount: input.lessonKeys.size,
    siblingCaseCount,
  };
}

/** True when either signature has strictly more of ANY input than the other.
 *  Equal signatures mean no new track was laid → skip the retry. */
export function worldChanged(prev: WorldSignature, now: WorldSignature): boolean {
  return (
    now.frozenCount > prev.frozenCount
    || now.lessonCount > prev.lessonCount
    || now.siblingCaseCount > prev.siblingCaseCount
  );
}

/**
 * SET 32g — effort floor per feature. Every feature gets ≥2 candidate
 * batches (with a substrate-anchored replan between them per SET 32c) before
 * it may defer, regardless of plan size. Features that others `uses` get
 * the floor RAISED (+1 per distinct dependent, capped) — inverts the
 * arithmetic that gave a root with 11 dependents the LEAST effort.
 *
 * Cap keeps the floor from ballooning on graph roots with many dependents.
 */
export function effortFloor(
  feature: FeatureNode,
  allFeatures: readonly FeatureNode[],
  opts?: { base?: number; cap?: number },
): number {
  const base = opts?.base ?? 2;
  const cap = opts?.cap ?? 4;
  let dependents = 0;
  for (const f of allFeatures) {
    if (f.deps?.includes(feature.id)) dependents++;
  }
  return Math.min(cap, base + dependents);
}

/**
 * SET 32h event: `feature deferred to end-of-phase retry (N frozen exemplars,
 * M sibling cases now available)`.
 */
export function deferralMessage(input: {
  frozenCount: number;
  siblingCaseCount: number;
}): string {
  return `feature deferred to end-of-phase retry (${input.frozenCount} frozen exemplars, `
    + `${input.siblingCaseCount} sibling cases now available)`;
}

/**
 * SET 32c event: `replan (substrate-anchored): N cases (M templated)`.
 */
export function replanMessage(totalCases: number, templated: number): string {
  return `replan (substrate-anchored): ${totalCases} cases (${templated} templated)`;
}

/** SET 32i — metaloop 2 batch-start event. */
export function metaloop2Message(ids: readonly string[]): string {
  return `metaloop 2: retrying [${ids.join(', ')}] (deferred)`;
}

/** SET 32i — metaloop 3 batch-start event. */
export function metaloop3Message(ids: readonly string[]): string {
  return `metaloop 3: bracket pass [${ids.join(', ')}]`;
}

/** SET 32i — changed-world skip event. */
export function skipNoTrackMessage(): string {
  return 'retry skipped: no new track to lay';
}

/** SET 32d — terminal fan-out helpers. */
export function noCoverageProblem(featureId: string): string {
  return `${featureId}: no-coverage (abandoned after metaloop 3)`;
}

export function dependencyUncoveredProblem(dependentId: string, uncoveredId: string): string {
  return `${dependentId}: uses ${uncoveredId} — dependency uncovered`;
}

export function uncoveredSummary(count: number): string {
  return `${count} feature${count === 1 ? '' : 's'} uncovered`;
}

/**
 * SET 32h — sort deferred features so dependency-roots go first (roots first).
 * A dep root's warm exemplars help every downstream retry, so freezing it in
 * metaloop 2 lays track for the rest of that pass.
 */
export function orderRootsFirst(
  featureIds: readonly string[],
  allFeatures: readonly FeatureNode[],
): string[] {
  const byId = new Map(allFeatures.map(f => [f.id, f]));
  // Depth = longest deps path length. Roots have depth 0.
  const depthCache = new Map<string, number>();
  const depthOf = (id: string): number => {
    const cached = depthCache.get(id);
    if (cached != null) return cached;
    const f = byId.get(id);
    if (!f || !f.deps.length) {
      depthCache.set(id, 0);
      return 0;
    }
    const d = 1 + Math.max(...f.deps.map(depthOf));
    depthCache.set(id, d);
    return d;
  };
  return [...featureIds].sort((a, b) => {
    const da = depthOf(a);
    const db = depthOf(b);
    if (da !== db) return da - db;
    return a.localeCompare(b);
  });
}
