/**
 * Section runtime engine — public facade (M7 — Variant C, layered).
 *
 * The facade is the narrow, stable entry point hosts use to mount,
 * drive, and dispose a section runtime. It composes three layers
 * introduced in earlier M7 PRs:
 *
 *   - the **registry** (`RuntimeRegistry`) — registered item/passage
 *     shells in document order, used for canonical-id lookup;
 *   - the **adapter** (`SectionEngineAdapter`) — single I/O seam over
 *     the pure FSM core, fanning outputs to the DOM, framework-error
 *     bus, instrumentation hook, and host subscribers;
 *   - the **resolver** (`resolveSectionEngineRuntimeState`) — the
 *     toolkit-side resolution helper that produces the effective
 *     runtime + tools snapshot a host feeds to the FSM core.
 *
 * The facade exposes an engine-side surface for stage/lifecycle inputs and a
 * controller-facing surface for item registration, session updates, and host
 * controller commands:
 *
 *   1. The **engine-side** surface (`attachHost`, `dispatchInput`,
 *      `subscribe`, `getState`, `getEffectiveRuntime`,
 *      `setInstrumentationHook`). This is what the kernel and the
 *      `pie-assessment-toolkit` CE will switch onto in M7 PR 5 and
 *      PR 6. Construction is lazy: `attachHost(...)` builds the
 *      adapter on first call, with the supplied host element,
 *      framework-error bus, and (optional) coordinator. After
 *      construction, host re-attachments (e.g. cohort change with a
 *      new layout shell) update the adapter's host element via
 *      `setHost(...)`.
 *
 *   2. The **controller-side** surface (`initialize`,
 *      `register`, `unregister`, `handleContent*`, `updateItemSession`,
 *      `navigateToItem`, `persist`, `hydrate`, `dispose`). The
 *      `PieAssessmentToolkit.svelte` CE drives section-controller and
 *      item-shell events through this surface.
 *
 * The two surfaces share `RuntimeRegistry` and the resolved
 * `SectionControllerHandle`.
 */
import { SectionEngineAdapter, } from "./adapter/SectionEngineAdapter.js";
import { resolveSectionEngineRuntimeState, } from "./core/engine-resolver.js";
import { createInitialEngineState, } from "./core/engine-state.js";
import { RuntimeRegistry } from "./RuntimeRegistry.js";
import { createRuntimeId } from "./runtime-id.js";
export class SectionRuntimeEngine {
    registry = new RuntimeRegistry();
    runtimeId = createRuntimeId("section-engine");
    // Engine-side state (PR 3+).
    adapter = null;
    // Controller-side state.
    controller = null;
    coordinator = null;
    sectionId = "";
    attemptId;
    unsubscribeController = null;
    activeInitToken = 0;
    /**
     * Tracks which `(canonicalItemId, contentKind)` pairs have already
     * fired `handleContentLoaded` on this engine. Mirrored alongside
     * `RuntimeRegistry`'s registered set so a cohort handoff can
     * replay the persistent shells' loaded state into the new
     * controller — see `replayRegisteredShellsIntoController` and the
     * PIE-512 Phase B regression test.
     *
     * Cleared in lockstep with `handleContentUnregistered` and on
     * `dispose`. Not exposed publicly: the registry alone is the
     * external surface; the loaded set exists only to seed cohort B's
     * controller with the live state cohort A's shells produced.
     */
    loadedRenderableKeys = new Set();
    // ============================================================
    // Engine-side surface (PR 3+; driven by the kernel from PR 5,
    // by the toolkit CE from PR 6, and by the facade smoke test today).
    // ============================================================
    /**
     * Attach the host element + framework-error bus + sourceCe so the
     * adapter can dispatch DOM events and bus errors. Required before
     * `dispatchInput` / `subscribe` produce observable effects.
     *
     * Constructs the adapter on the first call. Subsequent calls keep
     * the adapter and update its host element / instrumentation hook
     * (used by layout-shell rollovers and instrumentation reconfig).
     */
    attachHost(args) {
        if (this.adapter) {
            this.adapter.setHost(args.host);
            if (args.instrumentationHook !== undefined) {
                this.adapter.setInstrumentationHook(args.instrumentationHook);
            }
            return;
        }
        this.adapter = new SectionEngineAdapter({
            host: args.host,
            runtimeId: this.runtimeId,
            sourceCe: args.sourceCe,
            frameworkErrorBus: args.frameworkErrorBus,
            coordinator: args.coordinator,
            instrumentationHook: args.instrumentationHook,
            now: args.now,
        });
    }
    /**
     * Forward a host-constructed input to the FSM core via the adapter.
     * Returns the outputs the transition produced (subscribers and the
     * DOM/framework-error/instrumentation bridges have already
     * received them).
     *
     * No-op (returns `[]`) before `attachHost(...)` so callers that
     * dispatch optimistically during teardown do not throw.
     */
    dispatchInput(input) {
        return this.adapter?.dispatchInput(input) ?? [];
    }
    /**
     * Subscribe to batched engine outputs (one batch per
     * `dispatchInput`). Returns an idempotent disposer. Pre-`attachHost`
     * subscriptions return a no-op disposer so callers can wire
     * subscriptions early without lifecycle-ordering hazards.
     */
    subscribe(listener) {
        return this.adapter?.subscribe(listener) ?? noopDispose;
    }
    /**
     * Read-only snapshot of the FSM state. Returns the initial idle
     * state before `attachHost` so callers can probe state without
     * special-casing the pre-attach phase.
     */
    getState() {
        return this.adapter?.getState() ?? createInitialEngineState();
    }
    /**
     * Resolve the effective runtime + tools snapshot for the given
     * two-tier inputs. This is a pure function over the resolver — it
     * does not depend on adapter state — so callers can use it both
     * pre- and post-`attachHost`. The kernel uses it to seed
     * `dispatchInput({ kind: "initialize", effectiveRuntime, ... })`.
     */
    getEffectiveRuntime(args, deps) {
        return resolveSectionEngineRuntimeState(args, deps).effectiveRuntime;
    }
    /**
     * Update the adapter's instrumentation hook. No-op pre-attach.
     * Hosts that always want a hook should pass it via `attachHost`.
     */
    setInstrumentationHook(hook) {
        this.adapter?.setInstrumentationHook(hook);
    }
    /**
     * Stable per-engine runtime id used to scope DOM-event details and
     * registry telemetry. Exposed so tests and instrumentation can pin
     * the value the bridges emit.
     */
    getRuntimeId() {
        return this.runtimeId;
    }
    // ============================================================
    // Controller-side surface.
    // ============================================================
    async initialize(args) {
        this.activeInitToken += 1;
        const token = this.activeInitToken;
        this.coordinator = args.coordinator;
        this.sectionId = args.sectionId;
        this.attemptId = args.attemptId;
        const resolved = (await args.coordinator.getOrCreateSectionController({
            sectionId: args.sectionId,
            attemptId: args.attemptId,
            input: {
                section: args.section,
                sectionId: args.sectionId,
                assessmentId: args.assessmentId,
                view: args.view,
            },
            updateExisting: true,
            createDefaultController: args.createDefaultController,
        }));
        if (token !== this.activeInitToken)
            return;
        this.unsubscribeController?.();
        this.controller = resolved;
        // PIE-512 Phase C: always re-feed the registry's currently
        // registered shells (and their loaded state) into the resolved
        // controller. We do NOT gate on `resolved !== previousController`
        // any more.
        //
        // Why the gate had to go:
        //   - Cohort flip resolving to a fresh controller — replay seeds
        //     the new controller, which is the original Phase B fix.
        //   - Same-cohort `updateInput` resolving to the existing
        //     controller — the coordinator's
        //     `resolveExistingSectionController` calls
        //     `existingController.updateInput(input)` (engine always
        //     passes `updateExisting: true`), and pre-Phase-C
        //     `SectionController.initialize` wiped lifecycle tracking
        //     unconditionally. A subscriber attaching between the wipe
        //     and the next live event saw an empty
        //     `loadedRenderables` snapshot.
        //
        // Phase C makes this re-feed safe by combining (a) the
        // section-identity gate around `resetLifecycleTracking()` in
        // `SectionController.initialize` (so same-cohort `updateInput`
        // preserves tracking) with (b) idempotent
        // `handleContentRegistered` / `handleContentLoaded` on the
        // controller (so re-feeding the same registry entries is a
        // no-op if the controller already knows about them, but
        // re-seeds the controller in the post-`updateInput`-wipe case
        // that pre-Phase-C used to encounter).
        this.replayRegisteredShellsIntoController(resolved);
        args.onCompositionChanged?.(resolved.getCompositionModel?.());
        this.unsubscribeController =
            resolved.subscribe?.(() => {
                args.onCompositionChanged?.(resolved.getCompositionModel?.());
            }) || null;
    }
    /**
     * Re-feed the engine's `RuntimeRegistry` and loaded-set into the
     * resolved controller. Call site: `initialize(...)` — runs on
     * EVERY initialize, both cohort-flip-resolves-fresh-controller
     * and same-cohort-resolves-existing-controller cases. Phase C
     * dropped the `resolved !== previousController` gate; see the
     * comment at the call site for why.
     *
     * Two-pass order — register every shell in document order, then
     * issue `handleContentLoaded` for each shell whose load already
     * fired on this engine. The two-pass shape prevents
     * `evaluateSectionLoadingState` from flapping
     * `section-loading-complete` `false→true→false→…` while replay is
     * mid-walk: any subscriber wired before the engine's own
     * `controller.subscribe` (no such caller today, but cheap defense
     * for future wiring) sees one clean `false→true` transition.
     *
     * Idempotent on the controller side — Phase C makes
     * `SectionController.handleContentRegistered` and
     * `handleContentLoaded` early-return on duplicates so re-feeding
     * the same shells into a controller that already tracks them is
     * a true no-op (no spurious re-emits, no re-evaluation of
     * `section-loading-complete`). At the call site no listener is
     * attached during the replay window, so `emitChange` side effects
     * fire into a void on this pass anyway, but the controller-side
     * idempotence is what makes the replay safe to run on every
     * initialize regardless of whether the controller is fresh.
     */
    replayRegisteredShellsIntoController(controller) {
        if (!controller.handleContentRegistered)
            return;
        const shells = this.registry.getOrderedShells();
        if (shells.length === 0)
            return;
        for (const shell of shells) {
            const canonicalItemId = shell.canonicalItemId || shell.itemId;
            controller.handleContentRegistered({
                itemId: shell.itemId,
                canonicalItemId,
                contentKind: shell.contentKind || shell.kind,
            });
        }
        const now = Date.now();
        for (const shell of shells) {
            const canonicalItemId = shell.canonicalItemId || shell.itemId;
            const key = this.getLoadedKey(canonicalItemId, shell.contentKind);
            if (this.loadedRenderableKeys.has(key)) {
                controller.handleContentLoaded?.({
                    itemId: shell.itemId,
                    canonicalItemId,
                    contentKind: shell.contentKind || shell.kind,
                    timestamp: now,
                });
            }
        }
    }
    /**
     * Stable key for the engine's `loadedRenderableKeys` set. Mirrors
     * the `(canonicalItemId, contentKind)` shape `SectionController`
     * uses internally (see `getRenderableKey`); we only need
     * consistent add/check semantics on this side, not byte-identical
     * normalization with the controller.
     */
    getLoadedKey(canonicalItemId, contentKind) {
        return `${contentKind ?? ""}:${canonicalItemId}`;
    }
    register(detail) {
        return this.registry.register(detail);
    }
    unregister(element) {
        return this.registry.unregister(element);
    }
    getCompositionModel() {
        return this.controller?.getCompositionModel?.() ?? null;
    }
    getCanonicalItemId(itemId) {
        const map = this.registry.getCanonicalIdMap();
        return (map[itemId] || this.controller?.getCanonicalItemId?.(itemId) || itemId);
    }
    handleContentRegistered(detail) {
        this.controller?.handleContentRegistered?.({
            itemId: detail.itemId,
            canonicalItemId: detail.canonicalItemId || detail.itemId,
            contentKind: detail.contentKind || detail.kind,
        });
    }
    handleContentUnregistered(detail) {
        const canonicalItemId = detail.canonicalItemId || detail.itemId;
        // Key the load-set delete with the same `contentKind`-only
        // shape that `handleContentLoaded` and
        // `replayRegisteredShellsIntoController` use, so add/check/
        // delete keys round-trip identically. The `|| detail.kind`
        // fallback is preserved for the controller-forwarded payload
        // because the controller normalizes via `toSectionContentKind`
        // and the `kind` field remains meaningful there.
        this.loadedRenderableKeys.delete(this.getLoadedKey(canonicalItemId, detail.contentKind));
        this.controller?.handleContentUnregistered?.({
            itemId: detail.itemId,
            canonicalItemId,
            contentKind: detail.contentKind || detail.kind,
        });
    }
    handleContentLoaded(args) {
        const canonicalItemId = args.canonicalItemId || args.itemId;
        this.loadedRenderableKeys.add(this.getLoadedKey(canonicalItemId, args.contentKind));
        this.controller?.handleContentLoaded?.(args);
    }
    handleItemPlayerError(args) {
        this.controller?.handleItemPlayerError?.(args);
    }
    reportSectionError(args) {
        this.controller?.reportSectionError?.(args);
    }
    updateItemSession(itemId, session) {
        const canonicalId = this.getCanonicalItemId(itemId);
        return this.controller?.updateItemSession?.(canonicalId, session) ?? null;
    }
    navigateToItem(index) {
        return this.controller?.navigateToItem?.(index) ?? null;
    }
    async persist() {
        await this.controller?.persist?.();
    }
    async hydrate() {
        await this.controller?.hydrate?.();
    }
    getRegistry() {
        return this.registry;
    }
    async dispose() {
        this.activeInitToken += 1;
        this.unsubscribeController?.();
        this.unsubscribeController = null;
        if (this.coordinator && this.sectionId) {
            await this.coordinator.disposeSectionController({
                sectionId: this.sectionId,
                attemptId: this.attemptId,
            });
        }
        this.registry.clear();
        this.loadedRenderableKeys.clear();
        this.controller = null;
        this.coordinator = null;
        // Tear down the engine-side adapter (if attached) last so any
        // `dispose` FSM input flows through the bridges before they
        // detach. Adapter disposal is idempotent and safe even when
        // nothing is attached.
        if (this.adapter) {
            const adapter = this.adapter;
            this.adapter = null;
            await adapter.dispose();
        }
    }
}
const noopDispose = () => { };
//# sourceMappingURL=SectionRuntimeEngine.js.map