import { randomUUID } from 'node:crypto';
import { createAssistantMessageEventStream, } from '@mariozechner/pi-ai';
const DEFAULT_ESTIMATE_CENTS = 100;
export function createInferenceUsageSummary() {
    return {
        inputTokens: 0,
        outputTokens: 0,
        totalTokens: 0,
        totalCostUsd: 0,
    };
}
export function addInferenceUsage(summary, usage) {
    if (!usage || typeof usage !== 'object')
        return;
    const u = usage;
    const input = numberOrZero(u.input) + numberOrZero(u.cacheRead) + numberOrZero(u.cacheWrite);
    const output = numberOrZero(u.output);
    summary.inputTokens += input;
    summary.outputTokens += output;
    summary.totalTokens += numberOrZero(u.totalTokens) || input + output;
    summary.totalCostUsd += numberOrZero(u.cost?.total);
}
export function inferenceUsageToBillableCents(summary) {
    if (summary.totalCostUsd <= 0)
        return null;
    return Math.max(1, Math.ceil(summary.totalCostUsd * 100));
}
export function createCloudInferenceBillingClient(env = process.env, fetchFn = fetch) {
    return new CloudInferenceBillingClient(loadCloudConfig(env), fetchFn);
}
export function isStudiographCreditsMode(env = process.env) {
    return env.STUDIOGRAPH_AI_MODE === 'studiograph_credits';
}
export function studiographCreditsModel(env = process.env) {
    return {
        id: env.STUDIOGRAPH_CLOUD_MODEL_ID?.trim() || 'studiograph-credits',
        name: 'Studiograph Credits',
        api: 'studiograph-cloud-inference',
        provider: 'studiograph-cloud',
        baseUrl: trimTrailingSlash(env.STUDIOGRAPH_CLOUD_URL) || '',
        reasoning: true,
        input: ['text', 'image'],
        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
        contextWindow: 200000,
        maxTokens: 8192,
    };
}
export function createCloudInferenceStreamFn(env = process.env, fetchFn = fetch) {
    const client = new CloudInferenceBillingClient(loadCloudConfig(env), fetchFn);
    return (_model, context, options) => {
        return client.streamChat(context, options);
    };
}
export class CloudInferenceBillingClient {
    loaded;
    fetchFn;
    constructor(loaded, fetchFn) {
        this.loaded = loaded;
        this.fetchFn = fetchFn;
    }
    isConfigured() {
        return Boolean(this.loaded.config);
    }
    isRequired() {
        return this.loaded.required;
    }
    disabledReason() {
        return this.loaded.disabledReason;
    }
    estimateCents() {
        return this.loaded.config?.estimateCents ?? DEFAULT_ESTIMATE_CENTS;
    }
    nextRequestId(prefix = 'chat') {
        return `sg_${prefix}_${Date.now()}_${randomUUID()}`;
    }
    async getAiSettings() {
        const config = this.requireConfigOrNull();
        if (!config)
            return null;
        return this.requestJson('GET', this.runtimePath(config, '/ai'));
    }
    async reserve(input) {
        const config = this.requireConfigOrNull();
        if (!config)
            return null;
        const response = await this.requestJson('POST', this.runtimePath(config, '/inference-reservations'), {
            estimatedCents: input.estimatedCents ?? config.estimateCents,
            provider: input.provider ?? undefined,
            model: input.model ?? undefined,
            requestId: input.requestId ?? this.nextRequestId(),
            description: input.description ?? 'Hosted Studiograph chat inference.',
        });
        return response.reservation;
    }
    async settle(reservation, input) {
        const config = this.requireConfigOrNull();
        if (!config)
            return;
        await this.requestJson('POST', this.runtimePath(config, `/inference-reservations/${encodeURIComponent(reservation.id)}/settle`), {
            actualCents: input.actualCents,
            inputTokens: input.inputTokens,
            outputTokens: input.outputTokens,
            description: input.description,
        });
    }
    async release(reservation, reason) {
        const config = this.requireConfigOrNull();
        if (!config)
            return;
        await this.requestJson('POST', this.runtimePath(config, `/inference-reservations/${encodeURIComponent(reservation.id)}/release`), { reason });
    }
    streamChat(context, options) {
        const stream = createAssistantMessageEventStream();
        void this.runStreamChat(stream, context, options);
        return stream;
    }
    requireConfigOrNull() {
        if (this.loaded.config)
            return this.loaded.config;
        if (this.loaded.required) {
            throw new Error(`Studiograph Cloud billing is required but not configured: ${this.loaded.disabledReason}`);
        }
        return null;
    }
    runtimePath(config, suffix) {
        const orgId = encodeURIComponent(config.orgId);
        const workspaceId = encodeURIComponent(config.workspaceId);
        return `/api/runtime/orgs/${orgId}/workspaces/${workspaceId}${suffix}`;
    }
    async runStreamChat(stream, context, options) {
        try {
            const config = this.requireConfigOrNull();
            if (!config)
                throw new Error('Studiograph Cloud inference is not configured.');
            const requestStartedAt = Date.now();
            const response = await this.fetchFn(`${config.baseUrl}${this.runtimePath(config, '/inference/chat')}`, {
                method: 'POST',
                headers: {
                    Authorization: `Bearer ${config.token}`,
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    requestId: this.nextRequestId('chat'),
                    context,
                    stream: true,
                    options: sanitizeStreamOptions(options),
                }),
                signal: options?.signal,
            });
            if (!response.ok) {
                const json = await response.json().catch(() => ({}));
                const message = typeof json === 'object' && json && 'error' in json
                    ? String(json.error)
                    : `${response.status} ${response.statusText}`;
                throw new Error(`Studiograph Cloud inference request failed: ${message}`);
            }
            const reader = response.body?.getReader();
            if (!reader)
                throw new Error('Studiograph Cloud inference response did not include a stream.');
            const decoder = new TextDecoder();
            let buffer = '';
            let firstEventAt = null;
            while (true) {
                const { done, value } = await reader.read();
                if (done)
                    break;
                buffer += decoder.decode(value, { stream: true });
                const frames = buffer.split('\n\n');
                buffer = frames.pop() ?? '';
                for (const frame of frames) {
                    const event = parseSseDataFrame(frame);
                    if (!event)
                        continue;
                    if (!firstEventAt && event.type === 'text_delta') {
                        firstEventAt = Date.now();
                        // Keep local latency visibility near the request origin without
                        // changing the public chat SSE event shape.
                        console.info(`[cloud-inference] first token received after ${firstEventAt - requestStartedAt}ms`);
                    }
                    stream.push(event);
                }
            }
        }
        catch (err) {
            stream.push(createLocalErrorEvent(err));
        }
    }
    async requestJson(method, path, body) {
        const config = this.requireConfigOrNull();
        if (!config)
            throw new Error('Studiograph Cloud billing is not configured.');
        const response = await this.fetchFn(`${config.baseUrl}${path}`, {
            method,
            headers: {
                Authorization: `Bearer ${config.token}`,
                ...(body ? { 'Content-Type': 'application/json' } : {}),
            },
            body: body ? JSON.stringify(body) : undefined,
        });
        const json = await response.json().catch(() => ({}));
        if (!response.ok) {
            const message = typeof json === 'object' && json && 'error' in json
                ? String(json.error)
                : `${response.status} ${response.statusText}`;
            throw new Error(`Studiograph Cloud billing request failed: ${message}`);
        }
        return json;
    }
}
function loadCloudConfig(env) {
    const required = parseBoolean(env.STUDIOGRAPH_CLOUD_BILLING_REQUIRED);
    const baseUrl = trimTrailingSlash(env.STUDIOGRAPH_CLOUD_URL);
    const orgId = env.STUDIOGRAPH_CLOUD_ORG_ID?.trim();
    const workspaceId = env.STUDIOGRAPH_CLOUD_WORKSPACE_ID?.trim();
    const token = env.STUDIOGRAPH_CLOUD_TOKEN?.trim();
    const estimateCents = parsePositiveInt(env.STUDIOGRAPH_CLOUD_INFERENCE_ESTIMATE_CENTS) ?? DEFAULT_ESTIMATE_CENTS;
    const missing = [
        !baseUrl ? 'STUDIOGRAPH_CLOUD_URL' : '',
        !orgId ? 'STUDIOGRAPH_CLOUD_ORG_ID' : '',
        !workspaceId ? 'STUDIOGRAPH_CLOUD_WORKSPACE_ID' : '',
        !token ? 'STUDIOGRAPH_CLOUD_TOKEN' : '',
    ].filter(Boolean);
    if (missing.length > 0) {
        return {
            config: null,
            required,
            disabledReason: `missing ${missing.join(', ')}`,
        };
    }
    return {
        required,
        disabledReason: null,
        config: {
            baseUrl,
            orgId: orgId,
            workspaceId: workspaceId,
            token: token,
            required,
            estimateCents,
        },
    };
}
function trimTrailingSlash(value) {
    return value?.trim().replace(/\/+$/, '') ?? '';
}
function parseBoolean(value) {
    if (!value)
        return false;
    return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
}
function sanitizeStreamOptions(options) {
    if (!options)
        return {};
    return {
        temperature: options.temperature,
        maxTokens: options.maxTokens,
        reasoning: options.reasoning,
        thinkingBudgets: options.thinkingBudgets,
        cacheRetention: options.cacheRetention,
        sessionId: options.sessionId,
        metadata: options.metadata,
    };
}
function parseSseDataFrame(frame) {
    const data = frame
        .split(/\r?\n/)
        .filter((line) => line.startsWith('data: '))
        .map((line) => line.slice('data: '.length))
        .join('\n');
    if (!data)
        return null;
    try {
        return JSON.parse(data);
    }
    catch {
        return null;
    }
}
function createLocalErrorEvent(err) {
    const message = err instanceof Error ? err.message : String(err);
    return {
        type: 'error',
        reason: 'error',
        error: {
            role: 'assistant',
            content: [{ type: 'text', text: message }],
            api: 'studiograph-cloud-inference',
            provider: 'studiograph-cloud',
            model: 'studiograph-credits',
            usage: {
                input: 0,
                output: 0,
                cacheRead: 0,
                cacheWrite: 0,
                totalTokens: 0,
                cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
            },
            stopReason: 'error',
            errorMessage: message,
            timestamp: Date.now(),
        },
    };
}
function parsePositiveInt(value) {
    if (!value)
        return null;
    const parsed = Number.parseInt(value, 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
function numberOrZero(value) {
    return typeof value === 'number' && Number.isFinite(value) ? value : 0;
}
//# sourceMappingURL=cloud-inference-billing.js.map