"use strict";
/**
 * Sandbox class for interacting with a specific sandbox instance.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sandbox = void 0;
const errors_js_1 = require("./errors.cjs");
const helpers_js_1 = require("./helpers.cjs");
const command_handle_js_1 = require("./command_handle.cjs");
const ws_execute_js_1 = require("./ws_execute.cjs");
/**
 * Represents an active sandbox for running commands and file operations.
 *
 * This class is typically obtained from SandboxClient.createSandbox() and
 * provides methods for command execution and file I/O within the sandbox
 * environment.
 *
 * @example
 * ```typescript
 * const sandbox = await client.createSandbox(snapshot.id);
 * try {
 *   const result = await sandbox.run("python --version");
 *   console.log(result.stdout);
 * } finally {
 *   await sandbox.delete();
 * }
 * ```
 */
class Sandbox {
    /** @internal */
    constructor(data, client) {
        /** Display name (can be updated). */
        Object.defineProperty(this, "name", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** URL for data plane operations (file I/O, command execution). */
        Object.defineProperty(this, "dataplane_url", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Provisioning status ("provisioning", "ready", "failed", "stopped"). */
        Object.defineProperty(this, "status", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Human-readable status message (e.g., error details when failed). */
        Object.defineProperty(this, "status_message", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Unique identifier (UUID). Remains constant even if name changes. */
        Object.defineProperty(this, "id", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Timestamp when the sandbox was created. */
        Object.defineProperty(this, "created_at", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Timestamp when the sandbox was last updated. */
        Object.defineProperty(this, "updated_at", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**
         * Idle timeout TTL in seconds (`0` means disabled).
         * New sandboxes receive a server-side default of `600` seconds (10 minutes)
         * when the caller did not set `idleTtlSeconds` explicitly. The launcher
         * stops the sandbox after this many idle seconds.
         */
        Object.defineProperty(this, "idle_ttl_seconds", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**
         * Seconds after the sandbox enters the `stopped` state before it (and
         * its filesystem clone) are permanently deleted (`0` means disabled).
         */
        Object.defineProperty(this, "delete_after_stop_seconds", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**
         * Timestamp when the sandbox transitioned to `stopped`, or `undefined`
         * while running. The deletion deadline is
         * `stopped_at + delete_after_stop_seconds`.
         */
        Object.defineProperty(this, "stopped_at", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Snapshot ID used to create this sandbox. */
        Object.defineProperty(this, "snapshot_id", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Number of vCPUs allocated. */
        Object.defineProperty(this, "vCpus", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Memory allocation in bytes. */
        Object.defineProperty(this, "mem_bytes", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Root filesystem capacity in bytes. */
        Object.defineProperty(this, "fs_capacity_bytes", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "_client", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        this.name = data.name;
        this.dataplane_url = data.dataplane_url;
        this.status = data.status;
        this.status_message = data.status_message;
        this.id = data.id;
        this.created_at = data.created_at;
        this.updated_at = data.updated_at;
        this.idle_ttl_seconds = data.idle_ttl_seconds;
        this.delete_after_stop_seconds = data.delete_after_stop_seconds;
        this.stopped_at = data.stopped_at ?? undefined;
        this.snapshot_id = data.snapshot_id;
        this.vCpus = data.vcpus;
        this.mem_bytes = data.mem_bytes;
        this.fs_capacity_bytes = data.fs_capacity_bytes;
        this._client = client;
    }
    /**
     * Return the dataplane URL.
     *
     * The client does not gate on lifecycle status: a stopped sandbox is resumed
     * by the platform when the dataplane request arrives, so only the presence of
     * a URL is required here. A genuinely not-ready box surfaces the server's
     * LangSmithSandboxNotReadyError from the request itself.
     *
     * @throws LangSmithDataplaneNotConfiguredError if dataplane_url is not configured.
     */
    requireDataplaneUrl() {
        if (!this.dataplane_url) {
            throw new errors_js_1.LangSmithDataplaneNotConfiguredError(`Sandbox '${this.name}' does not have a dataplane_url configured. ` +
                "Runtime operations require a dataplane URL.");
        }
        return this.dataplane_url;
    }
    async run(command, options = {}) {
        const { wait = true, onStdout, onStderr, idleTimeout, killOnDisconnect, ttlSeconds, pty, ...restOptions } = options;
        const hasCallbacks = onStdout !== undefined || onStderr !== undefined;
        if (!wait || hasCallbacks) {
            // WebSocket required for streaming / non-blocking
            const handle = await this._runWs(command, {
                ...restOptions,
                idleTimeout,
                killOnDisconnect,
                ttlSeconds,
                pty,
                onStdout,
                onStderr,
            });
            if (!wait) {
                return handle;
            }
            // wait=true with callbacks: drain stream and return result
            return handle.result;
        }
        // wait=true, no callbacks: try WS, fall back to HTTP
        try {
            const handle = await this._runWs(command, {
                ...restOptions,
                idleTimeout,
                killOnDisconnect,
                ttlSeconds,
                pty,
            });
            return await handle.result;
        }
        catch (e) {
            // Fall back to HTTP on connection errors or missing ws package
            const name = e != null && typeof e === "object" ? e.name : "";
            const message = e != null && typeof e === "object" ? (e.message ?? "") : "";
            if (name === "LangSmithSandboxConnectionError" ||
                name === "LangSmithSandboxServerReloadError" ||
                message.includes("'ws' package")) {
                return this._runHttp(command, restOptions);
            }
            throw e;
        }
    }
    /**
     * Execute a command via WebSocket streaming.
     * @internal
     */
    async _runWs(command, options = {}) {
        const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout, killOnDisconnect, ttlSeconds, pty, } = options;
        const dataplaneUrl = this.requireDataplaneUrl();
        const clientHeaders = this._client.getDefaultHeaders();
        const [stream, control] = await (0, ws_execute_js_1.runWsStream)(dataplaneUrl, this._client.getApiKey(), command, {
            timeout,
            env,
            cwd,
            shell,
            commandId,
            idleTimeout,
            killOnDisconnect,
            ttlSeconds,
            pty,
            ...(Object.keys(clientHeaders).length > 0
                ? { headers: clientHeaders }
                : {}),
        });
        const handle = new command_handle_js_1.CommandHandle(stream, control, this, {
            onStdout,
            onStderr,
        });
        await handle._ensureStarted();
        return handle;
    }
    /**
     * Execute a command via HTTP POST (blocking).
     * @internal
     */
    async _runHttp(command, options = {}) {
        const { timeout = 60, env, cwd, shell = "/bin/bash" } = options;
        const dataplaneUrl = this.requireDataplaneUrl();
        const url = `${dataplaneUrl}/execute`;
        const payload = {
            command,
            timeout,
            shell,
        };
        if (env !== undefined) {
            payload.env = env;
        }
        if (cwd !== undefined) {
            payload.cwd = cwd;
        }
        const response = await this._client._fetch(url, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(payload),
            signal: AbortSignal.timeout((timeout + 10) * 1000),
        });
        if (!response.ok) {
            await (0, helpers_js_1.handleSandboxHttpError)(response);
        }
        const data = await response.json();
        return {
            stdout: data.stdout ?? "",
            stderr: data.stderr ?? "",
            exit_code: data.exit_code ?? -1,
        };
    }
    /**
     * Reconnect to a running command by its command ID.
     *
     * Returns a new CommandHandle that resumes output from the given offsets.
     *
     * @param commandId - The server-assigned command ID.
     * @param options - Reconnection options with byte offsets.
     * @returns A new CommandHandle.
     */
    async reconnect(commandId, options = {}) {
        const { stdoutOffset = 0, stderrOffset = 0 } = options;
        const dataplaneUrl = this.requireDataplaneUrl();
        const clientHeaders = this._client.getDefaultHeaders();
        const [stream, control] = await (0, ws_execute_js_1.reconnectWsStream)(dataplaneUrl, this._client.getApiKey(), commandId, {
            stdoutOffset,
            stderrOffset,
            ...(Object.keys(clientHeaders).length > 0
                ? { headers: clientHeaders }
                : {}),
        });
        return new command_handle_js_1.CommandHandle(stream, control, this, {
            commandId,
            stdoutOffset,
            stderrOffset,
        });
    }
    /**
     * Write content to a file in the sandbox.
     *
     * @param path - Target file path in the sandbox.
     * @param content - File content (string or bytes).
     * @param timeout - Request timeout in seconds.
     *
     * @example
     * ```typescript
     * await sandbox.write("/tmp/script.py", 'print("Hello!")');
     * ```
     */
    async write(path, content, timeout = 60) {
        const dataplaneUrl = this.requireDataplaneUrl();
        const url = `${dataplaneUrl}/upload?path=${encodeURIComponent(path)}`;
        // Ensure content is bytes for multipart upload
        const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
        const formData = new FormData();
        // Create a copy to ensure we have a plain ArrayBuffer (not SharedArrayBuffer)
        const buffer = new Uint8Array(bytes).buffer;
        const blob = new Blob([buffer], { type: "application/octet-stream" });
        formData.append("file", blob, "file");
        const response = await this._client._fetch(url, {
            method: "POST",
            body: formData,
            signal: AbortSignal.timeout(timeout * 1000),
        });
        if (!response.ok) {
            await (0, helpers_js_1.handleSandboxHttpError)(response);
        }
    }
    /**
     * Read a file from the sandbox.
     *
     * @param path - File path to read.
     * @param timeout - Request timeout in seconds.
     * @returns File contents as Uint8Array.
     *
     * @example
     * ```typescript
     * const content = await sandbox.read("/tmp/output.txt");
     * const text = new TextDecoder().decode(content);
     * console.log(text);
     * ```
     */
    async read(path, timeout = 60) {
        const dataplaneUrl = this.requireDataplaneUrl();
        const url = `${dataplaneUrl}/download?path=${encodeURIComponent(path)}`;
        const response = await this._client._fetch(url, {
            method: "GET",
            signal: AbortSignal.timeout(timeout * 1000),
        });
        if (!response.ok) {
            await (0, helpers_js_1.handleSandboxHttpError)(response);
        }
        const buffer = await response.arrayBuffer();
        return new Uint8Array(buffer);
    }
    /**
     * Delete this sandbox.
     *
     * @example
     * ```typescript
     * const sandbox = await client.createSandbox(snapshot.id);
     * try {
     *   await sandbox.run("echo hello");
     * } finally {
     *   await sandbox.delete();
     * }
     * ```
     */
    async delete() {
        await this._client.deleteSandbox(this.name);
    }
    /**
     * Start a stopped sandbox and wait until ready.
     *
     * Updates this sandbox's status and dataplane_url in place.
     *
     * @param timeout - Timeout in seconds when waiting for ready. Default: 120.
     */
    async start(options = {}) {
        const refreshed = await this._client.startSandbox(this.name, options);
        this.status = refreshed.status;
        this.dataplane_url = refreshed.dataplane_url;
    }
    /**
     * Stop a running sandbox (preserves sandbox files for later restart).
     */
    async stop() {
        await this._client.stopSandbox(this.name);
        this.status = "stopped";
        this.dataplane_url = undefined;
    }
    /**
     * Capture a snapshot from this sandbox.
     *
     * @param name - Snapshot name.
     * @param options - Capture options (timeout).
     * @returns Snapshot in "ready" status.
     */
    async captureSnapshot(name, options = {}) {
        return this._client.captureSnapshot(this.name, name, options);
    }
}
exports.Sandbox = Sandbox;
