/**
 * HTTP/WebSocket Client Library
 *
 * Core client for accessing the NeuroLink API with built-in authentication,
 * retry logic, and middleware support. Supports both browser and Node.js environments.
 *
 * @module @neurolink/client
 */
import { HttpError, ClientNetworkError, ClientTimeoutError } from "./errors.js";
import { logger } from "../utils/logger.js";
import { tracers } from "../telemetry/tracers.js";
import { withClientSpan } from "../telemetry/withSpan.js";
// =============================================================================
// Shared Utilities
// =============================================================================
/**
 * Combine multiple AbortSignals into a single signal.
 * Aborts as soon as any input signal is aborted.
 */
export function combineSignals(...signals) {
    const controller = new AbortController();
    for (const signal of signals) {
        if (signal.aborted) {
            controller.abort();
            break;
        }
        signal.addEventListener("abort", () => controller.abort());
    }
    return controller.signal;
}
/**
 * Promise-based delay utility.
 */
export function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
// =============================================================================
// Constants
// =============================================================================
/**
 * Default request timeout in milliseconds (30 seconds)
 */
const DEFAULT_TIMEOUT = 30_000;
/**
 * Default retry configuration
 */
const DEFAULT_RETRY_CONFIG = {
    maxAttempts: 3,
    initialDelayMs: 1000,
    maxDelayMs: 10000,
    backoffMultiplier: 2,
    retryableStatusCodes: [408, 429, 500, 502, 503, 504],
    retryOnNetworkError: true,
};
// Re-export error classes so existing consumers importing from httpClient still work
export { HttpError as NeuroLinkApiError, ClientNetworkError, ClientTimeoutError, };
// =============================================================================
// HTTP Client Class
// =============================================================================
/**
 * HTTP Client for NeuroLink API
 *
 * Provides type-safe access to all NeuroLink API endpoints with
 * built-in authentication, retry logic, and middleware support.
 *
 * @example Basic usage
 * ```typescript
 * import { createClient } from '@neurolink/client';
 *
 * const client = createClient({
 *   baseUrl: 'https://api.neurolink.example.com',
 *   apiKey: 'your-api-key',
 * });
 *
 * const result = await client.generate({
 *   input: { text: 'Hello, world!' },
 *   provider: 'openai',
 * });
 * ```
 *
 * @example With middleware
 * ```typescript
 * const client = createClient({ baseUrl: 'https://api.example.com' });
 *
 * client.use(async (request, next) => {
 *   console.log('Request:', request.url);
 *   const response = await next();
 *   console.log('Response:', response.status);
 *   return response;
 * });
 * ```
 */
export class NeuroLinkClient {
    config;
    middlewares = [];
    wsConnection = null;
    wsState = "disconnected";
    wsMessageHandlers = new Map();
    constructor(config) {
        // Validate required config
        if (!config.baseUrl) {
            throw new Error("baseUrl is required in client configuration");
        }
        this.config = {
            baseUrl: config.baseUrl.replace(/\/$/, ""),
            apiKey: config.apiKey ?? "",
            token: config.token ?? "",
            timeout: config.timeout ?? DEFAULT_TIMEOUT,
            headers: config.headers ?? {},
            retry: { ...DEFAULT_RETRY_CONFIG, ...config.retry },
            debug: config.debug ?? false,
            fetch: config.fetch ?? this.getDefaultFetch(),
            wsUrl: config.wsUrl ??
                config.baseUrl.replace(/^http/, "ws").replace(/\/$/, ""),
        };
    }
    /**
     * Get default fetch implementation
     */
    getDefaultFetch() {
        if (typeof globalThis.fetch === "function") {
            return globalThis.fetch.bind(globalThis);
        }
        throw new Error("No fetch implementation available. Please provide a custom fetch function in the client configuration.");
    }
    // ===========================================================================
    // ClientMiddleware Methods
    // ===========================================================================
    /**
     * Add middleware to the client
     *
     * @param middleware - ClientMiddleware function
     * @returns Client instance for chaining
     */
    use(middleware) {
        this.middlewares.push(middleware);
        return this;
    }
    /**
     * Clear all middleware
     */
    clearMiddleware() {
        this.middlewares = [];
        return this;
    }
    // ===========================================================================
    // Internal Request Methods
    // ===========================================================================
    /**
     * Generate unique request ID
     */
    generateRequestId() {
        return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }
    /**
     * Calculate delay for exponential backoff
     */
    calculateDelay(attempt, config) {
        const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
        const jitter = Math.random() * 0.1 * delay;
        return Math.min(delay + jitter, config.maxDelayMs);
    }
    /**
     * Delay utility — delegates to shared sleep()
     */
    delay(ms) {
        return sleep(ms);
    }
    /**
     * Combine multiple abort signals — delegates to shared combineSignals()
     */
    combineSignals(...signals) {
        return combineSignals(...signals);
    }
    /**
     * Debug logging via the unified NeuroLink logger.
     * Guarded by `logger.shouldLog("debug")` so expensive serialization is
     * skipped when debug output is disabled.
     */
    log(message, data) {
        if (logger.shouldLog("debug")) {
            logger.debug(`[HttpClient] ${message}`, data);
        }
    }
    /**
     * Execute HTTP request with middleware and retry logic
     */
    async request(method, path, body, options) {
        return withClientSpan({
            name: "neurolink.client.http.request",
            tracer: tracers.http,
            attributes: {
                "http.method": method,
                "http.route": path,
                "http.url": `${this.config.baseUrl}${path}`,
            },
        }, async () => this._doRequest(method, path, body, options));
    }
    async _doRequest(method, path, body, options) {
        const url = `${this.config.baseUrl}${path}`;
        const requestId = this.generateRequestId();
        const { retry } = this.config;
        const skipRetry = options?.skipRetry ?? false;
        const context = {
            startTime: Date.now(),
            requestId,
            retryCount: 0,
        };
        const middlewareRequest = {
            url,
            method,
            headers: {
                "Content-Type": "application/json",
                Accept: "application/json",
                "X-Request-ID": requestId,
                ...(this.config.apiKey ? { "X-API-Key": this.config.apiKey } : {}),
                ...(this.config.token
                    ? { Authorization: `Bearer ${this.config.token}` }
                    : {}),
                ...this.config.headers,
                ...options?.headers,
            },
            body,
            context,
        };
        /**
         * Execute the actual HTTP request
         */
        const executeRequest = async () => {
            const controller = new AbortController();
            const timeoutMs = options?.timeout ?? this.config.timeout;
            const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
            try {
                const signal = options?.signal
                    ? this.combineSignals(options.signal, controller.signal)
                    : controller.signal;
                this.log(`${method} ${url}`);
                const response = await this.config.fetch(url, {
                    method,
                    headers: middlewareRequest.headers,
                    body: body ? JSON.stringify(body) : undefined,
                    signal,
                });
                clearTimeout(timeoutId);
                let responseBody;
                const contentType = response.headers.get("content-type") ?? "";
                if (contentType.includes("application/json")) {
                    responseBody = await response.json();
                }
                else {
                    responseBody = await response.text();
                }
                const headers = {};
                response.headers.forEach((value, key) => {
                    headers[key] = value;
                });
                this.log(`Response ${response.status}`, {
                    duration: Date.now() - context.startTime,
                });
                return {
                    status: response.status,
                    headers,
                    body: responseBody,
                    context,
                };
            }
            catch (error) {
                clearTimeout(timeoutId);
                if (error.name === "AbortError") {
                    throw new ClientTimeoutError(timeoutMs);
                }
                throw new ClientNetworkError(`Network request failed: ${error.message}`, { cause: error });
            }
        };
        /**
         * Execute request with retry logic
         */
        const executeWithRetry = async () => {
            let lastError;
            for (let attempt = 0; attempt < retry.maxAttempts; attempt++) {
                try {
                    const response = await executeRequest();
                    // Check if response status is retryable
                    if (!skipRetry &&
                        retry.retryableStatusCodes?.includes(response.status)) {
                        if (attempt < retry.maxAttempts - 1) {
                            const delayMs = this.calculateDelay(attempt, retry);
                            this.log(`Retrying after ${delayMs}ms (attempt ${attempt + 1})`);
                            await this.delay(delayMs);
                            context.retryCount = attempt + 1;
                            continue;
                        }
                    }
                    return response;
                }
                catch (error) {
                    lastError = error;
                    // Check if network error and should retry
                    if (!skipRetry &&
                        retry.retryOnNetworkError &&
                        error instanceof ClientNetworkError &&
                        attempt < retry.maxAttempts - 1) {
                        const delayMs = this.calculateDelay(attempt, retry);
                        this.log(`Retrying after network error (attempt ${attempt + 1})`);
                        await this.delay(delayMs);
                        context.retryCount = attempt + 1;
                        continue;
                    }
                    throw error;
                }
            }
            throw lastError ?? new Error("Max retries exceeded");
        };
        // Execute with middleware chain
        let index = 0;
        const next = async () => {
            if (index < this.middlewares.length) {
                const middleware = this.middlewares[index++];
                return middleware(middlewareRequest, next);
            }
            return executeWithRetry();
        };
        const response = await next();
        // Check for error responses
        if (response.status >= 400) {
            // Server may wrap errors as { error: { code, message }, metadata: {...} }
            const rawBody = response.body;
            const errorPayload = rawBody && typeof rawBody === "object" && "error" in rawBody
                ? rawBody.error
                : rawBody;
            throw new HttpError(errorPayload?.message ?? `HTTP ${response.status}`, response.status, {
                code: errorPayload?.code,
                details: errorPayload?.details,
                requestId,
            });
        }
        // Unwrap server response envelope if present
        // The NeuroLink server wraps responses as { data: <payload>, metadata: {...} }
        const responseBody = response.body;
        const unwrapped = responseBody &&
            typeof responseBody === "object" &&
            "data" in responseBody &&
            "metadata" in responseBody
            ? responseBody.data
            : response.body;
        return {
            data: unwrapped,
            status: response.status,
            headers: response.headers,
            duration: Date.now() - context.startTime,
            requestId,
        };
    }
    // ===========================================================================
    // Generation API
    // ===========================================================================
    /**
     * Generate text using AI models
     *
     * @example
     * ```typescript
     * const response = await client.generate({
     *   input: { text: 'Write a poem about coding' },
     *   provider: 'openai',
     *   model: 'gpt-4o',
     *   temperature: 0.7,
     * });
     * console.log(response.data.content);
     * ```
     */
    async generate(options, requestOptions) {
        return this.request("POST", "/api/agent/execute", options, requestOptions);
    }
    /**
     * Stream text generation
     *
     * @example
     * ```typescript
     * await client.stream(
     *   { input: { text: 'Tell me a story' }, provider: 'openai' },
     *   {
     *     onText: (text) => process.stdout.write(text),
     *     onDone: (result) => console.log('\nDone!', result.usage),
     *   }
     * );
     * ```
     */
    async stream(options, callbacks, requestOptions) {
        const url = `${this.config.baseUrl}/api/agent/stream`;
        const requestId = this.generateRequestId();
        const response = await this.config.fetch(url, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                Accept: "text/event-stream",
                "X-Request-ID": requestId,
                ...(this.config.apiKey ? { "X-API-Key": this.config.apiKey } : {}),
                ...(this.config.token
                    ? { Authorization: `Bearer ${this.config.token}` }
                    : {}),
                ...this.config.headers,
                ...requestOptions?.headers,
            },
            body: JSON.stringify(options),
            signal: requestOptions?.signal,
        });
        if (!response.ok) {
            const rawBody = (await response.json());
            // Server may wrap errors as { error: { code, message }, metadata: {...} }
            const errorPayload = rawBody && typeof rawBody === "object" && "error" in rawBody
                ? rawBody.error
                : rawBody;
            throw new HttpError(errorPayload?.message ?? `HTTP ${response.status}`, response.status, {
                code: errorPayload?.code,
                details: errorPayload?.details,
                requestId,
            });
        }
        const reader = response.body?.getReader();
        if (!reader) {
            throw new Error("Response body is not readable");
        }
        const decoder = new TextDecoder();
        let buffer = "";
        let fullContent = "";
        const toolCalls = [];
        const toolResults = [];
        try {
            while (true) {
                const { done, value } = await reader.read();
                if (done) {
                    break;
                }
                buffer += decoder.decode(value, { stream: true });
                const lines = buffer.split("\n");
                buffer = lines.pop() ?? "";
                for (const line of lines) {
                    if (line.startsWith("data: ")) {
                        const data = line.slice(6);
                        if (data === "[DONE]") {
                            const result = {
                                content: fullContent,
                                toolCalls: toolCalls,
                                toolResults: toolResults,
                            };
                            callbacks?.onDone?.(result);
                            return result;
                        }
                        try {
                            const event = JSON.parse(data);
                            this.handleStreamEvent(event, callbacks, {
                                onText: (text) => {
                                    fullContent += text;
                                },
                                onToolCall: (toolCall) => {
                                    toolCalls.push(toolCall);
                                },
                                onToolResult: (toolResult) => {
                                    toolResults.push(toolResult);
                                },
                            });
                        }
                        catch {
                            // Ignore parse errors for malformed events
                            this.log("Failed to parse stream event", data);
                        }
                    }
                }
            }
        }
        finally {
            reader.releaseLock();
        }
        return {
            content: fullContent,
            toolCalls: toolCalls,
            toolResults: toolResults,
        };
    }
    /**
     * Handle individual stream events
     */
    handleStreamEvent(event, callbacks, internalCallbacks) {
        switch (event.type) {
            case "text":
                if (event.content) {
                    callbacks?.onText?.(event.content);
                    internalCallbacks?.onText?.(event.content);
                }
                break;
            case "tool-call":
                if (event.toolCall) {
                    callbacks?.onToolCall?.(event.toolCall);
                    internalCallbacks?.onToolCall?.(event.toolCall);
                }
                break;
            case "tool-result":
                if (event.toolResult) {
                    callbacks?.onToolResult?.(event.toolResult);
                    internalCallbacks?.onToolResult?.(event.toolResult);
                }
                break;
            case "error":
                if (event.error) {
                    callbacks?.onError?.(event.error);
                }
                break;
            case "metadata":
                if (event.metadata) {
                    callbacks?.onMetadata?.(event.metadata);
                }
                break;
            case "audio":
                if (event.audio) {
                    callbacks?.onAudio?.(event.audio);
                }
                break;
            case "thinking":
                if (event.thinking) {
                    callbacks?.onThinking?.(event.thinking);
                }
                break;
        }
    }
    // ===========================================================================
    // Agent API
    // ===========================================================================
    /**
     * Execute an agent
     *
     * @example
     * ```typescript
     * const result = await client.executeAgent({
     *   agentId: 'customer-support',
     *   input: 'I need help with my order',
     *   sessionId: 'user-123',
     * });
     * console.log(result.data.content);
     * ```
     */
    async executeAgent(options, requestOptions) {
        return this.request("POST", `/api/agents/${options.agentId}/execute`, options, requestOptions);
    }
    /**
     * Stream agent execution
     */
    async streamAgent(options, callbacks, requestOptions) {
        return this.stream({
            input: { text: options.input },
            context: options.context,
            ...(options.tools ? { tools: options.tools.enabled } : {}),
        }, callbacks, requestOptions);
    }
    /**
     * List available agents
     */
    async listAgents(requestOptions) {
        return this.request("GET", "/api/agents", undefined, requestOptions);
    }
    /**
     * Get agent details
     */
    async getAgent(agentId, requestOptions) {
        return this.request("GET", `/api/agents/${agentId}`, undefined, requestOptions);
    }
    // ===========================================================================
    // Workflow API
    // ===========================================================================
    /**
     * Execute a workflow
     *
     * @example
     * ```typescript
     * const result = await client.executeWorkflow({
     *   workflowId: 'data-pipeline',
     *   input: { data: [...] },
     * });
     *
     * if (result.data.status === 'running') {
     *   // Poll for completion
     *   const status = await client.getWorkflowStatus(
     *     'data-pipeline',
     *     result.data.runId
     *   );
     * }
     * ```
     */
    async executeWorkflow(options, requestOptions) {
        return this.request("POST", `/api/workflows/${options.workflowId}/execute`, options, requestOptions);
    }
    /**
     * Resume a suspended workflow
     */
    async resumeWorkflow(workflowId, resumeToken, resumeData, requestOptions) {
        return this.request("POST", `/api/workflows/${workflowId}/resume`, { resumeToken, resumeData }, requestOptions);
    }
    /**
     * Get workflow execution status
     */
    async getWorkflowStatus(workflowId, runId, requestOptions) {
        return this.request("GET", `/api/workflows/${workflowId}/runs/${runId}`, undefined, requestOptions);
    }
    /**
     * Cancel workflow execution
     */
    async cancelWorkflow(workflowId, runId, requestOptions) {
        return this.request("POST", `/api/workflows/${workflowId}/runs/${runId}/cancel`, undefined, requestOptions);
    }
    /**
     * List available workflows
     */
    async listWorkflows(requestOptions) {
        return this.request("GET", "/api/workflows", undefined, requestOptions);
    }
    /**
     * Get workflow details
     */
    async getWorkflow(workflowId, requestOptions) {
        return this.request("GET", `/api/workflows/${workflowId}`, undefined, requestOptions);
    }
    // ===========================================================================
    // Tools API
    // ===========================================================================
    /**
     * List available tools
     *
     * @example
     * ```typescript
     * const tools = await client.listTools({ category: 'data' });
     * console.log(tools.data);
     * ```
     */
    async listTools(options, requestOptions) {
        const params = new URLSearchParams();
        if (options?.category) {
            params.set("category", options.category);
        }
        if (options?.serverId) {
            params.set("serverId", options.serverId);
        }
        const query = params.toString();
        return this.request("GET", `/api/tools${query ? `?${query}` : ""}`, undefined, requestOptions);
    }
    /**
     * Execute a tool
     */
    async executeTool(toolName, params, requestOptions) {
        return this.request("POST", `/api/tools/${toolName}/execute`, { params }, requestOptions);
    }
    /**
     * Get tool details
     */
    async getTool(toolName, requestOptions) {
        return this.request("GET", `/api/tools/${toolName}`, undefined, requestOptions);
    }
    // ===========================================================================
    // Provider API
    // ===========================================================================
    /**
     * List available providers
     */
    async listProviders(requestOptions) {
        return this.request("GET", "/api/providers", undefined, requestOptions);
    }
    /**
     * Get provider status
     */
    async getProviderStatus(providerName, requestOptions) {
        return this.request("GET", `/api/providers/${providerName}/status`, undefined, requestOptions);
    }
    // ===========================================================================
    // Health API
    // ===========================================================================
    /**
     * Health check
     */
    async health(requestOptions) {
        return this.request("GET", "/api/health", undefined, requestOptions);
    }
    // ===========================================================================
    // WebSocket Methods
    // ===========================================================================
    /**
     * Connect to WebSocket for real-time communication
     *
     * @example
     * ```typescript
     * client.connectWebSocket({
     *   url: 'wss://api.example.com/ws',
     *   autoReconnect: true,
     * });
     *
     * client.onWebSocketMessage('chat', (data) => {
     *   console.log('Chat message:', data);
     * });
     * ```
     */
    connectWebSocket(options) {
        if (typeof WebSocket === "undefined") {
            throw new Error("WebSocket is not available in this environment. Please use a polyfill.");
        }
        const wsUrl = options?.url ?? `${this.config.wsUrl}/ws`;
        const protocols = options?.protocols;
        this.wsState = "connecting";
        this.wsConnection = new WebSocket(wsUrl, protocols);
        this.wsConnection.onopen = () => {
            this.wsState = "connected";
            this.log("WebSocket connected");
            // Send auth if configured
            if (this.config.apiKey || this.config.token) {
                this.sendWebSocketMessage({
                    type: "auth",
                    apiKey: this.config.apiKey,
                    token: this.config.token,
                });
            }
        };
        this.wsConnection.onmessage = (event) => {
            try {
                const data = JSON.parse(event.data);
                const messageType = data.type ?? "message";
                const handlers = this.wsMessageHandlers.get(messageType);
                if (handlers) {
                    handlers.forEach((handler) => handler(data));
                }
                // Also notify general message handlers
                const allHandlers = this.wsMessageHandlers.get("*");
                if (allHandlers) {
                    allHandlers.forEach((handler) => handler(data));
                }
            }
            catch {
                this.log("Failed to parse WebSocket message", event.data);
            }
        };
        this.wsConnection.onclose = () => {
            this.wsState = "disconnected";
            this.log("WebSocket disconnected");
            if (options?.autoReconnect) {
                const interval = options.reconnectInterval ?? 5000;
                setTimeout(() => {
                    if (this.wsState === "disconnected") {
                        this.connectWebSocket(options);
                    }
                }, interval);
            }
        };
        this.wsConnection.onerror = (error) => {
            this.log("WebSocket error", error);
        };
    }
    /**
     * Disconnect WebSocket
     */
    disconnectWebSocket() {
        if (this.wsConnection) {
            this.wsState = "disconnecting";
            this.wsConnection.close();
            this.wsConnection = null;
        }
    }
    /**
     * Send message over WebSocket
     */
    sendWebSocketMessage(data) {
        if (this.wsConnection?.readyState === WebSocket.OPEN) {
            this.wsConnection.send(JSON.stringify(data));
        }
        else {
            throw new Error("WebSocket is not connected");
        }
    }
    /**
     * Register WebSocket message handler
     */
    onWebSocketMessage(messageType, handler) {
        if (!this.wsMessageHandlers.has(messageType)) {
            this.wsMessageHandlers.set(messageType, new Set());
        }
        const handlers = this.wsMessageHandlers.get(messageType);
        if (handlers) {
            handlers.add(handler);
        }
        // Return unsubscribe function
        return () => {
            this.wsMessageHandlers.get(messageType)?.delete(handler);
        };
    }
    /**
     * Get WebSocket connection state
     */
    getWebSocketState() {
        return this.wsState;
    }
    // ===========================================================================
    // Configuration Methods
    // ===========================================================================
    /**
     * Update client configuration
     */
    updateConfig(config) {
        if (config.baseUrl) {
            this.config.baseUrl = config.baseUrl.replace(/\/$/, "");
        }
        if (config.apiKey !== undefined) {
            this.config.apiKey = config.apiKey;
        }
        if (config.token !== undefined) {
            this.config.token = config.token;
        }
        if (config.timeout !== undefined) {
            this.config.timeout = config.timeout;
        }
        if (config.headers) {
            this.config.headers = { ...this.config.headers, ...config.headers };
        }
        if (config.retry) {
            this.config.retry = { ...this.config.retry, ...config.retry };
        }
        if (config.debug !== undefined) {
            this.config.debug = config.debug;
        }
    }
    /**
     * Get current configuration (readonly)
     */
    getConfig() {
        return { ...this.config };
    }
}
// =============================================================================
// Factory Function
// =============================================================================
/**
 * Create a new NeuroLink client instance
 *
 * @example
 * ```typescript
 * import { createClient } from '@neurolink/client';
 *
 * const client = createClient({
 *   baseUrl: 'https://api.neurolink.example.com',
 *   apiKey: process.env.NEUROLINK_API_KEY,
 *   debug: true,
 * });
 * ```
 */
export function createClient(config) {
    return new NeuroLinkClient(config);
}
