import { getSignedCookie, setSignedCookie } from 'hono/cookie';
import { fireEvent } from './app';
import { getServiceUrls } from '@agentuity/server';
import { internal } from './logger/internal';
import { timingSafeEqual } from 'node:crypto';
/**
 * Parse serialized thread data, handling both old (flat state) and new ({ state, metadata }) formats.
 * @internal
 */
export function parseThreadData(raw) {
    if (!raw) {
        return {};
    }
    try {
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed === 'object' && ('state' in parsed || 'metadata' in parsed)) {
            return {
                flatStateJson: parsed.state ? JSON.stringify(parsed.state) : undefined,
                metadata: parsed.metadata && typeof parsed.metadata === 'object' ? parsed.metadata : undefined,
            };
        }
        return { flatStateJson: raw };
    }
    catch {
        return { flatStateJson: raw };
    }
}
// WeakMap to store event listeners for Thread and Session instances
const threadEventListeners = new WeakMap();
const sessionEventListeners = new WeakMap();
// Helper to fire thread event listeners
async function fireThreadEvent(thread, eventName) {
    const listeners = threadEventListeners.get(thread);
    if (!listeners)
        return;
    const callbacks = listeners.get(eventName);
    if (!callbacks || callbacks.size === 0)
        return;
    for (const callback of callbacks) {
        try {
            await callback(eventName, thread);
        }
        catch (error) {
            // Log but don't re-throw - event listener errors should not crash the server
            internal.error(`Error in thread event listener for '${eventName}':`, error);
        }
    }
}
// Helper to fire session event listeners
async function fireSessionEvent(session, eventName) {
    const listeners = sessionEventListeners.get(session);
    if (!listeners)
        return;
    const callbacks = listeners.get(eventName);
    if (!callbacks || callbacks.size === 0)
        return;
    for (const callback of callbacks) {
        try {
            await callback(eventName, session);
        }
        catch (error) {
            // Log but don't re-throw - event listener errors should not crash the server
            internal.error(`Error in session event listener for '${eventName}':`, error);
        }
    }
}
// Generate thread or session ID
export function generateId(prefix) {
    const arr = new Uint8Array(16);
    crypto.getRandomValues(arr);
    return `${prefix}${prefix ? '_' : ''}${arr.toHex()}`;
}
/**
 * Validates a thread ID against runtime constraints:
 * - Must start with 'thrd_'
 * - Must be at least 32 characters long (including prefix)
 * - Must be less than 64 characters long
 * - Must contain only [a-zA-Z0-9] after 'thrd_' prefix (no dashes for maximum randomness)
 */
export function isValidThreadId(threadId) {
    if (!threadId.startsWith('thrd_')) {
        return false;
    }
    if (threadId.length < 32 || threadId.length > 64) {
        return false;
    }
    const validThreadIdCharacters = /^[a-zA-Z0-9]+$/;
    if (!validThreadIdCharacters.test(threadId.substring(5))) {
        return false;
    }
    return true;
}
/**
 * Validates a thread ID and throws detailed error messages for debugging.
 * @param threadId The thread ID to validate
 * @throws Error with detailed message if validation fails
 */
export function validateThreadIdOrThrow(threadId) {
    if (!threadId) {
        throw new Error(`the ThreadIDProvider returned an empty thread id for getThreadId`);
    }
    if (!threadId.startsWith('thrd_')) {
        throw new Error(`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must start with the prefix 'thrd_'.`);
    }
    if (threadId.length > 64) {
        throw new Error(`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be less than 64 characters long.`);
    }
    if (threadId.length < 32) {
        throw new Error(`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be at least 32 characters long.`);
    }
    const validThreadIdCharacters = /^[a-zA-Z0-9]+$/;
    if (!validThreadIdCharacters.test(threadId.substring(5))) {
        throw new Error(`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must contain only characters that match the regular expression [a-zA-Z0-9].`);
    }
}
/**
 * Determines if the connection is secure (HTTPS) by checking the request protocol
 * and x-forwarded-proto header (for reverse proxy scenarios).
 * Defaults to false (HTTP) if unable to determine.
 */
export function isSecureConnection(ctx) {
    // Check x-forwarded-proto header first (reverse proxy)
    const forwardedProto = ctx.req.header('x-forwarded-proto');
    if (forwardedProto) {
        return forwardedProto === 'https';
    }
    // Check the request URL protocol if available
    try {
        if (ctx.req.url) {
            const url = new URL(ctx.req.url);
            return url.protocol === 'https:';
        }
    }
    catch {
        // Fall through to default
    }
    // Default to HTTP (e.g., for localhost development)
    return false;
}
/**
 * Signs a thread ID using HMAC SHA-256 and returns it in the format: threadId;signature
 * Format: thrd_abc123;base64signature
 */
export async function signThreadId(threadId, secret) {
    const hasher = new Bun.CryptoHasher('sha256', secret);
    hasher.update(threadId);
    const signatureBase64 = hasher.digest('base64');
    return `${threadId};${signatureBase64}`;
}
/**
 * Verifies a signed thread ID header and returns the thread ID if valid, or undefined if invalid.
 * Expected format: thrd_abc123;base64signature
 */
export async function verifySignedThreadId(signedValue, secret) {
    const parts = signedValue.split(';');
    if (parts.length !== 2) {
        return undefined;
    }
    const [threadId, providedSignature] = parts;
    // Validate both parts exist
    if (!threadId || !providedSignature) {
        return undefined;
    }
    // Validate thread ID format before verifying signature
    if (!isValidThreadId(threadId)) {
        return undefined;
    }
    // Re-sign the thread ID and compare signatures
    const expectedSigned = await signThreadId(threadId, secret);
    const expectedSignature = expectedSigned.split(';')[1];
    // Validate signature exists
    if (!expectedSignature) {
        return undefined;
    }
    // Constant-time comparison to prevent timing attacks
    // Check lengths match first (fail fast if different lengths)
    if (providedSignature.length !== expectedSignature.length) {
        return undefined;
    }
    try {
        // Convert to Buffers for constant-time comparison
        const providedBuffer = Buffer.from(providedSignature, 'base64');
        const expectedBuffer = Buffer.from(expectedSignature, 'base64');
        if (timingSafeEqual(providedBuffer, expectedBuffer)) {
            return threadId;
        }
    }
    catch {
        // Comparison failed or buffer conversion error
        return undefined;
    }
    return undefined;
}
/**
 * DefaultThreadIDProvider will look for an HTTP header `x-thread-id` first,
 * then fall back to a signed cookie named `atid`, and use that as the thread id.
 * If not found, generate a new one. Validates incoming thread IDs against
 * runtime constraints. Uses AGENTUITY_SDK_KEY for signing, falls back to 'agentuity'.
 */
export class DefaultThreadIDProvider {
    getSecret() {
        return process.env.AGENTUITY_SDK_KEY || 'agentuity';
    }
    async getThreadId(_appState, ctx) {
        let threadId;
        const secret = this.getSecret();
        // Check signed header first
        const headerValue = ctx.req.header('x-thread-id');
        if (headerValue) {
            const verifiedThreadId = await verifySignedThreadId(headerValue, secret);
            if (verifiedThreadId) {
                threadId = verifiedThreadId;
            }
        }
        // Fall back to signed cookie
        if (!threadId) {
            const cookieValue = await getSignedCookie(ctx, secret, 'atid');
            if (cookieValue && typeof cookieValue === 'string' && isValidThreadId(cookieValue)) {
                threadId = cookieValue;
            }
        }
        threadId = threadId || generateId('thrd');
        await setSignedCookie(ctx, 'atid', threadId, secret, {
            httpOnly: true,
            secure: isSecureConnection(ctx),
            sameSite: 'Lax',
            path: '/',
            maxAge: 604800, // 1 week in seconds
        });
        // Set signed header in response
        const signedHeader = await signThreadId(threadId, secret);
        ctx.header('x-thread-id', signedHeader);
        return threadId;
    }
}
export class LazyThreadState {
    #status = 'idle';
    #state = new Map();
    #pendingOperations = [];
    #initialStateJson;
    #restoreFn;
    #loadingPromise = null;
    constructor(restoreFn) {
        this.#restoreFn = restoreFn;
    }
    get loaded() {
        return this.#status === 'loaded';
    }
    get dirty() {
        if (this.#status === 'pending-writes') {
            return this.#pendingOperations.length > 0;
        }
        if (this.#status === 'loaded') {
            const currentJson = JSON.stringify(Object.fromEntries(this.#state));
            return currentJson !== this.#initialStateJson;
        }
        return false;
    }
    async ensureLoaded() {
        if (this.#status === 'loaded') {
            return;
        }
        if (this.#loadingPromise) {
            await this.#loadingPromise;
            return;
        }
        this.#loadingPromise = (async () => {
            try {
                await this.doLoad();
            }
            finally {
                this.#loadingPromise = null;
            }
        })();
        await this.#loadingPromise;
    }
    async doLoad() {
        const { state } = await this.#restoreFn();
        // Initialize state from restored data
        this.#state = new Map(state);
        this.#initialStateJson = JSON.stringify(Object.fromEntries(this.#state));
        // Apply any pending operations
        for (const op of this.#pendingOperations) {
            switch (op.op) {
                case 'clear':
                    this.#state.clear();
                    break;
                case 'set':
                    if (op.key !== undefined) {
                        this.#state.set(op.key, op.value);
                    }
                    break;
                case 'delete':
                    if (op.key !== undefined) {
                        this.#state.delete(op.key);
                    }
                    break;
                case 'push':
                    if (op.key !== undefined) {
                        const existing = this.#state.get(op.key);
                        if (Array.isArray(existing)) {
                            existing.push(op.value);
                            // Apply maxRecords limit
                            if (op.maxRecords !== undefined && existing.length > op.maxRecords) {
                                existing.splice(0, existing.length - op.maxRecords);
                            }
                        }
                        else if (existing === undefined) {
                            this.#state.set(op.key, [op.value]);
                        }
                        // If existing is non-array, silently skip (error would have been thrown if loaded)
                    }
                    break;
            }
        }
        this.#pendingOperations = [];
        this.#status = 'loaded';
    }
    async get(key) {
        await this.ensureLoaded();
        return this.#state.get(key);
    }
    async set(key, value) {
        if (this.#status === 'loaded') {
            this.#state.set(key, value);
        }
        else {
            this.#pendingOperations.push({ op: 'set', key, value });
            if (this.#status === 'idle') {
                this.#status = 'pending-writes';
            }
        }
    }
    async has(key) {
        await this.ensureLoaded();
        return this.#state.has(key);
    }
    async delete(key) {
        if (this.#status === 'loaded') {
            this.#state.delete(key);
        }
        else {
            this.#pendingOperations.push({ op: 'delete', key });
            if (this.#status === 'idle') {
                this.#status = 'pending-writes';
            }
        }
    }
    async clear() {
        if (this.#status === 'loaded') {
            this.#state.clear();
        }
        else {
            // Clear replaces all previous pending operations
            this.#pendingOperations = [{ op: 'clear' }];
            if (this.#status === 'idle') {
                this.#status = 'pending-writes';
            }
        }
    }
    async entries() {
        await this.ensureLoaded();
        return Array.from(this.#state.entries());
    }
    async keys() {
        await this.ensureLoaded();
        return Array.from(this.#state.keys());
    }
    async values() {
        await this.ensureLoaded();
        return Array.from(this.#state.values());
    }
    async size() {
        await this.ensureLoaded();
        return this.#state.size;
    }
    async push(key, value, maxRecords) {
        if (this.#status === 'loaded') {
            // When loaded, push to local array
            const existing = this.#state.get(key);
            if (Array.isArray(existing)) {
                existing.push(value);
                // Apply maxRecords limit
                if (maxRecords !== undefined && existing.length > maxRecords) {
                    existing.splice(0, existing.length - maxRecords);
                }
            }
            else if (existing === undefined) {
                this.#state.set(key, [value]);
            }
            else {
                throw new Error(`Cannot push to non-array value at key "${key}"`);
            }
        }
        else {
            // Queue push operation for merge
            const op = { op: 'push', key, value };
            if (maxRecords !== undefined) {
                op.maxRecords = maxRecords;
            }
            this.#pendingOperations.push(op);
            if (this.#status === 'idle') {
                this.#status = 'pending-writes';
            }
        }
    }
    /**
     * Get the current status for save logic
     * @internal
     */
    getStatus() {
        return this.#status;
    }
    /**
     * Get pending operations for merge command
     * @internal
     */
    getPendingOperations() {
        return [...this.#pendingOperations];
    }
    /**
     * Get serialized state for full save.
     * Ensures state is loaded before serializing.
     * @internal
     */
    async getSerializedState() {
        await this.ensureLoaded();
        return Object.fromEntries(this.#state);
    }
}
export class DefaultThread {
    id;
    state;
    #metadata = null;
    #metadataDirty = false;
    #metadataLoadPromise = null;
    provider;
    #restoreFn;
    #restoredMetadata;
    constructor(provider, id, restoreFn, initialMetadata) {
        this.provider = provider;
        this.id = id;
        this.#restoreFn = restoreFn;
        this.#restoredMetadata = initialMetadata;
        this.state = new LazyThreadState(restoreFn);
    }
    async ensureMetadataLoaded() {
        if (this.#metadata !== null) {
            return;
        }
        // If we have initial metadata from thread creation, use it
        if (this.#restoredMetadata !== undefined) {
            this.#metadata = this.#restoredMetadata;
            return;
        }
        if (this.#metadataLoadPromise) {
            await this.#metadataLoadPromise;
            return;
        }
        this.#metadataLoadPromise = (async () => {
            try {
                await this.doLoadMetadata();
            }
            finally {
                this.#metadataLoadPromise = null;
            }
        })();
        await this.#metadataLoadPromise;
    }
    async doLoadMetadata() {
        const { metadata } = await this.#restoreFn();
        this.#metadata = metadata;
    }
    async getMetadata() {
        await this.ensureMetadataLoaded();
        return { ...this.#metadata };
    }
    async setMetadata(metadata) {
        this.#metadata = metadata;
        this.#metadataDirty = true;
    }
    addEventListener(eventName, callback) {
        let listeners = threadEventListeners.get(this);
        if (!listeners) {
            listeners = new Map();
            threadEventListeners.set(this, listeners);
        }
        let callbacks = listeners.get(eventName);
        if (!callbacks) {
            callbacks = new Set();
            listeners.set(eventName, callbacks);
        }
        callbacks.add(callback);
    }
    removeEventListener(eventName, callback) {
        const listeners = threadEventListeners.get(this);
        if (!listeners)
            return;
        const callbacks = listeners.get(eventName);
        if (!callbacks)
            return;
        callbacks.delete(callback);
    }
    async fireEvent(eventName) {
        await fireThreadEvent(this, eventName);
    }
    async destroy() {
        await this.provider.destroy(this);
    }
    /**
     * Check if thread has any data (state or metadata)
     */
    async empty() {
        const stateSize = await this.state.size();
        // Check both loaded metadata and initial metadata from constructor
        const meta = this.#metadata ?? this.#restoredMetadata ?? {};
        return stateSize === 0 && Object.keys(meta).length === 0;
    }
    /**
     * Check if thread needs saving
     * @internal
     */
    needsSave() {
        return this.state.dirty || this.#metadataDirty;
    }
    /**
     * Get the save mode for this thread
     * @internal
     */
    getSaveMode() {
        const stateStatus = this.state.getStatus();
        if (stateStatus === 'idle' && !this.#metadataDirty) {
            return 'none';
        }
        if (stateStatus === 'pending-writes') {
            return 'merge';
        }
        if (stateStatus === 'loaded' && (this.state.dirty || this.#metadataDirty)) {
            return 'full';
        }
        // Only metadata was changed without loading state
        if (this.#metadataDirty) {
            return 'merge';
        }
        return 'none';
    }
    /**
     * Get pending operations for merge command
     * @internal
     */
    getPendingOperations() {
        return this.state.getPendingOperations();
    }
    /**
     * Get metadata for saving (returns null if not loaded/modified)
     * @internal
     */
    getMetadataForSave() {
        if (this.#metadataDirty && this.#metadata) {
            return this.#metadata;
        }
        return undefined;
    }
    /**
     * Get serialized state for full save.
     * Ensures state is loaded before serializing.
     * @internal
     */
    async getSerializedState() {
        const state = await this.state.getSerializedState();
        // Also ensure metadata is loaded
        const meta = this.#metadata ?? this.#restoredMetadata ?? {};
        const hasState = Object.keys(state).length > 0;
        const hasMetadata = Object.keys(meta).length > 0;
        if (!hasState && !hasMetadata) {
            return '';
        }
        const data = {};
        if (hasState) {
            data.state = state;
        }
        if (hasMetadata) {
            data.metadata = meta;
        }
        return JSON.stringify(data);
    }
}
export class DefaultSession {
    id;
    thread;
    state;
    metadata;
    constructor(thread, id, metadata) {
        this.id = id;
        this.thread = thread;
        this.state = new Map();
        this.metadata = metadata || {};
    }
    addEventListener(eventName, callback) {
        let listeners = sessionEventListeners.get(this);
        if (!listeners) {
            listeners = new Map();
            sessionEventListeners.set(this, listeners);
        }
        let callbacks = listeners.get(eventName);
        if (!callbacks) {
            callbacks = new Set();
            listeners.set(eventName, callbacks);
        }
        callbacks.add(callback);
    }
    removeEventListener(eventName, callback) {
        const listeners = sessionEventListeners.get(this);
        if (!listeners)
            return;
        const callbacks = listeners.get(eventName);
        if (!callbacks)
            return;
        callbacks.delete(callback);
    }
    async fireEvent(eventName) {
        await fireSessionEvent(this, eventName);
    }
    /**
     * Serialize session state to JSON string for persistence.
     * Returns undefined if state is empty or exceeds 1MB limit.
     * @internal
     */
    serializeUserData() {
        if (this.state.size === 0) {
            return undefined;
        }
        try {
            const obj = Object.fromEntries(this.state);
            const json = JSON.stringify(obj);
            // Check 1MB limit (1,048,576 bytes)
            const sizeInBytes = new TextEncoder().encode(json).length;
            if (sizeInBytes > 1048576) {
                console.error(`Session ${this.id} user_data exceeds 1MB limit (${sizeInBytes} bytes), data will not be persisted`);
                return undefined;
            }
            return json;
        }
        catch (err) {
            console.error(`Failed to serialize session ${this.id} user_data:`, err);
            return undefined;
        }
    }
}
export class ThreadWebSocketClient {
    ws = null;
    authenticated = false;
    pendingRequests = new Map();
    reconnectAttempts = 0;
    maxReconnectAttempts;
    apiKey;
    wsUrl;
    wsConnecting = null;
    reconnectTimer = null;
    isDisposed = false;
    initialConnectResolve = null;
    initialConnectReject = null;
    connectionTimeoutMs;
    requestTimeoutMs;
    reconnectBaseDelayMs;
    reconnectMaxDelayMs;
    constructor(apiKey, wsUrl, options = {}) {
        this.apiKey = apiKey;
        this.wsUrl = wsUrl;
        this.connectionTimeoutMs = options.connectionTimeoutMs ?? 10_000;
        this.requestTimeoutMs = options.requestTimeoutMs ?? 10_000;
        this.reconnectBaseDelayMs = options.reconnectBaseDelayMs ?? 1_000;
        this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? 30_000;
        this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
    }
    async connect() {
        return new Promise((resolve, reject) => {
            // Store the initial connect promise callbacks if this is the first attempt
            if (this.reconnectAttempts === 0) {
                this.initialConnectResolve = resolve;
                this.initialConnectReject = reject;
            }
            // Set connection timeout
            const connectionTimeout = setTimeout(() => {
                this.cleanup();
                const rejectFn = this.initialConnectReject || reject;
                this.initialConnectResolve = null;
                this.initialConnectReject = null;
                rejectFn(new Error(`WebSocket connection timeout (${this.connectionTimeoutMs}ms)`));
            }, this.connectionTimeoutMs);
            try {
                this.ws = new WebSocket(this.wsUrl);
                this.ws.addEventListener('open', () => {
                    internal.info('WebSocket connected');
                    // Send authentication (do NOT clear timeout yet - wait for auth response)
                    this.ws?.send(JSON.stringify({ authorization: this.apiKey }));
                });
                this.ws.addEventListener('message', (event) => {
                    try {
                        const message = JSON.parse(event.data);
                        // Handle auth response
                        if ('success' in message && !this.authenticated) {
                            clearTimeout(connectionTimeout);
                            if (message.success) {
                                this.authenticated = true;
                                this.reconnectAttempts = 0;
                                // Resolve both the current promise and the initial connect promise
                                const resolveFn = this.initialConnectResolve || resolve;
                                this.initialConnectResolve = null;
                                this.initialConnectReject = null;
                                resolveFn();
                            }
                            else {
                                const err = new Error(`WebSocket authentication failed: ${message.error || 'Unknown error'}`);
                                this.cleanup();
                                const rejectFn = this.initialConnectReject || reject;
                                this.initialConnectResolve = null;
                                this.initialConnectReject = null;
                                rejectFn(err);
                            }
                            return;
                        }
                        // Handle action response
                        if ('id' in message && this.pendingRequests.has(message.id)) {
                            const pending = this.pendingRequests.get(message.id);
                            this.pendingRequests.delete(message.id);
                            if (message.success) {
                                pending.resolve(message.data);
                            }
                            else {
                                pending.reject(new Error(message.error || 'Request failed'));
                            }
                        }
                    }
                    catch {
                        // Ignore parse errors
                    }
                });
                this.ws.addEventListener('error', (_event) => {
                    clearTimeout(connectionTimeout);
                    if (!this.authenticated) {
                        // Don't reject immediately if we'll attempt reconnection
                        if (this.reconnectAttempts >= this.maxReconnectAttempts || this.isDisposed) {
                            const rejectFn = this.initialConnectReject || reject;
                            this.initialConnectResolve = null;
                            this.initialConnectReject = null;
                            rejectFn(new Error(`WebSocket error`));
                        }
                    }
                });
                this.ws.addEventListener('close', () => {
                    clearTimeout(connectionTimeout);
                    const wasAuthenticated = this.authenticated;
                    this.authenticated = false;
                    // Reject all pending requests
                    for (const [id, pending] of this.pendingRequests) {
                        pending.reject(new Error('WebSocket connection closed'));
                        this.pendingRequests.delete(id);
                    }
                    // Don't attempt reconnection if disposed
                    if (this.isDisposed) {
                        // Reject initial connect if still pending
                        if (!wasAuthenticated && this.initialConnectReject) {
                            this.initialConnectReject(new Error('WebSocket closed before authentication'));
                            this.initialConnectResolve = null;
                            this.initialConnectReject = null;
                        }
                        return;
                    }
                    // Attempt reconnection if within retry limits (even if auth didn't complete)
                    // This handles server rollouts where connection closes before auth finishes
                    if (this.reconnectAttempts < this.maxReconnectAttempts) {
                        this.reconnectAttempts++;
                        const delay = Math.min(this.reconnectBaseDelayMs * Math.pow(2, this.reconnectAttempts), this.reconnectMaxDelayMs);
                        internal.info(`WebSocket disconnected, attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
                        // Schedule reconnection with backoff delay
                        this.reconnectTimer = setTimeout(() => {
                            this.reconnectTimer = null;
                            // Create new connection promise for reconnection
                            this.wsConnecting = this.connect().catch(() => {
                                // Reconnection failed, reset
                                this.wsConnecting = null;
                            });
                        }, delay);
                    }
                    else {
                        internal.error(`WebSocket disconnected after ${this.reconnectAttempts} attempts, giving up`);
                        // Reject initial connect if still pending (all attempts exhausted)
                        if (!wasAuthenticated && this.initialConnectReject) {
                            this.initialConnectReject(new Error(`WebSocket closed before authentication after ${this.reconnectAttempts} attempts`));
                            this.initialConnectResolve = null;
                            this.initialConnectReject = null;
                        }
                    }
                });
            }
            catch (err) {
                clearTimeout(connectionTimeout);
                const rejectFn = this.initialConnectReject || reject;
                this.initialConnectResolve = null;
                this.initialConnectReject = null;
                rejectFn(err);
            }
        });
    }
    async restore(threadId) {
        // Wait for connection/reconnection if in progress
        if (this.wsConnecting) {
            await this.wsConnecting;
        }
        if (!this.authenticated || !this.ws) {
            throw new Error('WebSocket not connected or authenticated');
        }
        return new Promise((resolve, reject) => {
            const requestId = crypto.randomUUID();
            this.pendingRequests.set(requestId, { resolve, reject });
            const message = {
                id: requestId,
                action: 'restore',
                data: { thread_id: threadId },
            };
            this.ws.send(JSON.stringify(message));
            // Timeout after configured duration
            setTimeout(() => {
                if (this.pendingRequests.has(requestId)) {
                    this.pendingRequests.delete(requestId);
                    reject(new Error('Request timeout'));
                }
            }, this.requestTimeoutMs);
        });
    }
    async save(threadId, userData, threadMetadata) {
        // Wait for connection/reconnection if in progress
        if (this.wsConnecting) {
            await this.wsConnecting;
        }
        if (!this.authenticated || !this.ws) {
            throw new Error('WebSocket not connected or authenticated');
        }
        // Check 1MB limit
        const sizeInBytes = new TextEncoder().encode(userData).length;
        if (sizeInBytes > 1048576) {
            console.error(`Thread ${threadId} user_data exceeds 1MB limit (${sizeInBytes} bytes), data will not be persisted`);
            return;
        }
        return new Promise((resolve, reject) => {
            const requestId = crypto.randomUUID();
            this.pendingRequests.set(requestId, {
                resolve: () => resolve(),
                reject,
            });
            const data = {
                thread_id: threadId,
                user_data: userData,
            };
            if (threadMetadata && Object.keys(threadMetadata).length > 0) {
                data.metadata = threadMetadata;
            }
            const message = {
                id: requestId,
                action: 'save',
                data,
            };
            this.ws.send(JSON.stringify(message));
            // Timeout after configured duration
            setTimeout(() => {
                if (this.pendingRequests.has(requestId)) {
                    this.pendingRequests.delete(requestId);
                    reject(new Error('Request timeout'));
                }
            }, this.requestTimeoutMs);
        });
    }
    async delete(threadId) {
        // Wait for connection/reconnection if in progress
        if (this.wsConnecting) {
            await this.wsConnecting;
        }
        if (!this.authenticated || !this.ws) {
            throw new Error('WebSocket not connected or authenticated');
        }
        return new Promise((resolve, reject) => {
            const requestId = crypto.randomUUID();
            this.pendingRequests.set(requestId, {
                resolve: () => resolve(),
                reject,
            });
            const message = {
                id: requestId,
                action: 'delete',
                data: { thread_id: threadId },
            };
            this.ws.send(JSON.stringify(message));
            // Timeout after configured duration
            setTimeout(() => {
                if (this.pendingRequests.has(requestId)) {
                    this.pendingRequests.delete(requestId);
                    reject(new Error('Request timeout'));
                }
            }, this.requestTimeoutMs);
        });
    }
    async merge(threadId, operations, metadata) {
        // Wait for connection/reconnection if in progress
        if (this.wsConnecting) {
            await this.wsConnecting;
        }
        if (!this.authenticated || !this.ws) {
            throw new Error('WebSocket not connected or authenticated');
        }
        return new Promise((resolve, reject) => {
            const requestId = crypto.randomUUID();
            this.pendingRequests.set(requestId, {
                resolve: () => resolve(),
                reject,
            });
            const data = {
                thread_id: threadId,
                operations,
            };
            if (metadata && Object.keys(metadata).length > 0) {
                data.metadata = metadata;
            }
            const message = {
                id: requestId,
                action: 'merge',
                data,
            };
            this.ws.send(JSON.stringify(message));
            // Timeout after configured duration
            setTimeout(() => {
                if (this.pendingRequests.has(requestId)) {
                    this.pendingRequests.delete(requestId);
                    reject(new Error('Request timeout'));
                }
            }, this.requestTimeoutMs);
        });
    }
    cleanup() {
        // Mark as disposed to prevent new reconnection attempts
        this.isDisposed = true;
        // Cancel any pending reconnection timer
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }
        if (this.ws) {
            this.ws.close();
            this.ws = null;
        }
        this.authenticated = false;
        this.pendingRequests.clear();
        this.reconnectAttempts = 0;
        this.wsConnecting = null;
        this.initialConnectResolve = null;
        this.initialConnectReject = null;
    }
}
export class DefaultThreadProvider {
    appState = null;
    wsClient = null;
    wsConnecting = null;
    threadIDProvider = null;
    async initialize(appState) {
        this.appState = appState;
        this.threadIDProvider = new DefaultThreadIDProvider();
        // Initialize WebSocket connection for thread persistence (async, non-blocking)
        const apiKey = process.env.AGENTUITY_SDK_KEY;
        if (apiKey) {
            const serviceUrls = getServiceUrls(process.env.AGENTUITY_REGION ?? 'usc');
            const catalystUrl = serviceUrls.catalyst;
            const wsUrl = new URL('/thread/ws', catalystUrl.replace(/^http/, 'ws'));
            internal.debug('connecting to %s', wsUrl);
            this.wsClient = new ThreadWebSocketClient(apiKey, wsUrl.toString());
            // Connect in background, don't block initialization
            this.wsConnecting = this.wsClient
                .connect()
                .then(() => {
                this.wsConnecting = null;
            })
                .catch((err) => {
                internal.error('Failed to connect to thread WebSocket:', err);
                this.wsClient = null;
                this.wsConnecting = null;
            });
        }
    }
    setThreadIDProvider(provider) {
        this.threadIDProvider = provider;
    }
    async restore(ctx) {
        const threadId = await this.threadIDProvider.getThreadId(this.appState, ctx);
        validateThreadIdOrThrow(threadId);
        internal.info('[thread] creating lazy thread %s (no eager restore)', threadId);
        // Create a restore function that will be called lazily when state/metadata is accessed
        const restoreFn = async () => {
            internal.info('[thread] lazy loading state for thread %s', threadId);
            // Wait for WebSocket connection if still connecting
            if (this.wsConnecting) {
                internal.info('[thread] waiting for WebSocket connection');
                await this.wsConnecting;
            }
            if (!this.wsClient) {
                internal.info('[thread] no WebSocket client available, returning empty state');
                return { state: new Map(), metadata: {} };
            }
            try {
                const restoredData = await this.wsClient.restore(threadId);
                if (restoredData) {
                    internal.info('[thread] restored state: %d bytes', restoredData.length);
                    const { flatStateJson, metadata } = parseThreadData(restoredData);
                    const state = new Map();
                    if (flatStateJson) {
                        try {
                            const data = JSON.parse(flatStateJson);
                            for (const [key, value] of Object.entries(data)) {
                                state.set(key, value);
                            }
                        }
                        catch {
                            internal.info('[thread] failed to parse state JSON');
                        }
                    }
                    return { state, metadata: metadata || {} };
                }
                internal.info('[thread] no existing state found');
                return { state: new Map(), metadata: {} };
            }
            catch (err) {
                internal.info('[thread] WebSocket restore failed: %s', err);
                return { state: new Map(), metadata: {} };
            }
        };
        const thread = new DefaultThread(this, threadId, restoreFn);
        await fireEvent('thread.created', thread);
        return thread;
    }
    async save(thread) {
        if (thread instanceof DefaultThread) {
            const saveMode = thread.getSaveMode();
            internal.info('[thread] DefaultThreadProvider.save() - thread %s, saveMode: %s, hasWsClient: %s', thread.id, saveMode, !!this.wsClient);
            if (saveMode === 'none') {
                internal.info('[thread] skipping save - no changes');
                return;
            }
            // Wait for WebSocket connection if still connecting
            if (this.wsConnecting) {
                internal.info('[thread] waiting for WebSocket connection');
                await this.wsConnecting;
            }
            if (!this.wsClient) {
                internal.info('[thread] no WebSocket client available, skipping save');
                return;
            }
            try {
                if (saveMode === 'merge') {
                    const operations = thread.getPendingOperations();
                    const metadata = thread.getMetadataForSave();
                    internal.info('[thread] sending merge command with %d operations', operations.length);
                    await this.wsClient.merge(thread.id, operations, metadata);
                    internal.info('[thread] WebSocket merge completed');
                }
                else if (saveMode === 'full') {
                    const serialized = await thread.getSerializedState();
                    internal.info('[thread] saving to WebSocket, serialized length: %d', serialized.length);
                    const metadata = thread.getMetadataForSave();
                    await this.wsClient.save(thread.id, serialized, metadata);
                    internal.info('[thread] WebSocket save completed');
                }
            }
            catch (err) {
                internal.info('[thread] WebSocket save/merge failed: %s', err);
                // Don't throw - allow request to complete even if save fails
            }
        }
    }
    async destroy(thread) {
        if (thread instanceof DefaultThread) {
            try {
                // Wait for WebSocket connection if still connecting
                if (this.wsConnecting) {
                    await this.wsConnecting;
                }
                // Delete thread from remote storage
                if (this.wsClient) {
                    try {
                        await this.wsClient.delete(thread.id);
                    }
                    catch {
                        // Thread might not exist in remote storage if it was never persisted
                        // This is normal for ephemeral threads, so just log at debug level
                        internal.debug(`Thread ${thread.id} not found in remote storage (already deleted or never persisted)`);
                        // Continue with local cleanup even if remote delete fails
                    }
                }
                await thread.fireEvent('destroyed');
                await fireEvent('thread.destroyed', thread);
            }
            finally {
                threadEventListeners.delete(thread);
            }
        }
    }
}
export class DefaultSessionProvider {
    sessions = new Map();
    async initialize(_appState) {
        // No initialization needed for in-memory provider
    }
    async restore(thread, sessionId) {
        internal.info('[session] restoring session %s for thread %s', sessionId, thread.id);
        let session = this.sessions.get(sessionId);
        if (!session) {
            session = new DefaultSession(thread, sessionId);
            this.sessions.set(sessionId, session);
            internal.info('[session] created new session, firing session.started');
            await fireEvent('session.started', session);
        }
        else {
            internal.info('[session] found existing session');
        }
        return session;
    }
    async save(session) {
        if (session instanceof DefaultSession) {
            internal.info('[session] DefaultSessionProvider.save() - firing completed event for session %s', session.id);
            try {
                await session.fireEvent('completed');
                internal.info('[session] session.fireEvent completed, firing app event');
                await fireEvent('session.completed', session);
                internal.info('[session] session.completed app event fired');
            }
            finally {
                this.sessions.delete(session.id);
                sessionEventListeners.delete(session);
            }
        }
    }
}
//# sourceMappingURL=session.js.map