/**
 * # frame/kernel-walk — the ONE cockpit-kernel walk + the shared kernel helpers (ADR-0052 §1)
 *
 * The SAFETY / CONTROL cockpit kernel has exactly ONE traversal: {@link walkKernel} visits the
 * optional mandate/brief prefix and then the 8 fixed-order {@link ./types.ts KERNEL_SECTION_LABELS}
 * sections, IN ORDER, calling a {@link KernelSink} method for each. Every section method is ALWAYS
 * called (absent-not-hidden is a structural property of the walk — a section can never be silently
 * skipped), and the SECTION ORDER lives here once. The two shipped renderers are format ADAPTERS
 * over this one walk: the canonical text sink ({@link ./render.ts}) materializes the delta-anchored
 * cell-lines, the token-optimal ARM sink ({@link ./render-arm.ts}) materializes the compact ASCII
 * lines. A new section, or a reordering, lands in BOTH renderers from a single edit here.
 *
 * This module also owns the kernel helpers both adapters share — the engine-log bucket set +
 * classification, the resting-order state label, and the action id token — parameterized by the
 * small vocabulary each adapter differs on, so the LOGIC is single-source while the BYTES stay each
 * adapter's own. PURE: no wall clock, no RNG (the whole render path is deterministic, RUNTIME §0).
 */
import type {
  Brief,
  Claim,
  EngineAction,
  EngineActionKind,
  Kernel,
  Mandate,
  OwnerAct,
  Position,
  RestingOrder,
  VehicleHealth,
  WakeRef,
} from "./types.ts";

// ─────────────────────────────────────────────────────────────────────────────
// The sink — one method per kernel section (called in fixed order by walkKernel)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * A format adapter for the cockpit kernel: {@link walkKernel} calls these in the fixed cockpit order
 * and each implementation accumulates its own line representation (canonical cell-lines, arm strings).
 * A section method is called EVEN WHEN its data is empty (absent-not-hidden) — the adapter renders the
 * explicit `none`/`UNKNOWN` placeholder, never a dropped section. The `mandate`/`brief` methods are
 * called ONLY when present (they are the optional standing-context prefix, not one of the 8 sections).
 */
export interface KernelSink {
  /** The lead block: the kernel sentinel (canonical) / the `#KERNEL <kind>` banner (arm). */
  sentinel(frameKind: "OPEN" | "WAKE"): void;
  /** The `frame=<kind>` line (canonical only; the arm folds the kind into its sentinel). */
  frameLine(frameKind: "OPEN" | "WAKE"): void;
  /** MANDATE — the hard, machine-checkable admission channel (ADR-0026). Present-only. */
  mandate(mandate: Mandate): void;
  /** BRIEF — the soft, directional English channel; NOT an admission input (ADR-0026). Present-only. */
  brief(brief: Brief): void;
  /** §1 WAKE — reason / severity / RELATIVE deadline. */
  wake(wake: WakeRef | null | undefined): void;
  /** §2 DATA-HEALTH per vehicle + the NAMED unavailable capabilities. */
  dataHealth(rows: readonly VehicleHealth[], unavailable: readonly string[]): void;
  /** §3 POSITIONS / INVENTORY-CLAIMS. */
  positions(positions: readonly Position[]): void;
  /** §4 RESTING ORDERS. */
  resting(resting: readonly RestingOrder[]): void;
  /** §5 BUDGET / REMAINING-R + the sizing headroom. */
  budget(envelope: BudgetEnvelopeArg): void;
  /** §6 OWNER ENVELOPE + ACTS (the envelope echo reads the same budget as §5). */
  ownerActs(envelope: BudgetEnvelopeArg, acts: readonly OwnerAct[]): void;
  /** §7 L0/L1 ENGINE LOG — bucketed fired | cancelled | rejected | clamped (+ catch-all `other`). */
  engineLog(elog: readonly EngineAction[]): void;
  /** §8 PREDICTOR / REGIME CLAIMS. */
  claims(claims: readonly Claim[]): void;
}

/** The budget-envelope argument §5 and §6 share (the cockpit risk envelope, or absent). */
export type BudgetEnvelopeArg = Kernel["budgetEnvelope"];

/**
 * The ONE cockpit-kernel traversal: mandate/brief prefix (present-only) then the 8 fixed-order
 * sections, each ALWAYS visited. This is the single source of the kernel's SECTION ORDER and its
 * absent-not-hidden completeness — both format adapters get an identical structure from it. PURE.
 */
export function walkKernel(kernel: Kernel, frameKind: "OPEN" | "WAKE", sink: KernelSink): void {
  sink.sentinel(frameKind);
  sink.frameLine(frameKind);
  if (kernel.mandate !== undefined && kernel.mandate !== null) sink.mandate(kernel.mandate);
  if (kernel.brief !== undefined && kernel.brief !== null) sink.brief(kernel.brief);
  sink.wake(kernel.wake); // §1
  sink.dataHealth(kernel.dataHealth ?? [], kernel.unavailable ?? []); // §2
  sink.positions(kernel.positions ?? []); // §3
  sink.resting(kernel.resting ?? []); // §4
  sink.budget(kernel.budgetEnvelope); // §5
  sink.ownerActs(kernel.budgetEnvelope, kernel.ownerActs ?? []); // §6
  sink.engineLog(kernel.engineLog ?? []); // §7
  sink.claims(kernel.claims ?? []); // §8
}

// ─────────────────────────────────────────────────────────────────────────────
// Shared kernel helpers (single-source logic; per-adapter vocabulary passed in)
// ─────────────────────────────────────────────────────────────────────────────

/** The four engine-log buckets, in fixed render order — shared by both adapters. */
export const ENGINE_LOG_BUCKETS: readonly EngineActionKind[] = ["fired", "cancelled", "rejected", "clamped"];

/** The engine log classified into the four fixed buckets (in order) + a catch-all `other` for any
 * kind outside the closed union (arrived across an untyped/JSON/`as` boundary — surfaced, never
 * dropped: absent-not-hidden). The classification is single-source; each adapter formats it. */
export interface EngineLogBuckets {
  readonly buckets: readonly { readonly kind: EngineActionKind; readonly items: readonly EngineAction[] }[];
  readonly other: readonly EngineAction[];
}

/** Classify an engine log into the fixed buckets (order-preserving) + the catch-all `other`. PURE. */
export function bucketEngineLog(elog: readonly EngineAction[]): EngineLogBuckets {
  const known: ReadonlySet<string> = new Set<string>(ENGINE_LOG_BUCKETS);
  return {
    buckets: ENGINE_LOG_BUCKETS.map((kind) => ({ kind, items: elog.filter((e) => e.kind === kind) })),
    other: elog.filter((e) => !known.has(e.kind)),
  };
}

/** The per-adapter label vocabulary for {@link restingLabel} — `live` and `clamped` are INDEPENDENT
 * flags, so a live+clamped order surfaces BOTH (a `live` label can never mask a premium-band clamp). */
export interface RestingLabels {
  readonly live: string;
  readonly liveClamped: string;
  readonly clamped: string;
  readonly off: string;
}

/** The resting-order state label under an adapter's vocabulary. `live`/`clamped` independent
 * (fail-closed honesty: a clamped price is never hidden by a LIVE label). PURE. */
export function restingLabel(o: RestingOrder, labels: RestingLabels): string {
  const live = o.live === true;
  const clamped = o.clamped === true;
  if (live) return clamped ? labels.liveClamped : labels.live;
  return clamped ? labels.clamped : labels.off;
}

/** The per-adapter shape of an engine action's id token (canonical `id@seqN (reason)`, arm
 * `id@N(reason)`). `seqPrefix` sits between `@` and the ordinal; `reasonSpace` inserts a space
 * before the `(reason)` group. Reason-when-present: no reason ⇒ byte-identical to the id@seq alone. */
export interface ActionTokenStyle {
  readonly seqPrefix: string;
  readonly reasonSpace: boolean;
}

/** An engine action's render token `id@<seqPrefix><ordinal>` plus `(reason)` WHEN present (the
 * engine-authored cause — never-naked / plan-budget / regime-UNKNOWN — that must not be silently
 * dropped at the render seam, kestrel-75n). PURE. */
export function actionToken(e: EngineAction, style: ActionTokenStyle): string {
  const reason = e.reason != null && e.reason !== "" ? `${style.reasonSpace ? " " : ""}(${e.reason})` : "";
  return `${e.id}@${style.seqPrefix}${e.asofSeq}${reason}`;
}

/**
 * A bucket's action tokens with each CONSECUTIVE RUN of items sharing an identical `(id, reason)`
 * collapsed to `<count>x <exemplar token>` instead of N near-duplicate tokens (kestrel-7bip). A plan
 * whose STATE trigger holds every tick emits the SAME refusal (`uncovered sell refused: never naked`)
 * once per tick — 24 byte-identical entries in one frame — flooding the very percept the token-optimal
 * renderer exists to conserve. The de-dup is a RENDER-time collapse ONLY (the engine still authors and
 * counts every action; the bucket's `items.length` count is unchanged): a run of one renders as the
 * bare token (byte-identical to pre-collapse, so no existing frame churns), and the exemplar keeps its
 * real `@seq` so the collapsed line stays a concrete, addressable receipt. Order-preserving; PURE. */
export function collapseActionTokens(items: readonly EngineAction[], style: ActionTokenStyle): string[] {
  const out: string[] = [];
  for (let i = 0; i < items.length; ) {
    const e = items[i]!;
    let j = i + 1;
    while (j < items.length && items[j]!.id === e.id && (items[j]!.reason ?? null) === (e.reason ?? null)) j++;
    const count = j - i;
    const token = actionToken(e, style);
    out.push(count > 1 ? `${count}x ${token}` : token);
    i = j;
  }
  return out;
}
