/**
 * Voice-call observer real-time stream.
 *
 * The voice agent ("agent-engine") exposes a per-call WebSocket that emits
 * the live timeline of a call: transcripts (user + agent), emotion / empathy
 * frames, latency markers, tool invocations, navigation timing, and lifecycle
 * events (start / end / participant join / leave).
 *
 * This resource is the canonical SDK consumer for that stream. Before it
 * existed, every developer-console-style app re-implemented the loop by hand
 * — different idle thresholds, missing close-code handling for the long list
 * of voice-specific 4xxx codes (4001 unauthorized, 4003 forbidden, 4029 rate
 * limit, 4100 token expired), and inconsistent reconnect backoff.
 *
 * The implementation composes {@link createReconnectingWebSocket} with a
 * lightweight ``ObserverSSEEvent`` parser. Frames that don't deserialize or
 * lack a recognized ``type`` discriminator are dropped — the stream remains
 * forward-compatible with new event types added on the server before the SDK
 * spec snapshot picks them up.
 *
 * @see ConversationsResource.subscribeTextStream for the analogous text-stream
 *   helper.
 */
import { ConfigurationError } from '../core/errors.js';
import { createReconnectingWebSocket, } from '../core/reconnecting-websocket.js';
import { WorkspaceScopedResource } from './base.js';
/** Token grammar accepted in WebSocket subprotocols (RFC 6455). */
const WEB_SOCKET_PROTOCOL_TOKEN_RE = /^[!#$%&'*+\-.^_`|~A-Za-z0-9]+$/;
const MAX_AUTH_TOKEN_CHARS = 4096;
/**
 * Build browser WebSocket subprotocols for the observer stream.
 *
 * Identical wire format to the text-stream subprotocol pair —
 * ``Sec-WebSocket-Protocol: auth, <token>`` — but exposed as its own
 * function so consumers do not have to reason about cross-resource sharing
 * and so the SDK can evolve the auth scheme independently per stream.
 *
 * @security The returned tuple contains the raw bearer token. Do not log,
 *   persist, serialize, or otherwise expose this value.
 */
export function observerAuthProtocols(token) {
    if (!token) {
        throw new ConfigurationError('observerAuthProtocols requires a non-empty token');
    }
    if (token.length > MAX_AUTH_TOKEN_CHARS) {
        throw new ConfigurationError(`observer token exceeds the ${MAX_AUTH_TOKEN_CHARS}-character WebSocket subprotocol limit`);
    }
    if (!WEB_SOCKET_PROTOCOL_TOKEN_RE.test(token)) {
        throw new ConfigurationError('observer token contains characters browsers reject in WebSocket subprotocols');
    }
    return ['auth', token];
}
/**
 * Voice-call observer resource.
 *
 * @example
 * ```ts
 * const handle = client.observers.subscribe({
 *   callSid: 'CAxxx',
 *   token: bearerToken,
 *   onEvent: (event) => {
 *     switch (event.type) {
 *       case 'agent_transcript_delta':
 *         renderAgentDelta(event.text)
 *         break
 *       case 'user_transcript':
 *         renderUserTurn(event.text)
 *         break
 *       case 'session_end':
 *         showSummary(event)
 *         break
 *     }
 *   },
 *   onError: (err) => console.error('observer terminal:', err.reason),
 * })
 *
 * // Later, to stop:
 * handle.close()
 * await handle.done
 * ```
 */
export class ObserversResource extends WorkspaceScopedResource {
    agentBaseUrl;
    constructor(client, workspaceId, agentBaseUrl) {
        super(client, workspaceId);
        this.agentBaseUrl = agentBaseUrl;
    }
    /**
     * Subscribe to the live observer stream for a call.
     *
     * Returns a {@link ReconnectingWebSocketHandle} that resolves
     * ``handle.done`` when the stream terminates (consumer-aborted, terminal
     * close code, or reconnect budget exhausted). Errors are surfaced through
     * ``onError``; the promise never rejects.
     */
    subscribe(options) {
        const url = buildObserverUrl({
            baseUrl: this.agentBaseUrl ?? this.platformBaseUrl,
            workspaceId: this.workspaceId,
            callSid: options.callSid,
            observerUrl: options.observerUrl,
        });
        const protocols = observerAuthProtocols(options.token);
        return createReconnectingWebSocket({
            url,
            protocols: [...protocols],
            onMessage: (ev) => {
                const parsed = parseObserverFrame(ev.data);
                if (parsed) {
                    try {
                        options.onEvent(parsed);
                    }
                    catch {
                        // Consumer threw in onEvent — do not let it kill the loop.
                    }
                }
            },
            onStateChange: options.onStateChange,
            onReconnect: options.onReconnect,
            onError: options.onError,
            signal: options.signal,
            idleTimeoutMs: options.idleTimeoutMs ?? 60_000,
            initialDelayMs: options.initialDelayMs ?? 1_000,
            maxDelayMs: options.maxDelayMs ?? 30_000,
            maxReconnects: options.maxReconnects ?? 10,
            webSocketFactory: options.webSocketFactory,
        });
    }
}
const CALL_SID_RE = /^CA[a-zA-Z0-9]{32}$/;
function buildObserverUrl(args) {
    if (!args.workspaceId) {
        throw new ConfigurationError('workspaceId is required to build the observer URL');
    }
    if (!args.callSid) {
        throw new ConfigurationError('callSid is required to subscribe to the observer stream');
    }
    if (args.observerUrl) {
        // Override path — the consumer has already constructed the full URL
        // (e.g. via a custom voice-agent gateway helper). Skip the Twilio CA
        // SID grammar check; the override owns the URL shape.
        return parseOverride(args.observerUrl).toString();
    }
    // Default path — derive ``/v1/{ws}/observers/{callSid}/ws`` and validate
    // the CA SID grammar so a typo fails at the function boundary instead of
    // mid-reconnect-loop with an opaque 4001.
    if (!CALL_SID_RE.test(args.callSid)) {
        throw new ConfigurationError(`callSid does not match Twilio CA SID format (CA + 32 hex chars): ${args.callSid}`);
    }
    return deriveFromBase(args.baseUrl, args.workspaceId, args.callSid).toString();
}
function parseOverride(observerUrl) {
    let url;
    try {
        url = new URL(observerUrl);
    }
    catch (cause) {
        throw new ConfigurationError(`observerUrl must be an absolute URL: ${String(cause)}`);
    }
    if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
        throw new ConfigurationError('observerUrl overrides must use ws: or wss: URLs');
    }
    if (url.search || url.hash) {
        throw new ConfigurationError('observerUrl overrides must not include query parameters or fragments');
    }
    return url;
}
function deriveFromBase(baseUrl, workspaceId, callSid) {
    let parsed;
    try {
        parsed = new URL(baseUrl);
    }
    catch (cause) {
        throw new ConfigurationError(`observerUrl cannot be derived from baseUrl: ${String(cause)}`);
    }
    // Map http(s) to ws(s) — the agent-engine ALB serves both on the same host.
    let scheme;
    if (parsed.protocol === 'https:' || parsed.protocol === 'wss:')
        scheme = 'wss:';
    else if (parsed.protocol === 'http:' || parsed.protocol === 'ws:')
        scheme = 'ws:';
    else {
        throw new ConfigurationError(`observerUrl can only be derived from an http, https, ws, or wss baseUrl: ${baseUrl}`);
    }
    // The pathname must be '/' — observer paths are owned by this helper,
    // not the consumer's baseUrl gateway.
    if (parsed.pathname !== '/' && parsed.pathname !== '') {
        throw new ConfigurationError('observerUrl can only be derived from an origin-only baseUrl; pass observerUrl explicitly when using path-prefixed gateways');
    }
    const out = new URL(`${scheme}//${parsed.host}/v1/${encodeURIComponent(workspaceId)}/observers/${encodeURIComponent(callSid)}/ws`);
    return out;
}
// ---------------------------------------------------------------------------
// Frame parser
// ---------------------------------------------------------------------------
/**
 * Parse a raw WebSocket frame into a typed {@link ObserverSSEEvent}.
 *
 * Drift-tolerant: returns ``null`` on JSON decode errors, non-object
 * payloads, missing ``type`` discriminator, or unknown discriminator
 * values. The static type of ``ObserverSSEEvent`` is the contract — the
 * server may add new event types ahead of an SDK release and the consumer's
 * exhaustive ``switch`` will simply not match them, leaving the unknown
 * frame on the floor.
 */
function parseObserverFrame(data) {
    let text;
    if (typeof data === 'string') {
        text = data;
    }
    else if (data instanceof ArrayBuffer) {
        text = new TextDecoder().decode(data);
    }
    else if (ArrayBuffer.isView(data)) {
        // ``data`` is some TypedArray; @types/node TextDecoder.decode wants
        // ``BufferSource``. Pass through as ``Uint8Array`` over the underlying
        // buffer so every TypedArray variant decodes consistently.
        const view = data;
        text = new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
    }
    else {
        return null;
    }
    let payload;
    try {
        payload = JSON.parse(text);
    }
    catch {
        return null;
    }
    if (typeof payload !== 'object' || payload === null || Array.isArray(payload))
        return null;
    const obj = payload;
    if (typeof obj['type'] !== 'string')
        return null;
    return obj;
}
//# sourceMappingURL=observers.js.map