import * as fs from "node:fs";
import * as path from "node:path";
import type {
  BunnyAgentCodingRunBody,
  ExecOptions,
  SandboxAdapter,
  SandboxHandle,
  Volume,
} from "@bunny-agent/manager";
import { createSandockClient, type SandockClient } from "sandock";

/** Single volume mount configuration (name → get/create by name; mountPath inside container) */
export interface SandockVolumeConfig {
  /** Volume name for persistence (will be created if not exists) */
  volumeName: string;
  /** Mount path inside the sandbox */
  volumeMountPath: string;
}

/**
 * Options for creating a SandockSandbox instance
 */
export interface SandockSandboxOptions {
  /** Sandock API base URL (defaults to https://sandock.ai) */
  baseUrl?: string;
  /** Sandock API key for authentication */
  apiKey?: string;
  /** Docker image to use for the sandbox */
  image?: string;
  /** Working directory inside the sandbox */
  workdir?: string;
  /** Memory limit in MB */
  memoryLimitMb?: number;
  /** CPU shares */
  cpuShares?: number;
  /**
   * If true (default), keep sandbox running after execution (platform may retain it for e.g. 30 minutes).
   * If false, sandbox is stopped and deleted after the command finishes.
   */
  keep?: boolean;
  /** Timeout for sandbox operations in milliseconds (default: 1800000 = 30 min) */
  timeout?: number;
  /** Path to template directory to upload */
  templatesPath?: string;
  /**
   * Volume mounts for persistence (e.g. workspace + Claude SDK session storage).
   * Each volume is created/fetched by name and mounted at the given path.
   */
  volumes?: SandockVolumeConfig[];
  /** Sandbox name/title for the Sandock API (e.g. for display in dashboard) */
  name?: string;

  /**
   * Existing sandbox ID to attach to. When set, attach() will first try to use this sandbox
   * (get + start); on failure (e.g. not found, deleted), falls back to creating a new sandbox.
   */
  sandboxId?: string;

  /**
   * If true, skip installing SDK and runner (image already has them).
   * Only upload template files and use `bunny-agent run`. Use with pre-built images like vikadata/bunny-agent.
   */
  skipBootstrap?: boolean;

  /**
   * Environment variables to set in the sandbox.
   * These will be available to all commands executed in the sandbox.
   */
  env?: Record<string, string>;

  /**
   * Maximum lifetime of the sandbox in seconds.
   * After this duration, the sandbox will be automatically terminated.
   */
  maxLifetimeSeconds?: number;

  /**
   * Auto-delete interval in minutes. -1 = never auto-delete.
   */
  autoDeleteInterval?: number;

  /**
   * Optional command to run when creating the sandbox.
   * If provided, this command will be passed to the Sandock API during sandbox creation.
   * If omitted, the default creation behavior is preserved.
   */
  command?: string[];
}

/**
 * Sandock-based sandbox implementation.
 *
 * Uses the official Sandock SDK (https://sandock.ai) for cloud-based
 * Docker sandbox execution with persistent filesystems.
 */
export class SandockSandbox implements SandboxAdapter {
  private readonly client: SandockClient;
  private readonly baseUrl: string;
  private readonly authHeaders?: Record<string, string>;
  private readonly image: string;
  private readonly workdir: string;
  private readonly memoryLimitMb?: number;
  private readonly cpuShares?: number;
  private readonly keep: boolean;
  private readonly timeout: number;
  private readonly templatesPath?: string;
  private readonly volumeConfigs: SandockVolumeConfig[];
  private readonly skipBootstrap: boolean;
  private readonly env: Record<string, string>;
  private readonly name?: string;
  private readonly maxLifetimeSeconds?: number;
  private readonly autoDeleteInterval?: number;
  private readonly command?: string[];

  /** Current handle for the sandbox instance; also holds optional existing sandbox id to attach to (before attach) */
  private currentHandle: SandockHandle | null = null;
  private _sandboxId: string | null = null;

  private shouldUseSandagentCli(): boolean {
    const img = this.image.toLowerCase();
    return (
      img.includes("vikadata/sandagent") ||
      img.includes("/sandagent:") ||
      img.endsWith("/sandagent") ||
      img === "sandagent"
    );
  }

  constructor(options: SandockSandboxOptions = {}) {
    const apiKey = options.apiKey ?? process.env.SANDOCK_API_KEY;
    this.baseUrl = options.baseUrl ?? "https://sandock.ai";
    this.authHeaders = apiKey
      ? { Authorization: `Bearer ${apiKey}` }
      : undefined;

    if (!apiKey) {
      console.warn(
        "SANDOCK_API_KEY not set. Sandock API calls will fail.\n" +
          "Get your API key at https://sandock.ai",
      );
    }

    this.client = createSandockClient({
      baseUrl: this.baseUrl,
      headers: this.authHeaders,
    });

    this.image = options.image ?? "sandockai/sandock-code:latest";
    this.workdir = options.workdir ?? "/workspace";
    this.memoryLimitMb = options.memoryLimitMb;
    this.cpuShares = options.cpuShares;
    this.keep = options.keep ?? true;
    this.timeout = options.timeout ?? 1_800_000;
    this.templatesPath = options.templatesPath;
    this.volumeConfigs = options.volumes ?? [];
    this.skipBootstrap = options.skipBootstrap ?? false;
    this.env = options.env ?? {};
    this.name = options.name;
    this._sandboxId = options.sandboxId ?? null;
    this.maxLifetimeSeconds = options.maxLifetimeSeconds;
    this.autoDeleteInterval = options.autoDeleteInterval;
    this.command = options.command;
  }

  /**
   * Get the environment variables configured for this sandbox.
   */
  getEnv(): Record<string, string> {
    return this.env;
  }

  /**
   * Get the working directory configured for this sandbox.
   */
  getWorkdir(): string {
    return this.workdir;
  }

  /**
   * Get the runner command to execute in the sandbox.
   * When skipBootstrap is true, use the image-bundled CLI command.
   * For sandagent images, prefer `sandagent run`; otherwise `bunny-agent run`.
   * When skipBootstrap is false, use npm-installed runner in workdir.
   */
  getRunnerCommand(): string[] {
    if (this.skipBootstrap) {
      if (this.shouldUseSandagentCli()) {
        return ["sandagent", "run"];
      }
      return ["bunny-agent", "run"];
    }
    return [`${this.workdir}/node_modules/.bin/bunny-agent`, "run"];
  }

  /**
   * Get the current handle if already attached, or null if not attached yet.
   */
  getHandle(): SandboxHandle | null {
    return this.currentHandle;
  }

  /**
   * Attach to or create a sandbox. When _sandboxId is set (from options.sandboxId), tries to
   * attach to that sandbox first (get + start); on failure, falls back to creating a new sandbox.
   */
  async attach(): Promise<SandboxHandle> {
    if (this.currentHandle) return this.currentHandle;
    const existing = await this.tryAttachExisting();
    if (existing) {
      return existing;
    }

    return await this.createAndAttachNew();
  }

  /** Try to attach to existing sandbox by _sandboxId; on failure clear id and return null. */
  private async tryAttachExisting(): Promise<SandockHandle | null> {
    const id = this._sandboxId;
    if (!id) return null;
    try {
      const { data } = await this.client.sandbox.get(id);
      const status = data.status;

      if (status === "STOPPED" || status === "PAUSED") {
        console.log(
          `[Sandock] Restarting existing sandbox ${id} (status: ${status})`,
        );
        const startResult = await this.client.sandbox.start(id);
        if (!startResult.data.started) {
          console.warn(
            `[Sandock] start() did not report started for ${id}, creating new`,
          );
          this._sandboxId = null;
          return null;
        }
      } else if (status !== "RUNNING") {
        console.warn(
          `[Sandock] Sandbox ${id} is not reusable (status: ${status}), creating new`,
        );
        this._sandboxId = null;
        return null;
      }

      const volumeMounts = await this.resolveVolumeMounts();
      const handle = new SandockHandle(
        this.client,
        this.baseUrl,
        this.authHeaders,
        id,
        this.workdir,
        this.timeout,
        () => {},
        this.keep,
        this.env,
        volumeMounts,
      );

      this.currentHandle = handle;
      console.log(`[Sandock] Attached to existing sandbox: ${id}`);
      return handle;
    } catch (err) {
      console.warn(
        `[Sandock] Failed to attach to sandbox ${id}, creating new:`,
        err instanceof Error ? err.message : err,
      );
      this._sandboxId = null;
      return null;
    }
  }

  /** Create a new sandbox, initialize it, and set as current handle. */
  private async createAndAttachNew(): Promise<SandockHandle> {
    const volumeMounts = await this.resolveVolumeMounts();
    const { sandboxId } = await this.createAndStartSandbox(volumeMounts);
    const handle = new SandockHandle(
      this.client,
      this.baseUrl,
      this.authHeaders,
      sandboxId,
      this.workdir,
      this.timeout,
      () => {},
      this.keep,
      this.env,
      volumeMounts,
    );
    this._sandboxId = sandboxId;
    await this.initializeSandbox(handle);
    this.currentHandle = handle;
    return handle;
  }

  /** Resolve volume configs to Volume[] (get/create by name, wait for ready). */
  private async resolveVolumeMounts(): Promise<Volume[]> {
    const volumeMounts: Volume[] = [];
    for (const v of this.volumeConfigs) {
      console.log(`[Sandock] Getting/creating volume: ${v.volumeName}`);
      const volume = await this.client.volume.getByName(v.volumeName, true);
      const mountPath = v.volumeMountPath;

      if (volume.data.status && volume.data.status !== "ready") {
        const ready = await this.waitVolumeReady(v.volumeName, 30000);
        if (!ready) {
          throw new Error(
            `Volume '${v.volumeName}' failed to become ready. Status: ${volume.data.status}`,
          );
        }
      }

      volumeMounts.push({
        volumeId: volume.data.id,
        ...(volume.data.spaceId ? { spaceId: volume.data.spaceId } : {}),
        mountPath,
        name: v.volumeName,
      });
      console.log(
        `[Sandock] Using volume ${volume.data.id} (${v.volumeName}) at ${mountPath}`,
      );
    }
    return volumeMounts;
  }

  private async waitVolumeReady(
    volumeName: string,
    maxWaitMs: number,
  ): Promise<boolean> {
    const startTime = Date.now();
    let current = await this.client.volume.getByName(volumeName, false);
    while (
      current.data.status !== "ready" &&
      Date.now() - startTime < maxWaitMs
    ) {
      console.log(
        `[Sandock] Volume ${volumeName} status: ${current.data.status}, waiting...`,
      );
      await new Promise((resolve) => setTimeout(resolve, 1000));
      current = await this.client.volume.getByName(volumeName, false);
    }
    return current.data.status === "ready";
  }

  /** Create sandbox and start it; returns sandbox id and volume mounts. */
  private async createAndStartSandbox(
    volumeMounts: Volume[],
  ): Promise<{ sandboxId: string; volumeMounts: Volume[] }> {
    const createOptions: {
      image: string;
      memory?: number;
      cpu?: number;
      volumes?: Array<{ volumeId: string; mountPath: string }>;
      title?: string;
      activeDeadlineSeconds?: number;
      autoDeleteInterval?: number;
      command?: string[];
      env?: Record<string, string>;
    } = {
      image: this.image,
      memory: this.memoryLimitMb,
      cpu: this.cpuShares,
      title: this.name,
      activeDeadlineSeconds: this.maxLifetimeSeconds,
      autoDeleteInterval: this.autoDeleteInterval,
      command: this.command,
    };
    if (volumeMounts.length > 0) {
      createOptions.volumes = volumeMounts.map((v) => ({
        volumeId: v.volumeId,
        mountPath: v.mountPath,
      }));
    }
    if (Object.keys(this.env).length > 0) {
      createOptions.env = this.env;
    }

    const createResult = await this.client.sandbox.create(createOptions);
    const sandboxId = createResult.data.id;
    if (!sandboxId) {
      throw new Error("No sandbox ID returned from Sandock API");
    }
    console.log(
      `[Sandock] Created new sandbox: ${sandboxId} ${this.name ? `, title: ${this.name}` : ""}`,
    );
    await this.client.sandbox.start(sandboxId);
    return { sandboxId, volumeMounts };
  }

  private async initializeSandbox(handle: SandockHandle): Promise<void> {
    // Step 0: Create workspace directory
    console.log(`[Sandock] Creating workspace directory: ${this.workdir}`);
    const mkdirResult = await handle.runCommand(`mkdir -p ${this.workdir}`);
    if (mkdirResult.exitCode !== 0) {
      console.warn(`[Sandock] mkdir warning: ${mkdirResult.stderr}`);
    }

    if (this.skipBootstrap) {
      console.log(
        `[Sandock] skipBootstrap=true, skipping SDK and runner install`,
      );
    } else {
      // Install runner-cli from npm (brings in @anthropic-ai/claude-agent-sdk as dependency)
      console.log(
        `[Sandock] Installing @bunny-agent/runner-cli@latest to ${this.workdir}`,
      );
      const installResult = await handle.runCommand(
        `cd ${this.workdir} && npm install --no-audit --no-fund --prefer-offline @bunny-agent/runner-cli@latest 2>&1`,
      );
      if (installResult.exitCode !== 0) {
        console.error(
          `[Sandock] Failed to install runner-cli: ${installResult.stdout}`,
        );
      }
    }

    // Step 3: Upload template (user-selected template; always apply)
    if (this.templatesPath && fs.existsSync(this.templatesPath)) {
      const templateFiles = this.collectFiles(this.templatesPath, "");
      console.log(
        `[Sandock] Uploading ${templateFiles.length} template files to ${this.workdir}`,
      );
      await handle.upload(templateFiles, this.workdir);
    } else if (this.templatesPath) {
      console.warn(
        `[Sandock] Template path not found: ${this.templatesPath}, skipping`,
      );
    }
  }

  private collectFiles(
    dir: string,
    prefix: string,
  ): Array<{ path: string; content: Uint8Array | string }> {
    const files: Array<{ path: string; content: Uint8Array | string }> = [];
    const entries = fs.readdirSync(dir, { withFileTypes: true });

    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;

      if (entry.isDirectory()) {
        // Skip node_modules and .git only
        if (entry.name === "node_modules" || entry.name === ".git") {
          continue;
        }
        files.push(...this.collectFiles(fullPath, relativePath));
      } else if (entry.isFile()) {
        files.push({
          path: relativePath,
          content: fs.readFileSync(fullPath),
        });
      }
    }

    return files;
  }
}

/**
 * Handle for an active Sandock sandbox
 */
class SandockHandle implements SandboxHandle {
  private readonly client: SandockClient;
  private readonly baseUrl: string;
  private readonly authHeaders?: Record<string, string>;
  private readonly sandboxId: string;
  private readonly defaultWorkdir: string;
  private readonly timeout: number;
  private readonly onDestroy: () => void;
  private readonly keep: boolean;
  private readonly sandboxEnv: Record<string, string>;
  private readonly volumes: Volume[] | null;

  constructor(
    client: SandockClient,
    baseUrl: string,
    authHeaders: Record<string, string> | undefined,
    sandboxId: string,
    defaultWorkdir: string,
    timeout: number,
    onDestroy: () => void,
    keep: boolean,
    sandboxEnv: Record<string, string> = {},
    volumes: Volume[] | null = null,
  ) {
    this.client = client;
    this.baseUrl = baseUrl;
    this.authHeaders = authHeaders;
    this.sandboxId = sandboxId;
    this.defaultWorkdir = defaultWorkdir;
    this.timeout = timeout;
    this.onDestroy = onDestroy;
    this.keep = keep;
    this.sandboxEnv = sandboxEnv;
    this.volumes = volumes;
  }

  /**
   * Get the sandbox instance ID.
   */
  getSandboxId(): string {
    return this.sandboxId;
  }

  /**
   * Get the volume mounts for this sandbox.
   */
  getVolumes(): Volume[] | null {
    return this.volumes;
  }

  /**
   * Get the working directory for this sandbox handle
   */
  getWorkdir(): string {
    return this.defaultWorkdir;
  }

  /**
   * Direct Sandock coding-run stream.
   * Prefer API streaming to avoid writing request JSON into sandbox /tmp.
   */
  streamCodingRun(
    body: BunnyAgentCodingRunBody,
    opts?: ExecOptions,
  ): AsyncIterable<Uint8Array> {
    const self = this;
    return {
      async *[Symbol.asyncIterator](): AsyncIterator<Uint8Array> {
        const base = self.baseUrl.replace(/\/$/, "");
        const endpoint = `${base}/api/v1/sandbox/${self.sandboxId}/coding/run`;
        const res = await fetch(endpoint, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Accept: "text/event-stream",
            ...(self.authHeaders ?? {}),
          },
          body: JSON.stringify(body),
          signal: opts?.signal,
        });
        if (!res.ok || !res.body) {
          const errText = await res.text().catch(() => "");
          const errorMsg = `${res.status} ${res.statusText} ${errText}`.trim();
          throw new Error(
            `Failed to stream Sandock coding run: ${errorMsg || "no response body"}`,
          );
        }

        const reader = res.body.getReader();
        try {
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            if (value != null) yield value;
          }
        } finally {
          reader.releaseLock();
        }
      },
    };
  }

  /**
   * Run a command and wait for completion (used internally)
   */
  async runCommand(
    cmd: string,
  ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
    let stdout = "";
    let stderr = "";

    const result = await this.client.sandbox.shell(
      this.sandboxId,
      { cmd, timeoutMs: this.timeout },
      {
        onStdout: (chunk: string) => {
          stdout += chunk;
        },
        onStderr: (chunk: string) => {
          stderr += chunk;
        },
      },
    );

    return {
      exitCode: result.data.exitCode ?? 0,
      stdout,
      stderr,
    };
  }

  /**
   * Execute a command in the sandbox and stream the output
   */
  exec(command: string[], opts?: ExecOptions): AsyncIterable<Uint8Array> {
    const self = this;
    const signal = opts?.signal;

    // Merge sandbox-level env with call-level env (call-level takes precedence)
    const envWithNodePath: Record<string, string> = {
      ...this.sandboxEnv,
      ...opts?.env,
      IS_SANDBOX: "1",
    };

    // Debug: log environment variables being passed to sandbox
    console.log("[Sandock] Executing command:", command.join(" "));

    return {
      async *[Symbol.asyncIterator](): AsyncIterator<Uint8Array> {
        // Build command string with proper shell escaping (single-quote wrapping)
        // Each argument is wrapped in single quotes with internal quotes escaped
        const baseCmd =
          command.length === 1
            ? command[0]
            : command
                .map((arg) => "'" + arg.replace(/'/g, "'\\''") + "'")
                .join(" ");

        // Build full command with cwd and env support
        const parts: string[] = [];

        // Add working directory change (escape single quotes in path)
        const workdir = opts?.cwd ?? self.defaultWorkdir;
        if (workdir) {
          const escapedWorkdir = workdir.replace(/'/g, "'\\''");
          parts.push(`cd '${escapedWorkdir}'`);
        }

        // Add environment variables (validate keys and escape values)
        const validKeyPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
        const envParts = Object.entries(envWithNodePath)
          .filter(([key]) => validKeyPattern.test(key))
          .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
          .join(" && ");
        if (envParts) {
          parts.push(envParts);
        }

        // Wrap the command to capture its PID and handle signals
        // We write the PID to a file so we can kill it if needed
        const pidFile = `/tmp/bunny-agent-${Date.now()}-${Math.random().toString(36).substring(7)}.pid`;
        const wrappedCmd = `(${baseCmd}) & echo $! > ${pidFile}; wait $!; rm -f ${pidFile}`;
        parts.push(wrappedCmd);

        const cmd = parts.join(" && ");

        // Queue for streaming chunks
        const queue: Uint8Array[] = [];
        let done = false;
        let error: Error | null = null;
        let resolveWait: (() => void) | null = null;

        // Monitor abort signal and kill the process
        const abortHandler = async () => {
          console.log(
            "[Sandock] Abort signal received, terminating process...",
          );
          console.log("[Sandock] PID file:", pidFile);

          // Try to kill the process using the PID file
          try {
            // First check if PID file exists and read it
            const checkCmd = `if [ -f ${pidFile} ]; then cat ${pidFile}; else echo "PID file not found"; fi`;
            const checkResult = await self.client.sandbox.shell(
              self.sandboxId,
              { cmd: checkCmd, timeoutMs: 2000 },
              {},
            );
            console.log("[Sandock] PID check result:", checkResult.data.stdout);

            // Now try to kill the process
            const killCmd = `if [ -f ${pidFile} ]; then PID=$(cat ${pidFile}); echo "Killing PID: $PID"; kill -TERM $PID 2>&1 || echo "Kill failed"; rm -f ${pidFile}; else echo "No PID file to kill"; fi`;
            const killResult = await self.client.sandbox.shell(
              self.sandboxId,
              { cmd: killCmd, timeoutMs: 5000 },
              {},
            );
            console.log(
              "[Sandock] Kill command result:",
              killResult.data.stdout,
            );
            console.log(
              "[Sandock] Kill command stderr:",
              killResult.data.stderr,
            );
          } catch (err) {
            console.error("[Sandock] Failed to send termination signal:", err);
          }

          done = true;
          error = new Error("Operation aborted");
          error.name = "AbortError";
          resolveWait?.();
        };

        if (signal) {
          console.log("[Sandock] Adding abort signal listener");
          signal.addEventListener("abort", abortHandler);
        } else {
          console.log("[Sandock] No signal provided");
        }

        // Track if we've received any output (indicates proper stream completion)
        let hasReceivedOutput = false;

        // Start shell command with streaming callbacks
        const shellPromise = self.client.sandbox.shell(
          self.sandboxId,
          { cmd, timeoutMs: self.timeout },
          {
            onStdout: (chunk: string) => {
              // Stop producing stdout chunks if signal is aborted
              if (signal?.aborted) return;
              hasReceivedOutput = true;
              queue.push(new TextEncoder().encode(chunk));
              resolveWait?.();
            },
            onStderr: (chunk: string) => {
              hasReceivedOutput = true;
              queue.push(new TextEncoder().encode(chunk));
              resolveWait?.();
            },
            onError: (err: unknown) => {
              console.log("[Sandock] SHELL ERROR:", err);
              // Only set error if:
              // 1. We haven't received any output (process failed before communicating)
              // 2. The stream isn't already done
              // If we received output, the process communicated its error properly
              // and we shouldn't override it with a generic "process exited" error
              if (!hasReceivedOutput && !done) {
                error = err instanceof Error ? err : new Error(String(err));
              }
              resolveWait?.();
            },
          },
        );

        // Handle completion
        shellPromise
          .then(
            (result: {
              success: boolean;
              data: {
                exitCode: number | null;
                stdout: string;
                stderr: string;
                timedOut: boolean;
                durationMs: number;
              };
            }) => {
              // Check for errors in the result
              if (result.data.timedOut) {
                error = new Error(
                  `Command timed out after ${result.data.durationMs}ms`,
                );
              } else if (
                result.data.exitCode !== 0 &&
                result.data.exitCode !== null
              ) {
                console.warn(
                  `Command exited with code ${result.data.exitCode}`,
                );
              }
              done = true;
              resolveWait?.();
            },
          )
          .catch((err: unknown) => {
            // Only set error if we haven't received any output
            // If we received output, the process communicated its error properly
            if (!hasReceivedOutput) {
              error = err instanceof Error ? err : new Error(String(err));
            }
            // Log AbortError appropriately
            if (err instanceof Error && err.name === "AbortError") {
              console.log("[Sandock] Command execution aborted by user");
            } else {
              console.error("[Sandock] Shell promise rejected:", err);
            }
            done = true;
            resolveWait?.();
          })
          .finally(() => {
            if (signal) {
              signal.removeEventListener("abort", abortHandler);
            }
            // When keep is false, stop and delete sandbox after execution (default keep=true ~30 min retention)
            if (!self.keep) {
              self.client.sandbox
                .stop(self.sandboxId)
                .then(() => self.client.sandbox.delete(self.sandboxId))
                .catch((e) =>
                  console.error(
                    "[Sandock] Failed to stop/delete sandbox after execution:",
                    e,
                  ),
                );
            }
          });

        // Yield chunks as they arrive
        while (true) {
          // Check if signal is aborted
          if (signal?.aborted) {
            break;
          }

          // Yield all queued chunks
          while (queue.length > 0) {
            const chunk = queue.shift();
            if (chunk) {
              yield chunk;
            }
          }

          // Check for errors
          if (error) {
            throw error;
          }

          // Check if done
          if (done) {
            break;
          }

          // Wait for more data
          await new Promise<void>((resolve) => {
            resolveWait = resolve;
          });
        }
      },
    };
  }

  /**
   * Upload files to the sandbox
   */
  async upload(
    files: Array<{ path: string; content: Uint8Array | string }>,
    targetDir: string,
  ): Promise<void> {
    if (files.length === 0) return;
    // Ensure target directory exists (fs.write may not create parent dirs on the volume)
    const escapedDir = targetDir.replace(/'/g, "'\\''");
    const mkdirResult = await this.runCommand(`mkdir -p '${escapedDir}'`);
    if (mkdirResult.exitCode !== 0) {
      console.warn(
        `[Sandock] mkdir -p ${targetDir} failed: ${mkdirResult.stderr}`,
      );
    }
    for (const file of files) {
      const fullPath = `${targetDir}/${file.path}`;

      // Convert content to string
      const content =
        typeof file.content === "string"
          ? file.content
          : new TextDecoder().decode(file.content);

      // Use high-level fs.write API
      await this.client.fs.write(this.sandboxId, fullPath, content);
    }
  }

  async readFile(filePath: string): Promise<string> {
    const result = await this.client.fs.read(this.sandboxId, filePath);
    // Sandock fs.read returns { success: true, data: { path: string, content: string } }
    if (result.success && result.data) {
      return typeof result.data === "string"
        ? result.data
        : (result.data as { content?: string }).content || "";
    }
    throw new Error(`Failed to read file ${filePath}`);
  }

  /**
   * Destroy the sandbox and release resources
   */
  async destroy(): Promise<void> {
    // Stop the sandbox using high-level API
    await this.client.sandbox.stop(this.sandboxId);

    // Delete sandbox using high-level API
    await this.client.sandbox.delete(this.sandboxId);

    this.onDestroy();
  }
}
