"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StagehandAPIClient = exports.REGION_API_URLS = void 0;
exports.getApiUrlForRegion = getApiUrlForRegion;
const fetch_cookie_1 = __importDefault(require("fetch-cookie"));
const utils_js_1 = require("../utils.js");
const version_js_1 = require("../version.js");
const index_js_1 = require("./types/public/index.js");
const zodCompat_js_1 = require("./zodCompat.js");
// =============================================================================
// Multi-region API URL mapping
// =============================================================================
/**
 * Mapping of Browserbase regions to their corresponding Stagehand API base URLs.
 * Users should configure their client to hit the API endpoint that matches
 * the region where their browser session is running.
 */
exports.REGION_API_URLS = {
    "us-west-2": "https://api.stagehand.browserbase.com",
    "us-east-1": "https://api.use1.stagehand.browserbase.com",
    "eu-central-1": "https://api.euc1.stagehand.browserbase.com",
    "ap-southeast-1": "https://api.apse1.stagehand.browserbase.com",
};
/**
 * Returns the full API URL (with /v1 suffix) for a given Browserbase region.
 * If no region is specified or the region is unknown, defaults to us-west-2.
 *
 * @param region - The Browserbase region (e.g., "us-west-2", "eu-central-1")
 * @returns The full API URL including /v1 suffix
 */
function getApiUrlForRegion(region) {
    const baseUrl = exports.REGION_API_URLS[region] ??
        exports.REGION_API_URLS["us-west-2"];
    return `${baseUrl}/v1`;
}
class StagehandAPIClient {
    apiKey;
    projectId;
    sessionId;
    modelApiKey;
    modelProvider;
    region;
    logger;
    fetchWithCookies;
    serverCache;
    lastFinishedEventData = null;
    latestAgentCacheEntry = null;
    constructor({ apiKey, projectId, logger, serverCache, }) {
        this.apiKey = apiKey;
        this.projectId = projectId;
        this.logger = logger;
        this.serverCache = serverCache ?? true;
        // Create a single cookie jar instance that will persist across all requests
        this.fetchWithCookies = (0, fetch_cookie_1.default)(fetch);
    }
    async init({ modelName, modelApiKey, domSettleTimeoutMs, verbose, systemPrompt, selfHeal, browserbaseSessionCreateParams, browserbaseSessionID,
    // browser,  TODO for local browsers
     }) {
        if (!modelApiKey) {
            throw new index_js_1.StagehandAPIError("modelApiKey is required");
        }
        this.modelApiKey = modelApiKey;
        // Extract provider from modelName (e.g., "openai/gpt-5-nano" -> "openai")
        this.modelProvider = modelName?.includes("/")
            ? modelName.split("/")[0]
            : undefined;
        // Store the region for multi-region API URL resolution
        this.region = browserbaseSessionCreateParams?.region;
        this.logger({
            category: "init",
            message: "Creating new browserbase session...",
            level: 1,
        });
        // Build wire-format request body (Api.SessionStartRequest shape)
        const requestBody = {
            modelName,
            domSettleTimeoutMs,
            verbose,
            systemPrompt,
            selfHeal,
            browserbaseSessionCreateParams,
            browserbaseSessionID,
            // browser, TODO: only send when connected to local fastify
        };
        const sessionResponse = await this.request("/sessions/start", {
            method: "POST",
            body: JSON.stringify(requestBody),
        });
        if (sessionResponse.status === 401) {
            throw new index_js_1.StagehandAPIUnauthorizedError("Unauthorized. Ensure you provided a valid API key.");
        }
        else if (sessionResponse.status !== 200) {
            const errorText = await sessionResponse.text();
            this.logger({
                category: "api",
                message: `API error (${sessionResponse.status}): ${errorText}`,
                level: 0,
            });
            throw new index_js_1.StagehandHttpError(`Unknown error: ${sessionResponse.status}`);
        }
        const sessionResponseBody = (await sessionResponse.json());
        if (sessionResponseBody.success === false) {
            throw new index_js_1.StagehandAPIError(sessionResponseBody.message);
        }
        // Temporary reroute for rollout
        if (!sessionResponseBody.data?.available && browserbaseSessionID) {
            sessionResponseBody.data.sessionId = browserbaseSessionID;
        }
        this.sessionId = sessionResponseBody.data.sessionId;
        return sessionResponseBody.data;
    }
    async act({ input, options, frameId, }) {
        // Strip non-serializable `page` and SDK-only fields from options before wire serialization
        let wireOptions;
        let serverCache;
        if (options) {
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const { page: _, serverCache: enableCache, ...restOptions } = options;
            serverCache = enableCache;
            if (Object.keys(restOptions).length > 0) {
                if (restOptions.model) {
                    restOptions.model = this.prepareModelConfig(restOptions.model);
                }
                wireOptions = restOptions;
            }
        }
        // Build wire-format request body
        const requestBody = {
            input,
            options: wireOptions,
            frameId,
        };
        return this.execute({
            method: "act",
            args: requestBody,
            serverCache,
        });
    }
    async extract({ instruction, schema: zodSchema, options, frameId, }) {
        // Convert Zod schema to JSON schema for wire format
        const jsonSchema = zodSchema ? (0, zodCompat_js_1.toJsonSchema)(zodSchema) : undefined;
        // Strip non-serializable `page` and SDK-only fields from options before wire serialization
        let wireOptions;
        let serverCache;
        if (options) {
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const { page: _, serverCache: enableCache, ...restOptions } = options;
            serverCache = enableCache;
            if (Object.keys(restOptions).length > 0) {
                if (restOptions.model) {
                    restOptions.model = this.prepareModelConfig(restOptions.model);
                }
                wireOptions = restOptions;
            }
        }
        // Build wire-format request body
        const requestBody = {
            instruction,
            schema: jsonSchema,
            options: wireOptions,
            frameId,
        };
        return this.execute({
            method: "extract",
            args: requestBody,
            serverCache,
        });
    }
    async observe({ instruction, options, frameId, }) {
        // Strip non-serializable `page` and SDK-only fields from options before wire serialization
        let wireOptions;
        let serverCache;
        if (options) {
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const { page: _, serverCache: enableCache, ...restOptions } = options;
            serverCache = enableCache;
            if (Object.keys(restOptions).length > 0) {
                if (restOptions.model) {
                    restOptions.model = this.prepareModelConfig(restOptions.model);
                }
                wireOptions = restOptions;
            }
        }
        // Build wire-format request body
        const requestBody = {
            instruction,
            options: wireOptions,
            frameId,
        };
        return this.execute({
            method: "observe",
            args: requestBody,
            serverCache,
        });
    }
    async goto(url, options, frameId) {
        const requestBody = { url, options, frameId };
        return this.execute({
            method: "navigate",
            args: requestBody,
        });
    }
    async agentExecute(agentConfig, executeOptions, frameId, shouldCache) {
        // Check if integrations are being used in API mode (not supported)
        if (agentConfig.integrations && agentConfig.integrations.length > 0) {
            throw new index_js_1.ExperimentalNotConfiguredError("MCP integrations");
        }
        // Strip non-serializable `page` from executeOptions before wire serialization
        let wireExecuteOptions;
        if (typeof executeOptions === "string") {
            wireExecuteOptions = { instruction: executeOptions };
        }
        else if (executeOptions.page) {
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const { page: _, ...rest } = executeOptions;
            wireExecuteOptions = rest;
        }
        else {
            wireExecuteOptions = executeOptions;
        }
        const wireAgentConfig = {
            systemPrompt: agentConfig.systemPrompt,
            mode: agentConfig.mode ?? (agentConfig.cua === true ? "cua" : undefined),
            cua: agentConfig.mode === undefined ? agentConfig.cua : undefined,
            model: agentConfig.model
                ? this.prepareModelConfig(agentConfig.model)
                : undefined,
            executionModel: agentConfig.executionModel
                ? this.prepareModelConfig(agentConfig.executionModel)
                : undefined,
        };
        // Build wire-format request body
        const requestBody = {
            agentConfig: wireAgentConfig,
            executeOptions: wireExecuteOptions,
            frameId,
            shouldCache,
        };
        const result = await this.execute({
            method: "agentExecute",
            args: requestBody,
        });
        const finishedData = this.consumeFinishedEventData() ?? null;
        this.latestAgentCacheEntry =
            finishedData?.cacheEntry !== undefined
                ? finishedData.cacheEntry
                : null;
        return result;
    }
    consumeLatestAgentCacheEntry() {
        const entry = this.latestAgentCacheEntry;
        this.latestAgentCacheEntry = null;
        return entry;
    }
    async end() {
        const url = `/sessions/${this.sessionId}/end`;
        const response = await this.request(url, {
            method: "POST",
        });
        return response;
    }
    async getReplayMetrics() {
        if (!this.sessionId) {
            throw new index_js_1.StagehandAPIError("sessionId is required to fetch metrics.");
        }
        const response = await this.request(`/sessions/${this.sessionId}/replay`, {
            method: "GET",
        });
        if (response.status !== 200) {
            const errorText = await response.text();
            this.logger({
                category: "api",
                message: `Failed to fetch metrics. Status ${response.status}: ${errorText}`,
                level: 0,
            });
            throw new index_js_1.StagehandHttpError(`Failed to fetch metrics with status ${response.status}: ${errorText}`);
        }
        const data = (await response.json());
        if (!data.success) {
            const errorData = data;
            throw new index_js_1.StagehandAPIError(`Failed to fetch metrics: ${errorData.error || "Unknown error"}`);
        }
        // Parse the API data into StagehandMetrics format
        const apiData = data.data;
        const metrics = {
            actPromptTokens: 0,
            actCompletionTokens: 0,
            actReasoningTokens: 0,
            actCachedInputTokens: 0,
            actInferenceTimeMs: 0,
            extractPromptTokens: 0,
            extractCompletionTokens: 0,
            extractReasoningTokens: 0,
            extractCachedInputTokens: 0,
            extractInferenceTimeMs: 0,
            observePromptTokens: 0,
            observeCompletionTokens: 0,
            observeReasoningTokens: 0,
            observeCachedInputTokens: 0,
            observeInferenceTimeMs: 0,
            agentPromptTokens: 0,
            agentCompletionTokens: 0,
            agentReasoningTokens: 0,
            agentCachedInputTokens: 0,
            agentInferenceTimeMs: 0,
            totalPromptTokens: 0,
            totalCompletionTokens: 0,
            totalReasoningTokens: 0,
            totalCachedInputTokens: 0,
            totalInferenceTimeMs: 0,
        };
        // Parse pages and their actions
        const pages = apiData?.pages || [];
        for (const page of pages) {
            const actions = page.actions || [];
            for (const action of actions) {
                // Get method name and token usage
                const method = (action.method || "").toLowerCase();
                const tokenUsage = action.tokenUsage;
                if (tokenUsage) {
                    const inputTokens = tokenUsage.inputTokens || 0;
                    const outputTokens = tokenUsage.outputTokens || 0;
                    const reasoningTokens = "reasoningTokens" in tokenUsage
                        ? Number(tokenUsage
                            .reasoningTokens ?? 0)
                        : 0;
                    const cachedInputTokens = "cachedInputTokens" in tokenUsage
                        ? Number(tokenUsage
                            .cachedInputTokens ?? 0)
                        : 0;
                    const timeMs = tokenUsage.timeMs || 0;
                    // Map method to metrics fields
                    if (method === "act") {
                        metrics.actPromptTokens += inputTokens;
                        metrics.actCompletionTokens += outputTokens;
                        metrics.actReasoningTokens += reasoningTokens;
                        metrics.actCachedInputTokens += cachedInputTokens;
                        metrics.actInferenceTimeMs += timeMs;
                    }
                    else if (method === "extract") {
                        metrics.extractPromptTokens += inputTokens;
                        metrics.extractCompletionTokens += outputTokens;
                        metrics.extractReasoningTokens += reasoningTokens;
                        metrics.extractCachedInputTokens += cachedInputTokens;
                        metrics.extractInferenceTimeMs += timeMs;
                    }
                    else if (method === "observe") {
                        metrics.observePromptTokens += inputTokens;
                        metrics.observeCompletionTokens += outputTokens;
                        metrics.observeReasoningTokens += reasoningTokens;
                        metrics.observeCachedInputTokens += cachedInputTokens;
                        metrics.observeInferenceTimeMs += timeMs;
                    }
                    else if (method === "agent") {
                        metrics.agentPromptTokens += inputTokens;
                        metrics.agentCompletionTokens += outputTokens;
                        metrics.agentReasoningTokens += reasoningTokens;
                        metrics.agentCachedInputTokens += cachedInputTokens;
                        metrics.agentInferenceTimeMs += timeMs;
                    }
                    // Always update totals for any method with token usage
                    metrics.totalPromptTokens += inputTokens;
                    metrics.totalCompletionTokens += outputTokens;
                    metrics.totalReasoningTokens += reasoningTokens;
                    metrics.totalCachedInputTokens += cachedInputTokens;
                    metrics.totalInferenceTimeMs += timeMs;
                }
            }
        }
        return metrics;
    }
    /**
     * Prepares a model configuration for the API payload by ensuring the `apiKey`
     * is included. If the model is passed as a string, converts it to an object
     * with `modelName` and `apiKey`.
     *
     * In API mode, we only attempt to load an API key from env vars when the
     * model provider differs from the one used to init the session.
     */
    prepareModelConfig(model) {
        if (typeof model === "string") {
            // Extract provider from model string (e.g., "openai/gpt-5-nano" -> "openai")
            const provider = model.includes("/") ? model.split("/")[0] : undefined;
            const apiKey = provider && provider !== this.modelProvider
                ? ((0, utils_js_1.loadApiKeyFromEnv)(provider, this.logger) ?? this.modelApiKey)
                : this.modelApiKey;
            return {
                modelName: model,
                apiKey,
            };
        }
        if (!model.apiKey) {
            const provider = model.modelName?.includes("/")
                ? model.modelName.split("/")[0]
                : undefined;
            const apiKey = provider && provider !== this.modelProvider
                ? ((0, utils_js_1.loadApiKeyFromEnv)(provider, this.logger) ?? this.modelApiKey)
                : this.modelApiKey;
            return {
                ...model,
                apiKey,
            };
        }
        return model;
    }
    consumeFinishedEventData() {
        const data = this.lastFinishedEventData;
        this.lastFinishedEventData = null;
        return data;
    }
    async execute({ method, args, params, serverCache, }) {
        this.lastFinishedEventData = null;
        const urlParams = new URLSearchParams(params);
        const queryString = urlParams.toString();
        const url = `/sessions/${this.sessionId}/${method}${queryString ? `?${queryString}` : ""}`;
        const response = await this.request(url, {
            method: "POST",
            body: JSON.stringify(args),
        }, serverCache);
        // Capture cache status from response header
        const cacheStatus = response.headers.get("browserbase-cache-status");
        if (!response.ok) {
            const errorBody = await response.text();
            throw new index_js_1.StagehandHttpError(`HTTP error! status: ${response.status}, body: ${errorBody}`);
        }
        if (!response.body) {
            throw new index_js_1.StagehandResponseBodyError();
        }
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        while (true) {
            const { value, done } = await reader.read();
            if (done && !buffer) {
                throw new index_js_1.StagehandServerError("Stream ended without completion signal");
            }
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split("\n\n");
            buffer = lines.pop() || "";
            for (const line of lines) {
                if (!line.startsWith("data: "))
                    continue;
                try {
                    const eventData = JSON.parse(line.slice(6));
                    if (eventData.type === "system") {
                        if (eventData.data.status === "error") {
                            const { error: errorMsg } = eventData.data;
                            // Throw plain Error to match local SDK behavior (useApi: false)
                            throw new Error(errorMsg);
                        }
                        if (eventData.data.status === "finished") {
                            this.lastFinishedEventData = eventData.data;
                            // If caching was bypassed for this request, suppress cache status
                            // so we don't log or surface a MISS that the server emits anyway.
                            const cacheEnabled = this.shouldUseCache(serverCache);
                            return this.attachCacheStatus(eventData.data.result, method, cacheEnabled ? cacheStatus : null, cacheEnabled ? eventData : { data: {} });
                        }
                    }
                    else if (eventData.type === "log") {
                        const msg = eventData.data.message;
                        // Skip server-side internal logs that don't apply to API mode
                        if (msg?.message === "Connecting to local browser") {
                            continue;
                        }
                        this.logger(eventData.data.message);
                    }
                }
                catch (e) {
                    // Let Error instances pass through (server errors thrown above)
                    // Only wrap SyntaxError from JSON.parse as parse errors
                    if (e instanceof Error && !(e instanceof SyntaxError)) {
                        throw e;
                    }
                    const errorMessage = e instanceof Error ? e.message : String(e);
                    this.logger({
                        category: "api",
                        message: `Failed to parse SSE event: ${errorMessage}`,
                        level: 0,
                    });
                    throw new index_js_1.StagehandResponseParseError(`Failed to parse server response: ${errorMessage}`);
                }
            }
            if (done) {
                // Process any remaining data in buffer before exiting
                if (buffer.trim() && buffer.startsWith("data: ")) {
                    try {
                        const eventData = JSON.parse(buffer.slice(6));
                        if (eventData.type === "system" &&
                            eventData.data.status === "finished") {
                            return this.attachCacheStatus(eventData.data.result, method, cacheStatus, eventData);
                        }
                    }
                    catch {
                        this.logger({
                            category: "api",
                            message: `Incomplete data in final buffer: ${buffer.substring(0, 100)}`,
                            level: 0,
                        });
                    }
                }
                throw new index_js_1.StagehandServerError("Stream ended without completion signal");
            }
        }
    }
    /**
     * Resolves the final cache status from the response header or SSE event data,
     * logs it, and attaches it to act/extract results before returning.
     */
    attachCacheStatus(result, method, cacheStatus, eventData) {
        const finalCacheStatus = cacheStatus ||
            (typeof eventData.data.cacheHit === "boolean"
                ? eventData.data.cacheHit
                    ? "HIT"
                    : "MISS"
                : undefined);
        if (finalCacheStatus &&
            (method === "act" || method === "extract" || method === "observe")) {
            this.logger({
                category: "cache",
                message: `${method} server cache ${finalCacheStatus.toLowerCase()}`,
                level: 1,
            });
        }
        if (finalCacheStatus &&
            result &&
            typeof result === "object" &&
            (method === "act" || method === "extract" || method === "observe")) {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            result.cacheStatus =
                finalCacheStatus;
        }
        return result;
    }
    /**
     * Determine if caching should be enabled for a request.
     * Method-level setting takes precedence over instance-level setting.
     */
    shouldUseCache(methodServerCache) {
        // If method-level setting is explicitly provided, use it
        if (methodServerCache !== undefined) {
            return methodServerCache;
        }
        // Otherwise, use instance-level setting
        return this.serverCache;
    }
    async request(path, options, serverCache) {
        const defaultHeaders = {
            "x-bb-api-key": this.apiKey,
            "x-bb-project-id": this.projectId,
            "x-bb-session-id": this.sessionId,
            // we want real-time logs, so we stream the response
            "x-stream-response": "true",
            "x-model-api-key": this.modelApiKey,
            "x-language": "typescript",
            "x-sdk-version": version_js_1.STAGEHAND_VERSION,
        };
        // Add cache bypass header if caching is disabled
        if (!this.shouldUseCache(serverCache)) {
            defaultHeaders["browserbase-cache-bypass"] = "true";
        }
        if (options.method === "POST" && options.body) {
            defaultHeaders["Content-Type"] = "application/json";
        }
        // Use STAGEHAND_API_URL env var if set, otherwise use region-based URL
        // Ensure /v1 suffix is present for consistency
        let baseUrl;
        if (process.env.STAGEHAND_API_URL) {
            const envUrl = process.env.STAGEHAND_API_URL.replace(/\/+$/, "");
            // Append /v1 if not already present
            baseUrl = envUrl.endsWith("/v1") ? envUrl : `${envUrl}/v1`;
        }
        else {
            baseUrl = getApiUrlForRegion(this.region);
        }
        const response = await this.fetchWithCookies(`${baseUrl}${path}`, {
            ...options,
            headers: {
                ...defaultHeaders,
                ...options.headers,
            },
        });
        return response;
    }
}
exports.StagehandAPIClient = StagehandAPIClient;
//# sourceMappingURL=api.js.map