/**
 * # bus/plan-instances — group a bus's PLAN stream into exact Plan INSTANCES (kestrel-22j.15)
 *
 * A pure, deterministic fold of a bus's {@link PlanEvent}s into the distinct Plan **instances** they trace —
 * the ONE place both report projections (`project(bus)` in {@link ../blotter/project.ts} and `buildReport`
 * in {@link ../session/sim.ts}) agree on "what is one instance", so their groupings can never drift.
 *
 * ## Why a name is not an instance (the P0 this fixes)
 * A plan NAME is a **Lineage** key (names-are-data, ADR-0006): `fade-ladder` authored fresh on 40 days is one
 * strategy with 40 instances, and a name may legally RECUR after an earlier instance is `done` (the F4
 * same-name-supersession guard in {@link ../engine/plans.ts} only forbids re-declaring a name still owned by a
 * NOT-done plan). Grouping the PLAN stream by name ALONE therefore merges a `done` instance and its same-name
 * replacement into one trace whose terminal outcome is the last one seen — silently erasing the predecessor's
 * (kestrel-22j.15). Grouping by INSTANCE keeps them apart; Lineage aggregation (by name) is a SEPARATE roll-up
 * a consumer does over `group.plan`.
 *
 * ## The instance key (v5 explicit, else an explicit pre-v5 migration)
 * - **v5 bus** — every PLAN event carries {@link PlanEvent.plan_instance}, the id the engine minted
 *   deterministically at registration. The key IS that id: exact, no inference.
 * - **pre-v5 bus** (no `plan_instance`) — MIGRATE EXPLICITLY by **lifecycle sessionization**: a name's next
 *   instance opens at the structural boundary where its currently-open instance has already reached a terminal
 *   `done`. This is exact — never a timestamp heuristic, never fuzzy name-matching — because F4 guarantees a
 *   name only recurs after its predecessor is `done`, so a `done`→next-event boundary is precisely one
 *   instance boundary. Fail-closed doctrine (RUNTIME §8): a pre-v5 bus is migrated, never silently
 *   mis-identified.
 *
 * A single bus is all-or-nothing (an engine either stamps every plan event with an id or, pre-v5, none), so the
 * two paths never interleave within one bus.
 *
 * Determinism (RUNTIME §0): pure function of the bus bytes — no wall clock, no RNG. Output order is
 * `(name asc, first-seq asc)`, which reproduces the prior name-sorted order exactly when every name has a
 * single instance, and orders same-name instances by first appearance.
 */

import type { BusEvent, PlanEvent } from "./types.ts";

/** One exact Plan instance traced off the bus: its Lineage name, its instance identity (the explicit
 * `plan_instance` on a v5 bus, else a deterministic sessionized key), and its PLAN events in seq order. */
export interface PlanInstanceGroup {
  /** The authored plan NAME — the Lineage aggregation key (a consumer rolls instances up BY this). */
  readonly plan: string;
  /** The exact instance identity: {@link PlanEvent.plan_instance} (v5) or a sessionized migration key (pre-v5). */
  readonly instance: string;
  /** This instance's PLAN events, in bus seq order (lifecycle transitions + any reject notices). */
  readonly events: readonly PlanEvent[];
}

/** The synthetic instance key for a pre-v5 (id-less) PLAN stream — the migration key. ` ` cannot appear
 * in a JSON string token boundary a name would collide on, and the `#<ordinal>` suffix disambiguates same-name
 * instances by their session order. Never leaks onto the bus; it is a projection-time grouping key only. */
function sessionKey(name: string, ordinal: number): string {
  return `${name} #${ordinal}`;
}

/**
 * Group a bus's PLAN events into the distinct Plan instances they trace (see the module header). Pure and
 * deterministic. Non-PLAN events are ignored. The bus is expected in seq order (as every well-formed bus is);
 * events are folded in the order given.
 */
export function groupPlanInstances(bus: readonly BusEvent[]): PlanInstanceGroup[] {
  interface Group {
    plan: string;
    instance: string;
    events: PlanEvent[];
    firstSeq: number;
  }
  const byKey = new Map<string, Group>();
  const order: string[] = [];

  // Sessionization bookkeeping (pre-v5 path only): the currently-open instance per name + whether it has
  // reached a terminal `done`, plus the running ordinal for that name.
  const openByName = new Map<string, { key: string; terminal: boolean }>();
  const ordinalByName = new Map<string, number>();

  for (const e of bus) {
    if (e.stream !== "PLAN") continue;
    const pe = e as PlanEvent;
    const name = pe.plan;

    let key: string;
    if (pe.plan_instance !== undefined) {
      // v5: the exact minted identity — no inference.
      key = pe.plan_instance;
    } else {
      // pre-v5: explicit migration by lifecycle sessionization.
      const open = openByName.get(name);
      let openedNew: boolean;
      if (open === undefined || open.terminal) {
        const ordinal = (ordinalByName.get(name) ?? -1) + 1;
        ordinalByName.set(name, ordinal);
        key = sessionKey(name, ordinal);
        openedNew = true;
      } else {
        key = open.key;
        openedNew = false;
      }
      // The freshly-opened instance's terminal flag is derived from the CURRENT event alone — never carried
      // from the still-terminal predecessor (which would immediately re-stamp the replacement terminal and
      // mint a phantom instance per subsequent same-name event, kestrel-22j.15).
      openByName.set(name, { key, terminal: (openedNew ? false : open!.terminal) || pe.state === "done" });
    }

    let g = byKey.get(key);
    if (g === undefined) {
      g = { plan: name, instance: key, events: [], firstSeq: pe.seq };
      byKey.set(key, g);
      order.push(key);
    }
    g.events.push(pe);
  }

  return order
    .map((k) => byKey.get(k)!)
    .sort((a, b) => (a.plan < b.plan ? -1 : a.plan > b.plan ? 1 : a.firstSeq - b.firstSeq))
    .map((g) => ({ plan: g.plan, instance: g.instance, events: g.events }));
}
