/**
 * streams-bridge — proxy data-stream signals into A2UI surface data
 * models (OD-CHART-16, option b).
 *
 *                                effect()
 *   data-stream signal  ──────────────────────▶  renderer.process({
 *      (any consumer:                              type: 'updateDataModel',
 *       chart-ui, table-ui,                        surfaceId, path, value
 *       headless app code)                       })
 *
 * Lives in `web-components/core/` rather than `a2ui-utils/` because
 * the dependency direction is web-components → a2ui-utils, not the
 * reverse. The bridge duck-types the renderer (it needs `.process()`,
 * nothing else), so it works with any A2UI-protocol consumer — the
 * actual `A2UIRenderer` from `@adia-ai/a2ui-runtime`, a custom renderer,
 * or a test stub.
 *
 * Contract:
 *   - One-way only: stream → data model. A2UI writes back to its model
 *     (e.g. via `update-data-model` wiring actions) do NOT propagate
 *     into the stream signal. If you need bidirectional state, the
 *     stream is the wrong substrate — use a wiring action.
 *   - Tear down with the returned dispose function. Dispose detaches
 *     the effect; it does NOT release the stream's refcount, because
 *     the bridge isn't the source of truth for the stream's lifecycle
 *     (the DOM consumer or a separate `acquireStream` caller is).
 *   - `select` uses dot-separated path syntax (matches
 *     `data-stream-path` on the trait). `path` uses slash-separated
 *     syntax (matches A2UI bindings).
 *
 * Usage:
 *   const dispose = bridgeStream(renderer, {
 *     surfaceId: 'main',
 *     streamId:  'id:rev',           // explicit data-stream-id
 *     path:      'metrics/revenue',  // A2UI data-model path
 *     select:    'data',             // optional: sub-path within stream
 *   });
 *   // ... later
 *   dispose();
 *
 *   // For DOM-first sources where the stream may register after the
 *   // bridge call returns:
 *   const dispose = await bridgeStreamAsync(renderer, opts);
 */

import { effect } from './signals.js';
import { streams, whenStream } from './data-stream.js';

function resolveSelect(obj, dottedPath) {
  if (!dottedPath) return obj;
  let cursor = obj;
  for (const seg of dottedPath.split('.')) {
    if (cursor == null) return null;
    cursor = cursor[seg];
  }
  return cursor;
}

function attach(renderer, stream, { surfaceId, path, select }) {
  return effect(() => {
    const value = stream.signal.value;
    if (value == null) return;
    const out = select ? resolveSelect(value, select) : value;
    renderer.process({ type: 'updateDataModel', surfaceId, path, value: out });
  });
}

/**
 * Bridge a registered stream into a renderer's surface data model.
 * If the stream is not yet registered, logs a warning and returns a
 * no-op dispose. Use {@link bridgeStreamAsync} when the stream might
 * register after this call (e.g. DOM source connecting on the same
 * tick as the bridge wiring).
 */
export function bridgeStream(renderer, opts) {
  const { streamId } = opts;
  const stream = streams.get(streamId);
  if (!stream) {
    console.warn(
      `[streams-bridge] stream "${streamId}" not registered — ` +
      `call bridgeStreamAsync() to wait, or ensure the DOM source is ` +
      `connected before bridging.`,
    );
    return () => {};
  }
  return attach(renderer, stream, opts);
}

/**
 * Async variant — awaits {@link whenStream} before attaching the
 * effect. Resolves once the bridge is wired (returns the dispose fn).
 */
export async function bridgeStreamAsync(renderer, opts) {
  const stream = await whenStream(opts.streamId);
  return attach(renderer, stream, opts);
}
