/**
 * ONE LOOP PRIMITIVE — serial substrate-mediated resolution.
 *
 * Every LLM call is a stateless fresh instance. Continuity lives only in the
 * substrate (ledger + lessons). A "loop" is a series of fresh invocations whose
 * context is COMPILED from frozen contracts, diagnostics, calling-line evidence,
 * and documented prior resolutions — never the broken artifact.
 *
 * Progress is monotone by construction: each iteration zeroes a class group
 * or escalates (removes) an owning feature.
 */
import { getConfig, temperatureLadder } from './config';

async function mapLimit<T, R>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<R>,
): Promise<R[]> {
  const results = new Array<R>(items.length);
  let next = 0;
  const workers = Array.from({ length: Math.min(limit, Math.max(1, items.length)) }, async () => {
    while (true) {
      const i = next++;
      if (i >= items.length) return;
      results[i] = await fn(items[i]);
    }
  });
  await Promise.all(workers);
  return results;
}

export interface ClassKey {
  key: string;
  bugClass: string;
  target: string;
  member?: string | null;
}

export interface FindingGroup<E> {
  classKey: ClassKey;
  findings: E[];
}

export interface ResolveOutcome<E> {
  errors: E[];
  iterations: number;
  escalatedFeatures: string[];
  /** Finding counts after each re-measure (for monotonicity tests). */
  history: number[];
}

export interface ResolveSerialOpts<E> {
  measure: () => E[] | Promise<E[]>;
  /** Group findings into class keys. */
  group: (errs: E[]) => FindingGroup<E>[];
  /** Tree depth of a routed target (lower = upstream). */
  depthOf: (target: string) => number;
  /** Apply matching lessons / deterministic fixes. Return true if any applied. */
  applyLessons: (groups: FindingGroup<E>[]) => boolean | Promise<boolean>;
  /**
   * ONE fresh invocation for this group at the given temperature.
   * Must not embed broken unit bodies in the prompt.
   */
  repairFresh: (group: FindingGroup<E>, temperature: number | undefined) => Promise<void>;
  /** Scoped check: remaining findings for this group's class key (empty = resolved). */
  scopedRemaining: (group: FindingGroup<E>) => E[] | Promise<E[]>;
  /** Persist lesson after a successful fresh resolution. */
  onLesson: (group: FindingGroup<E>) => void | Promise<void>;
  /** Escalate owning feature; remove it from the workset. */
  escalateFeature: (featureId: string, group: FindingGroup<E>) => void | Promise<void>;
  owningFeature: (target: string) => string | null;
  /** Ceiling on outer iterations (generous; real stop is empty or all escalated). */
  maxIterations?: number;
}

/**
 * Serial resolver used by every gate. Groups sharing a stub serialize;
 * disjoint targets may resolve concurrently on the operator pool.
 */
export async function resolveSerial<E>(opts: ResolveSerialOpts<E>): Promise<ResolveOutcome<E>> {
  const width = Math.max(1, getConfig().swarmWidth);
  const ladder = temperatureLadder();
  const maxIter = opts.maxIterations ?? getConfig().maxPasses;
  const escalatedFeatures: string[] = [];
  const history: number[] = [];
  let iterations = 0;

  let errors = await opts.measure();
  history.push(errors.length);

  while (errors.length > 0 && iterations < maxIter) {
    iterations++;
    let groups = opts.group(errors);
    if (!groups.length) break;

    // (c) lessons FIRST — zero tokens
    const lessonApplied = await opts.applyLessons(groups);
    if (lessonApplied) {
      errors = await opts.measure();
      history.push(errors.length);
      if (!errors.length) break;
      groups = opts.group(errors);
      if (!groups.length) break;
    }

    // (d) pick upstream-most; batch concurrent only for disjoint stub targets
    groups.sort((a, b) => {
      const da = opts.depthOf(a.classKey.target);
      const db = opts.depthOf(b.classKey.target);
      if (da !== db) return da - db;
      return a.classKey.key.localeCompare(b.classKey.key);
    });

    // Take the upstream-most target; include other groups with different targets
    // at the same "wave" for parallel repair (disjoint stubs only).
    const wave: FindingGroup<E>[] = [];
    const usedTargets = new Set<string>();
    for (const g of groups) {
      if (usedTargets.has(g.classKey.target)) continue;
      if (!wave.length) {
        wave.push(g);
        usedTargets.add(g.classKey.target);
        continue;
      }
      // Only add if distinct target (serialize same stub)
      if (!usedTargets.has(g.classKey.target)) {
        wave.push(g);
        usedTargets.add(g.classKey.target);
      }
      // Cap wave size to concurrency
      if (wave.length >= getConfig().concurrency) break;
    }

    // Resolve wave: each group gets up to swarmWidth fresh draws
    await mapLimit(wave, getConfig().concurrency, async (g) => {
      let resolved = false;
      for (let i = 0; i < width; i++) {
        const temp = ladder[i % ladder.length];
        await opts.repairFresh(g, temp);
        const left = await opts.scopedRemaining(g);
        if (!left.length) {
          await opts.onLesson(g);
          resolved = true;
          break;
        }
      }
      if (!resolved) {
        const fid = opts.owningFeature(g.classKey.target);
        if (fid && !escalatedFeatures.includes(fid)) {
          escalatedFeatures.push(fid);
          await opts.escalateFeature(fid, g);
        }
      }
    });

    errors = await opts.measure();
    // Filter out findings owned by escalated features when measure can't
    history.push(errors.length);

    // Guard: if count grew, something violated monotone — still continue but
    // the ratchet at call sites should have prevented this.
    if (history.length >= 2 && history[history.length - 1] > history[history.length - 2]) {
      // Do not spin forever on growth — break to escalate path at caller
      break;
    }
  }

  return { errors, iterations, escalatedFeatures, history };
}
