/**
 * remote/port — the RemoteControlPort + the RemoteControlHub reference impl.
 *
 * The hub is the central object of remote-control. It:
 *   - IS a `SessionEventSink` — the agent loop emits events to it;
 *   - keeps a ring buffer of recent events so a reconnecting client can
 *     backfill gap-free (using each event's monotonic `seq`);
 *   - fans events out to live subscribers (the SSE/relay transports);
 *   - owns the `InstructionInbox` — the inbound steering channel;
 *   - derives live `SessionState` from the event stream.
 *
 * Transport-agnostic: `http-server.ts` and `relay-client.ts` are thin
 * adapters over this hub — exactly the port/transport split the SDK already
 * uses for HITL (`ApprovalChannel` + `hitl/slack.ts`).
 *
 * @public @since 2.11.0 — preste remote-control T1
 */
import { normalizeEventType } from "./event-name-map.js";
import { InMemoryInstructionInbox } from "./inbox.js";
import { tryPersistEvent } from "./persistence.js";
import { signEvent } from "./signing.js";
import { InMemoryVetoChannel } from "./veto.js";
/** Create a RemoteControlHub — the reference RemoteControlPort. */
export function createRemoteControlHub(opts = {}) {
    const bufferSize = opts.bufferSize ?? 1000;
    const inbox = opts.inbox ?? new InMemoryInstructionInbox();
    const vetoChannel = opts.vetoChannel ?? new InMemoryVetoChannel();
    const eventNaming = opts.eventNaming ?? "canonical";
    const buffer = [];
    const subscribers = new Set();
    const state = {
        runId: null,
        agentId: null,
        status: "idle",
        stepCount: 0,
        costUsd: 0,
        elapsedMs: 0,
        pendingHitl: 0,
    };
    let startedAtMs = null;
    /**
     * Fold one event into the derived session state. The folder operates on
     * the LEGACY type vocabulary ; if the incoming event arrived in canonical
     * form, we project its type to legacy for the switch — payload shapes
     * are vocabulary-agnostic so this is safe.
     */
    function updateState(event) {
        const legacyType = normalizeEventType(event.type, "legacy");
        switch (legacyType) {
            case "run.start":
                state.runId = event.data.runId;
                state.agentId = event.data.agentId;
                state.status = "running";
                state.stepCount = 0;
                state.costUsd = 0;
                startedAtMs = Date.now();
                break;
            case "run.step":
                state.stepCount = event.data.stepIndex + 1;
                state.costUsd += event.data.costUsd;
                break;
            case "run.finished":
                state.status = "finished";
                state.stepCount = event.data.stepCount;
                state.costUsd = event.data.costUsd;
                break;
            case "hitl.request":
                state.pendingHitl += 1;
                break;
            case "hitl.resolved":
                state.pendingHitl = Math.max(0, state.pendingHitl - 1);
                break;
            default:
                break;
        }
    }
    return {
        emit(rawEvent) {
            // Sign first — the buffered + streamed event must carry `sig` so a
            // reconnecting client and a live client see the same signed event.
            let event = rawEvent;
            if (opts.signFn) {
                try {
                    event = signEvent(rawEvent, opts.signFn);
                }
                catch {
                    event = rawEvent; // signing failure is non-fatal — emit unsigned
                }
            }
            // Normalise the `type` field to the configured naming mode at the
            // emit boundary. Legacy mode is the default ; canonical opt-in
            // rewrites the type to AG-UI UPPER_SNAKE_CASE (no double-emission,
            // one logical event = one buffered + streamed frame).
            //
            // The signature is computed BEFORE the type rewrite so signed payloads
            // remain canonical-content-stable through the rename. A verifier that
            // accepts both forms (per `looksLikeSessionEvent`) will validate the
            // signature regardless of the surface type vocabulary.
            const normalizedType = normalizeEventType(event.type, eventNaming);
            if (normalizedType !== event.type) {
                event = { ...event, type: normalizedType };
            }
            updateState(event);
            buffer.push(event);
            if (buffer.length > bufferSize)
                buffer.shift();
            // Fire-and-forget persistence write. Failures are logged inside
            // tryPersistEvent ; never throw into the agent loop.
            void tryPersistEvent(opts.persistence, event);
            for (const fn of subscribers) {
                try {
                    fn(event);
                }
                catch {
                    // a broken subscriber must never break the run
                    subscribers.delete(fn);
                }
            }
        },
        subscribe(fn) {
            subscribers.add(fn);
            return () => subscribers.delete(fn);
        },
        backlog(sinceSeq) {
            if (sinceSeq === undefined)
                return [...buffer];
            return buffer.filter((e) => e.seq > sinceSeq);
        },
        inbox,
        vetoChannel,
        state() {
            return {
                ...state,
                elapsedMs: startedAtMs === null ? 0 : Date.now() - startedAtMs,
            };
        },
    };
}
/**
 * Warm a hub's in-memory backlog from a persistence store. Call once
 * after `createRemoteControlHub` and before serving traffic ; the events
 * become accessible via `hub.backlog()` exactly as if they had just been
 * emitted (no re-signing, no subscriber dispatch).
 *
 * Bounded by the hub's `bufferSize` ; oldest events are dropped first.
 *
 * @param hub          The hub returned by {@link createRemoteControlHub}.
 * @param persistence  The same persistence handed to the hub options.
 * @param opts         Optional cutoff `sinceSeq` (load only newer events).
 *
 * @public @since 2.21.0 — preste remote-control P7
 */
export async function loadHubFromPersistence(hub, persistence, opts = {}) {
    const events = await persistence.loadEvents(opts.sinceSeq);
    const maxSeq = await persistence.getMaxSeq();
    // The port doesn't expose `buffer.push` directly — we go through emit().
    // To avoid double-persistence (re-writing every loaded event), the port
    // skips `tryPersistEvent` when no persistence is configured. For replay,
    // we therefore use a side-door: subscribe-replay-unsubscribe.
    //
    // The simpler path is to use the public `emit` and accept idempotent
    // re-writes — `InMemoryPersistencePort.saveEvent` is idempotent on
    // `(seq, id)` and SQLite uses `INSERT OR IGNORE`. The cost is one extra
    // round-trip per event but reload is a startup-time operation.
    for (const event of events) {
        hub.emit(event);
    }
    return { loaded: events.length, maxSeq };
}
//# sourceMappingURL=port.js.map