import * as Credential from '../../Credential.js';
import { ChannelClosedError } from '../../Errors.js';
import { createSessionReceipt } from './Receipt.js';
/**
 * Format a session receipt as a Server-Sent Event.
 *
 * Produces a valid SSE event string with `event: payment-receipt`
 * and the receipt JSON as the `data` field.
 */
export function formatReceiptEvent(receipt) {
    return `event: payment-receipt\ndata: ${JSON.stringify(receipt)}\n\n`;
}
/**
 * Format a need-voucher event as a Server-Sent Event.
 *
 * Emitted when the channel balance is exhausted mid-stream.
 * The client responds by sending a new voucher credential to
 * any mppx-protected endpoint.
 */
export function formatNeedVoucherEvent(params) {
    return `event: payment-need-voucher\ndata: ${JSON.stringify(params)}\n\n`;
}
/**
 * Format an application message as SSE, preserving embedded newlines.
 *
 * SSE requires multi-line payloads to be emitted as separate `data:` fields.
 */
export function formatMessageEvent(value) {
    const data = String(value)
        .split('\n')
        .map((line) => `data: ${line}`)
        .join('\n');
    return `event: message\n${data}\n\n`;
}
/**
 * Parse a raw SSE event string into a typed event.
 *
 * Handles the three event types used by mppx streaming:
 * - `message` (default / no event field) — application data
 * - `payment-need-voucher` — balance exhausted, client should send voucher
 * - `payment-receipt` — final receipt
 */
export function parseEvent(raw) {
    let eventType = 'message';
    const dataLines = [];
    for (const line of raw.split('\n')) {
        if (line.startsWith('event: ')) {
            eventType = line.slice(7).trim();
        }
        else if (line.startsWith('data: ')) {
            dataLines.push(line.slice(6));
        }
        else if (line === 'data:') {
            dataLines.push('');
        }
    }
    if (dataLines.length === 0)
        return null;
    const data = dataLines.join('\n');
    switch (eventType) {
        case 'message':
            return { type: 'message', data };
        case 'payment-need-voucher':
            return { type: 'payment-need-voucher', data: JSON.parse(data) };
        case 'payment-receipt':
            return { type: 'payment-receipt', data: JSON.parse(data) };
        default:
            return { type: 'message', data };
    }
}
/**
 * Wrap an async iterable with payment metering, producing an SSE stream.
 *
 * `generate` may be either:
 * - An `AsyncIterable<string>` — each yielded value is automatically charged
 *   (one `tickCost` per value).
 * - A callback `(stream: SessionController) => AsyncIterable<string>` — the
 *   generator controls when charges happen by calling `stream.charge()`.
 *
 * For each emitted value the stream:
 * 1. Reserves `tickCost` from the channel's available voucher headroom
 *    (auto or manual).
 * 2. If balance is sufficient, emits `event: message` with the value.
 * 3. If balance is exhausted, emits `event: payment-need-voucher`
 *    and polls store until the client tops up the channel.
 * 4. Commits the reserved charge immediately before the chunk is emitted.
 * 5. On generator completion, emits a final `event: payment-receipt`.
 *
 * Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
 */
export function serve(options) {
    const { store, channelId, challengeId, tickCost, generate, pollIntervalMs = 100, signal, } = options;
    const encoder = new TextEncoder();
    return new ReadableStream({
        async start(controller) {
            const aborted = () => signal?.aborted ?? false;
            const emit = (event) => controller.enqueue(encoder.encode(event));
            let prepaidUnits = options.prepaidUnits ?? 0;
            let reservedAmount = 0n;
            let reservedUnits = 0;
            const charge = () => {
                if (prepaidUnits > 0) {
                    prepaidUnits -= 1;
                    return Promise.resolve();
                }
                return reserveChargeOrWait({
                    store,
                    channelId,
                    amount: tickCost,
                    reservedAmount,
                    emit,
                    pollIntervalMs,
                    signal,
                }).then(() => {
                    reservedAmount += tickCost;
                    reservedUnits += 1;
                });
            };
            const iterable = typeof generate === 'function' ? generate({ charge }) : generate;
            try {
                for await (const value of iterable) {
                    if (aborted())
                        break;
                    if (typeof generate !== 'function')
                        await charge();
                    await commitReservedCharges({
                        store,
                        channelId,
                        amount: reservedAmount,
                        units: reservedUnits,
                    });
                    reservedAmount = 0n;
                    reservedUnits = 0;
                    controller.enqueue(encoder.encode(formatMessageEvent(value)));
                }
                if (!aborted()) {
                    const channel = await store.getChannel(channelId);
                    if (channel) {
                        const receipt = createSessionReceipt({
                            challengeId,
                            channelId,
                            acceptedCumulative: channel.highestVoucherAmount,
                            spent: channel.spent,
                            units: channel.units,
                        });
                        controller.enqueue(encoder.encode(formatReceiptEvent(receipt)));
                    }
                }
            }
            catch (e) {
                if (!aborted())
                    controller.error(e);
            }
            finally {
                controller.close();
            }
        },
    });
}
/**
 * Wrap a `ReadableStream<Uint8Array>` (from {@link serve}) in an HTTP
 * `Response` with the correct SSE headers.
 */
export function toResponse(body) {
    return new Response(body, {
        headers: {
            'Cache-Control': 'no-cache, no-transform',
            Connection: 'keep-alive',
            'Content-Type': 'text/event-stream; charset=utf-8',
        },
    });
}
/**
 * Extract `channelId`, `challengeId`, and `tickCost` from a `Request`'s
 * `Authorization: Payment …` header.
 *
 * This is a convenience for callers that receive a raw `Request` and need
 * the parameters required by {@link serve}.
 */
export function fromRequest(request) {
    const header = request.headers.get('Authorization');
    if (!header)
        throw new Error('Missing Authorization header.');
    const payment = Credential.extractPaymentScheme(header);
    if (!payment)
        throw new Error('Missing Payment credential in Authorization header.');
    const credential = Credential.deserialize(payment);
    const payload = credential.payload;
    return {
        challengeId: credential.challenge.id,
        channelId: payload.channelId,
        tickCost: BigInt(credential.challenge.request.amount),
    };
}
/**
 * Reserve `amount` of voucher headroom for a future emission, retrying when
 * balance is insufficient. Uses `store.waitForUpdate()` when available for
 * event-driven wakeups, falling back to polling otherwise. Emits
 * `payment-need-voucher` events via `emit` while waiting.
 */
async function reserveChargeOrWait(options) {
    const { store, channelId, amount, emit, pollIntervalMs, reservedAmount, signal } = options;
    let channel = await store.getChannel(channelId);
    if (!channel)
        throw new Error('channel not found');
    throwIfChannelClosed(channel);
    const hasHeadroom = (state) => state.highestVoucherAmount - state.spent - reservedAmount >= amount;
    if (hasHeadroom(channel))
        return;
    // Emit a single need-voucher event, then wait until the accepted voucher
    // headroom covers both already-reserved units and the next requested unit.
    await Promise.resolve(emit(formatNeedVoucherEvent({
        channelId,
        requiredCumulative: (channel.spent + reservedAmount + amount).toString(),
        acceptedCumulative: channel.highestVoucherAmount.toString(),
        deposit: channel.deposit.toString(),
    })));
    while (!hasHeadroom(channel)) {
        await waitForUpdate(store, channelId, pollIntervalMs, signal);
        channel = await store.getChannel(channelId);
        if (!channel)
            throw new Error('channel not found');
        throwIfChannelClosed(channel);
    }
}
async function commitReservedCharges(options) {
    const { store, channelId, amount, units } = options;
    if (amount === 0n || units === 0)
        return;
    let committed = false;
    const channel = await store.updateChannel(channelId, (current) => {
        if (!current)
            return null;
        if (current.finalized)
            return current;
        if (current.closeRequestedAt !== 0n)
            return current;
        if (current.highestVoucherAmount - current.spent < amount)
            return current;
        committed = true;
        return {
            ...current,
            spent: current.spent + amount,
            units: current.units + units,
        };
    });
    if (!channel)
        throw new Error('channel not found');
    if (channel.finalized)
        throw new ChannelClosedError({ reason: 'channel is finalized' });
    if (channel.closeRequestedAt !== 0n)
        throw new ChannelClosedError({ reason: 'channel has a pending close request' });
    if (!committed)
        throw new Error('reserved voucher coverage is no longer available');
}
function throwIfChannelClosed(channel) {
    if (channel.finalized)
        throw new ChannelClosedError({ reason: 'channel is finalized' });
    if (channel.closeRequestedAt !== 0n)
        throw new ChannelClosedError({ reason: 'channel has a pending close request' });
}
async function waitForUpdate(store, channelId, pollIntervalMs, signal) {
    if (signal?.aborted)
        throw new Error('Aborted while waiting for voucher');
    if (store.waitForUpdate) {
        await Promise.race([store.waitForUpdate(channelId), ...(signal ? [abortPromise(signal)] : [])]);
    }
    else {
        await sleep(pollIntervalMs);
    }
    if (signal?.aborted)
        throw new Error('Aborted while waiting for voucher');
}
function abortPromise(signal) {
    return new Promise((resolve) => {
        if (signal.aborted)
            return resolve();
        signal.addEventListener('abort', () => resolve(), { once: true });
    });
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * Check whether a `Response` carries an SSE event stream.
 *
 * Returns `true` when the `Content-Type` header starts with
 * `text/event-stream` (case-insensitive, ignoring charset params).
 */
export function isEventStream(response) {
    const ct = response.headers.get('content-type');
    return ct?.toLowerCase().startsWith('text/event-stream') ?? false;
}
/**
 * Parse an SSE `Response` body into an async iterable of `data:` payloads.
 *
 * Yields the raw `data:` field content for each SSE event in the stream.
 * Events whose data matches the `skip` predicate are silently dropped
 * (e.g. `[DONE]` sentinels used by OpenAI-compatible APIs).
 *
 * Each yielded value typically becomes one charge tick when fed to
 * {@link serve} via the SSE transport's auto-charge mode.
 *
 * @example
 * ```ts
 * const upstream = await fetch('https://api.example.com/stream')
 * for await (const data of Sse.iterateData(upstream)) {
 *   console.log(data)
 * }
 * ```
 */
export async function* iterateData(response, options) {
    const skip = options?.skip;
    const body = response.body;
    if (!body)
        return;
    const reader = body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    try {
        while (true) {
            const { value, done } = await reader.read();
            if (done)
                break;
            buffer += decoder.decode(value, { stream: true });
            // Split on double-newline SSE event boundaries.
            const events = buffer.split('\n\n');
            // Last element may be incomplete — keep in buffer.
            buffer = events.pop() ?? '';
            for (const event of events) {
                if (!event.trim())
                    continue;
                const data = extractData(event);
                if (data === null)
                    continue;
                if (skip?.(data))
                    continue;
                yield data;
            }
        }
        // Flush remaining buffer.
        if (buffer.trim()) {
            const data = extractData(buffer);
            if (data !== null && !skip?.(data))
                yield data;
        }
    }
    finally {
        reader.releaseLock();
    }
}
/** Extract the `data:` field value from a single SSE event block. */
function extractData(event) {
    const dataLines = [];
    for (const line of event.split('\n')) {
        if (line.startsWith('data: '))
            dataLines.push(line.slice(6));
        else if (line === 'data:')
            dataLines.push('');
    }
    return dataLines.length > 0 ? dataLines.join('\n') : null;
}
//# sourceMappingURL=Sse.js.map