<?php
/**
 * PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
 *
 * unsandbox.com PHP SDK (Synchronous)
 *
 * Library Usage:
 *     require_once 'un.php';
 *     use Unsandbox\Unsandbox;
 *
 *     $client = new Unsandbox();
 *
 *     // Execute code synchronously
 *     $result = $client->executeCode('python', 'print("hello")');
 *
 *     // Execute asynchronously
 *     $jobId = $client->executeAsync('javascript', 'console.log("hello")');
 *
 *     // Wait for job completion with exponential backoff
 *     $result = $client->waitForJob($jobId);
 *
 *     // List all jobs
 *     $jobs = $client->listJobs();
 *
 *     // Get supported languages (cached for 1 hour)
 *     $languages = $client->getLanguages();
 *
 *     // Snapshot operations
 *     $snapshotId = $client->sessionSnapshot($sessionId);
 *
 * Authentication Priority (4-tier):
 *     1. Method arguments (publicKey, secretKey)
 *     2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
 *     3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
 *     4. Local directory (./accounts.csv, line 0 by default)
 *
 * Request Authentication (HMAC-SHA256):
 *     Authorization: Bearer <public_key>
 *     X-Timestamp: <unix_seconds>
 *     X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
 *
 * Languages Cache:
 *     - Cached in ~/.unsandbox/languages.json
 *     - TTL: 1 hour
 *     - Updated on successful API calls
 */

namespace Unsandbox;

/**
 * Exception thrown when credentials cannot be found or are invalid.
 */
class CredentialsException extends \Exception {}

/**
 * Exception thrown when an API request fails.
 */
class ApiException extends \Exception {
    private ?array $response;

    public function __construct(string $message, int $code = 0, ?array $response = null, ?\Throwable $previous = null) {
        parent::__construct($message, $code, $previous);
        $this->response = $response;
    }

    public function getResponse(): ?array {
        return $this->response;
    }
}

/**
 * Unsandbox PHP SDK - Synchronous Client
 *
 * Provides methods to execute code, manage jobs, and handle snapshots
 * using the unsandbox.com API.
 */
class Unsandbox {
    private const API_BASE = 'https://api.unsandbox.com';
    private const LANGUAGES_CACHE_TTL = 3600; // 1 hour
    private const POLL_DELAYS_MS = [300, 450, 700, 900, 650, 1600, 2000];

    /**
     * Language detection mapping (file extension -> language)
     */
    private const LANGUAGE_MAP = [
        'py' => 'python',
        'js' => 'javascript',
        'ts' => 'typescript',
        'rb' => 'ruby',
        'php' => 'php',
        'pl' => 'perl',
        'sh' => 'bash',
        'r' => 'r',
        'R' => 'r',
        'lua' => 'lua',
        'go' => 'go',
        'rs' => 'rust',
        'c' => 'c',
        'cpp' => 'cpp',
        'cc' => 'cpp',
        'cxx' => 'cpp',
        'java' => 'java',
        'kt' => 'kotlin',
        'm' => 'objc',
        'cs' => 'csharp',
        'fs' => 'fsharp',
        'hs' => 'haskell',
        'ml' => 'ocaml',
        'clj' => 'clojure',
        'scm' => 'scheme',
        'ss' => 'scheme',
        'erl' => 'erlang',
        'ex' => 'elixir',
        'exs' => 'elixir',
        'jl' => 'julia',
        'd' => 'd',
        'nim' => 'nim',
        'zig' => 'zig',
        'v' => 'v',
        'cr' => 'crystal',
        'dart' => 'dart',
        'groovy' => 'groovy',
        'f90' => 'fortran',
        'f95' => 'fortran',
        'lisp' => 'commonlisp',
        'lsp' => 'commonlisp',
        'cob' => 'cobol',
        'tcl' => 'tcl',
        'raku' => 'raku',
        'pro' => 'prolog',
        'p' => 'prolog',
        '4th' => 'forth',
        'forth' => 'forth',
        'fth' => 'forth',
    ];

    private ?string $defaultPublicKey = null;
    private ?string $defaultSecretKey = null;
    private int $accountIndex = 0;

    /**
     * Create a new Unsandbox client.
     *
     * @param string|null $publicKey Default public key (optional)
     * @param string|null $secretKey Default secret key (optional)
     * @param int $accountIndex Account index for CSV files (default: 0)
     */
    public function __construct(?string $publicKey = null, ?string $secretKey = null, int $accountIndex = 0) {
        $this->defaultPublicKey = $publicKey;
        $this->defaultSecretKey = $secretKey;
        $this->accountIndex = $accountIndex;
    }

    /**
     * Execute code synchronously (awaits until completion).
     *
     * @param string $language Programming language (e.g., "python", "javascript", "go")
     * @param string $code Source code to execute
     * @param string|null $publicKey Optional API key (uses credentials resolution if not provided)
     * @param string|null $secretKey Optional API secret (uses credentials resolution if not provided)
     * @return array Response array containing stdout, stderr, exit code, etc.
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function executeCode(string $language, string $code, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $response = $this->makeRequest(
            'POST',
            '/execute',
            $publicKey,
            $secretKey,
            ['language' => $language, 'code' => $code]
        );

        // If we got a job_id, poll until completion
        $jobId = $response['job_id'] ?? null;
        $status = $response['status'] ?? null;

        if ($jobId && in_array($status, ['pending', 'running'], true)) {
            return $this->waitForJob($jobId, $publicKey, $secretKey);
        }

        return $response;
    }

    /**
     * Execute code asynchronously (returns immediately with job_id).
     *
     * @param string $language Programming language (e.g., "python", "javascript")
     * @param string $code Source code to execute
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return string Job ID string
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function executeAsync(string $language, string $code, ?string $publicKey = null, ?string $secretKey = null): string {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $response = $this->makeRequest(
            'POST',
            '/execute',
            $publicKey,
            $secretKey,
            ['language' => $language, 'code' => $code]
        );

        return $response['job_id'] ?? '';
    }

    /**
     * Get current status/result of a job (single poll, no waiting).
     *
     * @param string $jobId Job ID from executeAsync()
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Job response array
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function getJob(string $jobId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('GET', "/jobs/{$jobId}", $publicKey, $secretKey);
    }

    /**
     * Wait for job completion with exponential backoff polling.
     *
     * Polling delays (ms): [300, 450, 700, 900, 650, 1600, 2000, ...]
     * Cumulative: 300, 750, 1450, 2350, 3000, 4600, 6600ms+
     *
     * @param string $jobId Job ID from executeAsync()
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @param int $timeout Maximum wait time in seconds (default: 3600)
     * @return array Final job result when status is terminal (completed, failed, timeout, cancelled)
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed or timeout exceeded
     */
    public function waitForJob(string $jobId, ?string $publicKey = null, ?string $secretKey = null, int $timeout = 3600): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        $pollCount = 0;
        $startTime = time();

        while (true) {
            // Check timeout
            if ((time() - $startTime) >= $timeout) {
                throw new ApiException("Timeout waiting for job {$jobId}");
            }

            // Sleep before polling
            $delayIdx = min($pollCount, count(self::POLL_DELAYS_MS) - 1);
            usleep(self::POLL_DELAYS_MS[$delayIdx] * 1000);
            $pollCount++;

            $response = $this->getJob($jobId, $publicKey, $secretKey);
            $status = $response['status'] ?? null;

            if (in_array($status, ['completed', 'failed', 'timeout', 'cancelled'], true)) {
                return $response;
            }

            // Still running, continue polling
        }
    }

    /**
     * Cancel a running job.
     *
     * @param string $jobId Job ID to cancel
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with cancellation confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function cancelJob(string $jobId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('DELETE', "/jobs/{$jobId}", $publicKey, $secretKey);
    }

    /**
     * List all jobs for the authenticated account.
     *
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array List of job arrays
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function listJobs(?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        $response = $this->makeRequest('GET', '/jobs', $publicKey, $secretKey);
        return $response['jobs'] ?? [];
    }

    /**
     * Get list of supported programming languages.
     *
     * Results are cached for 1 hour in ~/.unsandbox/languages.json
     *
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array List of language identifiers (e.g., ["python", "javascript", "go", ...])
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function getLanguages(?string $publicKey = null, ?string $secretKey = null): array {
        // Try cache first
        $cached = $this->loadLanguagesCache();
        if ($cached !== null) {
            return $cached;
        }

        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        $response = $this->makeRequest('GET', '/languages', $publicKey, $secretKey);
        $languages = $response['languages'] ?? [];

        // Cache the result
        $this->saveLanguagesCache($languages);
        return $languages;
    }

    /**
     * Detect programming language from filename extension.
     *
     * @param string $filename Filename to detect language from (e.g., "script.py")
     * @return string|null Language identifier (e.g., "python") or null if unknown
     */
    public static function detectLanguage(string $filename): ?string {
        if (empty($filename) || strpos($filename, '.') === false) {
            return null;
        }

        $parts = explode('.', $filename);
        $ext = end($parts);

        return self::LANGUAGE_MAP[$ext] ?? null;
    }

    /**
     * Create a snapshot of a session.
     *
     * @param string $sessionId Session ID to snapshot
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @param string|null $name Optional snapshot name
     * @param bool $ephemeral If true, snapshot is ephemeral (hot snapshot)
     * @return string Snapshot ID
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function sessionSnapshot(
        string $sessionId,
        ?string $publicKey = null,
        ?string $secretKey = null,
        ?string $name = null,
        bool $ephemeral = false
    ): string {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $data = ['session_id' => $sessionId, 'hot' => $ephemeral];
        if ($name !== null) {
            $data['name'] = $name;
        }

        $response = $this->makeRequest('POST', '/snapshots', $publicKey, $secretKey, $data);
        return $response['snapshot_id'] ?? '';
    }

    /**
     * Create a snapshot of a service.
     *
     * @param string $serviceId Service ID to snapshot
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @param string|null $name Optional snapshot name
     * @return string Snapshot ID
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function serviceSnapshot(
        string $serviceId,
        ?string $publicKey = null,
        ?string $secretKey = null,
        ?string $name = null
    ): string {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $data = ['service_id' => $serviceId];
        if ($name !== null) {
            $data['name'] = $name;
        }

        $response = $this->makeRequest('POST', '/snapshots', $publicKey, $secretKey, $data);
        return $response['snapshot_id'] ?? '';
    }

    /**
     * List all snapshots.
     *
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array List of snapshot arrays
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function listSnapshots(?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        $response = $this->makeRequest('GET', '/snapshots', $publicKey, $secretKey);
        return $response['snapshots'] ?? [];
    }

    /**
     * Restore a snapshot.
     *
     * @param string $snapshotId Snapshot ID to restore
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with restored resource info
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function restoreSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/snapshots/{$snapshotId}/restore", $publicKey, $secretKey, []);
    }

    /**
     * Delete a snapshot.
     *
     * @param string $snapshotId Snapshot ID to delete
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with deletion confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function deleteSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('DELETE', "/snapshots/{$snapshotId}", $publicKey, $secretKey);
    }

    /**
     * Lock a snapshot to prevent deletion.
     *
     * @param string $snapshotId Snapshot ID to lock
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with lock confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function lockSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/snapshots/{$snapshotId}/lock", $publicKey, $secretKey, []);
    }

    /**
     * Unlock a snapshot to allow deletion.
     *
     * @param string $snapshotId Snapshot ID to unlock
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with unlock confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function unlockSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/snapshots/{$snapshotId}/unlock", $publicKey, $secretKey, []);
    }

    /**
     * Clone a snapshot to create a new session or service.
     *
     * @param string $snapshotId Snapshot ID to clone
     * @param string|null $name Optional name for the new resource
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @param array $opts Optional parameters: 'type' (session|service), 'shell', 'ports'
     * @return array Response array with cloned resource info
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function cloneSnapshot(
        string $snapshotId,
        ?string $name = null,
        ?string $publicKey = null,
        ?string $secretKey = null,
        array $opts = []
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $data = [];
        if ($name !== null) {
            $data['name'] = $name;
        }
        if (isset($opts['type'])) {
            $data['type'] = $opts['type'];
        }
        if (isset($opts['shell'])) {
            $data['shell'] = $opts['shell'];
        }
        if (isset($opts['ports'])) {
            $data['ports'] = $opts['ports'];
        }

        return $this->makeRequest('POST', "/snapshots/{$snapshotId}/clone", $publicKey, $secretKey, $data);
    }

    // =========================================================================
    // Images Methods (LXD Container Images)
    // =========================================================================

    /**
     * Publish a new image from a session or service container.
     *
     * @param string $sourceType Source type: "session" or "service"
     * @param string $sourceId Session ID or Service ID to publish from
     * @param string|null $name Optional name for the image
     * @param string|null $description Optional description for the image
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with image_id and image details
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function imagePublish(
        string $sourceType,
        string $sourceId,
        ?string $name = null,
        ?string $description = null,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $data = [
            'source_type' => $sourceType,
            'source_id' => $sourceId,
        ];
        if ($name !== null) {
            $data['name'] = $name;
        }
        if ($description !== null) {
            $data['description'] = $description;
        }

        return $this->makeRequest('POST', '/images', $publicKey, $secretKey, $data);
    }

    /**
     * List all images owned by the authenticated account.
     *
     * @param string|null $filterType Optional filter: "owned", "shared", "public", or null for all
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array List of image arrays
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function listImages(?string $filterType = null, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $path = $filterType !== null ? "/images/{$filterType}" : '/images';
        $response = $this->makeRequest('GET', $path, $publicKey, $secretKey);
        return $response['images'] ?? [];
    }

    /**
     * Get details of a specific image.
     *
     * @param string $imageId Image ID
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Image details
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function getImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('GET', "/images/{$imageId}", $publicKey, $secretKey);
    }

    /**
     * Delete an image.
     *
     * @param string $imageId Image ID to delete
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with deletion confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function deleteImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('DELETE', "/images/{$imageId}", $publicKey, $secretKey);
    }

    /**
     * Lock an image to prevent deletion.
     *
     * @param string $imageId Image ID to lock
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with lock confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function lockImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/images/{$imageId}/lock", $publicKey, $secretKey, []);
    }

    /**
     * Unlock an image to allow deletion.
     *
     * @param string $imageId Image ID to unlock
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with unlock confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function unlockImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/images/{$imageId}/unlock", $publicKey, $secretKey, []);
    }

    /**
     * Set visibility of an image.
     *
     * @param string $imageId Image ID
     * @param string $visibility Visibility level: "private" or "public"
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with visibility update confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function setImageVisibility(
        string $imageId,
        string $visibility,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/images/{$imageId}/visibility", $publicKey, $secretKey, [
            'visibility' => $visibility,
        ]);
    }

    /**
     * Grant access to an image for another API key.
     *
     * @param string $imageId Image ID
     * @param string $trustedApiKey The API key to grant access to
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with grant confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function grantImageAccess(
        string $imageId,
        string $trustedApiKey,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/images/{$imageId}/grant", $publicKey, $secretKey, [
            'trusted_api_key' => $trustedApiKey,
        ]);
    }

    /**
     * Revoke access to an image from another API key.
     *
     * @param string $imageId Image ID
     * @param string $trustedApiKey The API key to revoke access from
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with revoke confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function revokeImageAccess(
        string $imageId,
        string $trustedApiKey,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/images/{$imageId}/revoke", $publicKey, $secretKey, [
            'trusted_api_key' => $trustedApiKey,
        ]);
    }

    /**
     * List API keys that have been granted access to an image.
     *
     * @param string $imageId Image ID
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array List of trusted API key information
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function listImageTrusted(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        $response = $this->makeRequest('GET', "/images/{$imageId}/trusted", $publicKey, $secretKey);
        return $response['trusted'] ?? [];
    }

    /**
     * Transfer ownership of an image to another API key.
     *
     * @param string $imageId Image ID to transfer
     * @param string $toApiKey The API key to transfer ownership to
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with transfer confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function transferImage(
        string $imageId,
        string $toApiKey,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/images/{$imageId}/transfer", $publicKey, $secretKey, [
            'to_api_key' => $toApiKey,
        ]);
    }

    /**
     * Spawn a new service from an image.
     *
     * @param string $imageId Image ID to spawn from
     * @param string|null $name Optional service name
     * @param array|string|null $ports Optional port(s) to expose (array of ints or comma-separated string)
     * @param string|null $bootstrap Optional bootstrap command or URL
     * @param string $networkMode Network mode: "zerotrust" or "semitrusted"
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with spawned service info
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function spawnFromImage(
        string $imageId,
        ?string $name = null,
        $ports = null,
        ?string $bootstrap = null,
        string $networkMode = 'zerotrust',
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $data = [
            'network_mode' => $networkMode,
        ];
        if ($name !== null) {
            $data['name'] = $name;
        }
        if ($ports !== null) {
            // Convert ports to array if string
            if (is_string($ports)) {
                $ports = array_map('intval', explode(',', $ports));
            }
            $data['ports'] = $ports;
        }
        if ($bootstrap !== null) {
            $data['bootstrap'] = $bootstrap;
        }

        return $this->makeRequest('POST', "/images/{$imageId}/spawn", $publicKey, $secretKey, $data);
    }

    /**
     * Clone an image to create a new image with a different name/description.
     *
     * @param string $imageId Image ID to clone
     * @param string|null $name Optional name for the cloned image
     * @param string|null $description Optional description for the cloned image
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with cloned image info
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function cloneImage(
        string $imageId,
        ?string $name = null,
        ?string $description = null,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $data = [];
        if ($name !== null) {
            $data['name'] = $name;
        }
        if ($description !== null) {
            $data['description'] = $description;
        }

        return $this->makeRequest('POST', "/images/{$imageId}/clone", $publicKey, $secretKey, $data);
    }

    // =========================================================================
    // Session Methods
    // =========================================================================

    /**
     * List all active sessions for the authenticated account.
     *
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array List of session arrays
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function listSessions(?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        $response = $this->makeRequest('GET', '/sessions', $publicKey, $secretKey);
        return $response['sessions'] ?? [];
    }

    /**
     * Get details of a specific session.
     *
     * @param string $sessionId Session ID
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Session details
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function getSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('GET', "/sessions/{$sessionId}", $publicKey, $secretKey);
    }

    /**
     * Create a new interactive session.
     *
     * @param string $language Programming language or shell (e.g., "bash", "python3")
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @param array $opts Optional parameters: 'network_mode', 'ttl', 'shell', 'multiplexer', 'vcpu'
     * @return array Session info including session_id and container_name
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function createSession(
        string $language,
        ?string $publicKey = null,
        ?string $secretKey = null,
        array $opts = []
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $data = [
            'network_mode' => $opts['network_mode'] ?? 'zerotrust',
            'ttl' => $opts['ttl'] ?? 3600,
        ];
        if (!empty($language)) {
            $data['shell'] = $language;
        }
        if (isset($opts['shell'])) {
            $data['shell'] = $opts['shell'];
        }
        if (isset($opts['multiplexer'])) {
            $data['multiplexer'] = $opts['multiplexer'];
        }
        if (isset($opts['vcpu']) && $opts['vcpu'] > 1) {
            $data['vcpu'] = $opts['vcpu'];
        }
        if (isset($opts['input_files'])) {
            $data['input_files'] = $opts['input_files'];
        }

        return $this->makeRequest('POST', '/sessions', $publicKey, $secretKey, $data);
    }

    /**
     * Delete (terminate) a session.
     *
     * @param string $sessionId Session ID to delete
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with deletion confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function deleteSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('DELETE', "/sessions/{$sessionId}", $publicKey, $secretKey);
    }

    /**
     * Freeze a session to pause execution and reduce resource usage.
     *
     * @param string $sessionId Session ID to freeze
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with freeze confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function freezeSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/sessions/{$sessionId}/freeze", $publicKey, $secretKey, []);
    }

    /**
     * Unfreeze a session to resume execution.
     *
     * @param string $sessionId Session ID to unfreeze
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with unfreeze confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function unfreezeSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/sessions/{$sessionId}/unfreeze", $publicKey, $secretKey, []);
    }

    /**
     * Boost a session's resources (increase vCPU, memory is derived: vcpu * 2048MB).
     *
     * @param string $sessionId Session ID to boost
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @param int $vcpu Number of vCPUs (default: 2)
     * @return array Response array with boost confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function boostSession(
        string $sessionId,
        ?string $publicKey = null,
        ?string $secretKey = null,
        int $vcpu = 2
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/sessions/{$sessionId}/boost", $publicKey, $secretKey, ['vcpu' => $vcpu]);
    }

    /**
     * Remove boost from a session (return to base resources).
     *
     * @param string $sessionId Session ID to unboost
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with unboost confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function unboostSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/sessions/{$sessionId}/unboost", $publicKey, $secretKey, []);
    }

    /**
     * Execute a shell command in an active session.
     *
     * Note: This is for one-shot commands. For interactive sessions, use WebSocket connection.
     *
     * @param string $sessionId Session ID
     * @param string $command Command to execute
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with command output
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function shellSession(
        string $sessionId,
        string $command,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/sessions/{$sessionId}/shell", $publicKey, $secretKey, ['command' => $command]);
    }

    // =========================================================================
    // Service Methods
    // =========================================================================

    /**
     * List all services for the authenticated account.
     *
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array List of service arrays
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function listServices(?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        $response = $this->makeRequest('GET', '/services', $publicKey, $secretKey);
        return $response['services'] ?? [];
    }

    /**
     * Create a new persistent service.
     *
     * @param string $name Service name
     * @param array|string $ports Port(s) to expose (array of ints or comma-separated string)
     * @param string $bootstrap Bootstrap command or URL
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @param array $opts Optional parameters: 'network_mode', 'vcpu', 'service_type', 'custom_domains', 'bootstrap_content', 'input_files'
     * @return array Service info including service_id
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function createService(
        string $name,
        $ports,
        string $bootstrap,
        ?string $publicKey = null,
        ?string $secretKey = null,
        array $opts = []
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        // Convert ports to array if string
        if (is_string($ports)) {
            $ports = array_map('intval', explode(',', $ports));
        }

        $data = [
            'name' => $name,
            'ports' => $ports,
            'bootstrap' => $bootstrap,
        ];

        if (isset($opts['network_mode'])) {
            $data['network_mode'] = $opts['network_mode'];
        }
        if (isset($opts['vcpu']) && $opts['vcpu'] > 1) {
            $data['vcpu'] = $opts['vcpu'];
        }
        if (isset($opts['service_type'])) {
            $data['service_type'] = $opts['service_type'];
        }
        if (isset($opts['custom_domains'])) {
            $data['custom_domains'] = $opts['custom_domains'];
        }
        if (isset($opts['bootstrap_content'])) {
            $data['bootstrap_content'] = $opts['bootstrap_content'];
        }
        if (isset($opts['input_files'])) {
            $data['input_files'] = $opts['input_files'];
        }
        if (isset($opts['unfreeze_on_demand']) && $opts['unfreeze_on_demand']) {
            $data['unfreeze_on_demand'] = true;
        }

        return $this->makeRequest('POST', '/services', $publicKey, $secretKey, $data);
    }

    /**
     * Get details of a specific service.
     *
     * @param string $serviceId Service ID
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Service details
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function getService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('GET', "/services/{$serviceId}", $publicKey, $secretKey);
    }

    /**
     * Update a service (e.g., resize vCPU).
     *
     * @param string $serviceId Service ID
     * @param array $opts Update parameters: 'vcpu', 'name', etc.
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Updated service details
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function updateService(
        string $serviceId,
        array $opts,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('PATCH', "/services/{$serviceId}", $publicKey, $secretKey, $opts);
    }

    /**
     * Delete (destroy) a service.
     *
     * @param string $serviceId Service ID to delete
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with deletion confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function deleteService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('DELETE', "/services/{$serviceId}", $publicKey, $secretKey);
    }

    /**
     * Freeze a service to pause execution and reduce resource usage.
     *
     * @param string $serviceId Service ID to freeze
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with freeze confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function freezeService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/services/{$serviceId}/freeze", $publicKey, $secretKey, []);
    }

    /**
     * Unfreeze a service to resume execution.
     *
     * @param string $serviceId Service ID to unfreeze
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with unfreeze confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function unfreezeService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/services/{$serviceId}/unfreeze", $publicKey, $secretKey, []);
    }

    /**
     * Lock a service to prevent deletion.
     *
     * @param string $serviceId Service ID to lock
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with lock confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function lockService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/services/{$serviceId}/lock", $publicKey, $secretKey, []);
    }

    /**
     * Unlock a service to allow deletion.
     *
     * @param string $serviceId Service ID to unlock
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with unlock confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function unlockService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/services/{$serviceId}/unlock", $publicKey, $secretKey, []);
    }

    /**
     * Set unfreeze_on_demand flag for a service.
     *
     * @param string $serviceId Service ID
     * @param bool $enabled Whether to enable unfreeze on demand
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with update confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function setUnfreezeOnDemand(string $serviceId, bool $enabled, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('PATCH', "/services/{$serviceId}", $publicKey, $secretKey, ['unfreeze_on_demand' => $enabled]);
    }

    /**
     * Get bootstrap logs for a service.
     *
     * @param string $serviceId Service ID
     * @param bool $all If true, get all logs; if false, get last 9000 lines
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with log content
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function getServiceLogs(
        string $serviceId,
        bool $all = false,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        $path = "/services/{$serviceId}/logs" . ($all ? '?all=true' : '');
        return $this->makeRequest('GET', $path, $publicKey, $secretKey);
    }

    /**
     * Get environment vault status for a service.
     *
     * @param string $serviceId Service ID
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with vault status (has_vault, count, updated_at)
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function getServiceEnv(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('GET', "/services/{$serviceId}/env", $publicKey, $secretKey);
    }

    /**
     * Set environment vault for a service.
     *
     * @param string $serviceId Service ID
     * @param string $env Environment content in .env format (KEY=VALUE\nKEY2=VALUE2)
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with update confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function setServiceEnv(
        string $serviceId,
        string $env,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequestRaw('PUT', "/services/{$serviceId}/env", $publicKey, $secretKey, $env, 'text/plain');
    }

    /**
     * Delete environment vault for a service.
     *
     * @param string $serviceId Service ID
     * @param array|null $keys Optional specific keys to delete; if null, deletes entire vault
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with deletion confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function deleteServiceEnv(
        string $serviceId,
        ?array $keys = null,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('DELETE', "/services/{$serviceId}/env", $publicKey, $secretKey);
    }

    /**
     * Export environment vault for a service (returns .env format).
     *
     * @param string $serviceId Service ID
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with env content
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function exportServiceEnv(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/services/{$serviceId}/env/export", $publicKey, $secretKey, []);
    }

    /**
     * Redeploy a service (re-run bootstrap script).
     *
     * @param string $serviceId Service ID
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with redeploy confirmation
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, []);
    }

    /**
     * Execute a command in a running service container.
     *
     * @param string $serviceId Service ID
     * @param string $command Command to execute
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @param int $timeout Timeout in milliseconds (default: 30000)
     * @return array Response array with command output (stdout, stderr, exit_code)
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function executeInService(
        string $serviceId,
        string $command,
        ?string $publicKey = null,
        ?string $secretKey = null,
        int $timeout = 30000
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $response = $this->makeRequest('POST', "/services/{$serviceId}/execute", $publicKey, $secretKey, [
            'command' => $command,
            'timeout' => $timeout,
        ]);

        // If we got a job_id, poll until completion
        $jobId = $response['job_id'] ?? null;
        if ($jobId) {
            return $this->waitForJob($jobId, $publicKey, $secretKey);
        }

        return $response;
    }

    // =========================================================================
    // Key Validation
    // =========================================================================

    /**
     * Validate API keys.
     *
     * @param string|null $publicKey Optional API key
     * @param string|null $secretKey Optional API secret
     * @return array Response array with validation result
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function validateKeys(?string $publicKey = null, ?string $secretKey = null): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
        return $this->makeRequest('POST', '/keys/validate', $publicKey, $secretKey, []);
    }

    // =========================================================================
    // Image Generation
    // =========================================================================

    /**
     * Generate images from text prompt using AI.
     *
     * @param string $prompt Text description of the image to generate
     * @param string|null $model Model to use (optional)
     * @param string $size Image size (default: "1024x1024")
     * @param string $quality "standard" or "hd" (default: "standard")
     * @param int $n Number of images to generate (default: 1)
     * @param string|null $publicKey API public key
     * @param string|null $secretKey API secret key
     * @return array Result with 'images' array and 'created_at'
     * @throws CredentialsException Missing credentials
     * @throws ApiException API request failed
     */
    public function image(
        string $prompt,
        ?string $model = null,
        string $size = "1024x1024",
        string $quality = "standard",
        int $n = 1,
        ?string $publicKey = null,
        ?string $secretKey = null
    ): array {
        [$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);

        $payload = [
            'prompt' => $prompt,
            'size' => $size,
            'quality' => $quality,
            'n' => $n,
        ];
        if ($model !== null) {
            $payload['model'] = $model;
        }

        return $this->makeRequest('POST', '/image', $publicKey, $secretKey, $payload);
    }

    /**
     * Get path to ~/.unsandbox directory, creating if necessary.
     *
     * @return string Path to unsandbox directory
     */
    private function getUnsandboxDir(): string {
        $home = getenv('HOME') ?: (getenv('USERPROFILE') ?: '');
        if (empty($home)) {
            $home = posix_getpwuid(posix_getuid())['dir'] ?? '/tmp';
        }

        $dir = $home . '/.unsandbox';
        if (!is_dir($dir)) {
            mkdir($dir, 0700, true);
        }

        return $dir;
    }

    /**
     * Load credentials from a CSV file.
     *
     * @param string $csvPath Path to CSV file
     * @param int $accountIndex Account index (0-based)
     * @return array|null [publicKey, secretKey] or null if not found
     */
    private function loadCredentialsFromCsv(string $csvPath, int $accountIndex = 0): ?array {
        if (!file_exists($csvPath)) {
            return null;
        }

        $handle = fopen($csvPath, 'r');
        if ($handle === false) {
            return null;
        }

        $currentIndex = 0;
        while (($line = fgets($handle)) !== false) {
            $line = trim($line);
            if (empty($line) || $line[0] === '#') {
                continue;
            }

            if ($currentIndex === $accountIndex) {
                $parts = explode(',', $line);
                if (count($parts) >= 2) {
                    $publicKey = trim($parts[0]);
                    $secretKey = trim($parts[1]);
                    fclose($handle);
                    return [$publicKey, $secretKey];
                }
            }
            $currentIndex++;
        }

        fclose($handle);
        return null;
    }

    /**
     * Resolve credentials from 4-tier priority system.
     *
     * Priority:
     *     1. Method arguments
     *     2. Environment variables
     *     3. ~/.unsandbox/accounts.csv
     *     4. ./accounts.csv
     *
     * @param string|null $publicKey Public key from method argument
     * @param string|null $secretKey Secret key from method argument
     * @return array [publicKey, secretKey]
     * @throws CredentialsException If no credentials found
     */
    private function resolveCredentials(?string $publicKey, ?string $secretKey): array {
        // Tier 1: Method arguments
        if (!empty($publicKey) && !empty($secretKey)) {
            return [$publicKey, $secretKey];
        }

        // Use default keys if provided to constructor
        if (!empty($this->defaultPublicKey) && !empty($this->defaultSecretKey)) {
            return [$this->defaultPublicKey, $this->defaultSecretKey];
        }

        // Tier 2: Environment variables
        $envPk = getenv('UNSANDBOX_PUBLIC_KEY');
        $envSk = getenv('UNSANDBOX_SECRET_KEY');
        if (!empty($envPk) && !empty($envSk)) {
            return [$envPk, $envSk];
        }

        // Determine account index
        $accountIndex = $this->accountIndex;
        $envAccount = getenv('UNSANDBOX_ACCOUNT');
        if ($envAccount !== false && $envAccount !== '') {
            $accountIndex = (int)$envAccount;
        }

        // Tier 3: ~/.unsandbox/accounts.csv
        $unsandboxDir = $this->getUnsandboxDir();
        $creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', $accountIndex);
        if ($creds !== null) {
            return $creds;
        }

        // Tier 4: ./accounts.csv
        $creds = $this->loadCredentialsFromCsv('./accounts.csv', $accountIndex);
        if ($creds !== null) {
            return $creds;
        }

        throw new CredentialsException(
            "No credentials found. Please provide via:\n" .
            "  1. Method arguments (publicKey, secretKey)\n" .
            "  2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" .
            "  3. ~/.unsandbox/accounts.csv\n" .
            "  4. ./accounts.csv"
        );
    }

    /**
     * Sign a request using HMAC-SHA256.
     *
     * Message format: "timestamp:METHOD:path:body"
     *
     * @param string $secretKey Secret key for signing
     * @param int $timestamp Unix timestamp
     * @param string $method HTTP method
     * @param string $path API endpoint path
     * @param string|null $body Request body (optional)
     * @return string 64-character hex signature
     */
    private function signRequest(string $secretKey, int $timestamp, string $method, string $path, ?string $body = null): string {
        $bodyStr = $body ?? '';
        $message = "{$timestamp}:{$method}:{$path}:{$bodyStr}";
        return hash_hmac('sha256', $message, $secretKey);
    }

    /**
     * Make an authenticated HTTP request to the API.
     *
     * @param string $method HTTP method (GET, POST, DELETE)
     * @param string $path API endpoint path
     * @param string $publicKey API public key
     * @param string $secretKey API secret key
     * @param array|null $data Request data (optional)
     * @return array Decoded JSON response
     * @throws ApiException On network errors or non-2xx response
     */
    private function makeRequest(string $method, string $path, string $publicKey, string $secretKey, ?array $data = null): array {
        $url = self::API_BASE . $path;
        $timestamp = time();
        $body = $data !== null ? json_encode($data) : '';

        $signature = $this->signRequest($secretKey, $timestamp, $method, $path, $data !== null ? $body : null);

        $headers = [
            'Authorization: Bearer ' . $publicKey,
            'X-Timestamp: ' . $timestamp,
            'X-Signature: ' . $signature,
            'Content-Type: application/json',
        ];

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_TIMEOUT, 120);

        switch ($method) {
            case 'POST':
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
                break;
            case 'PUT':
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
                curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
                break;
            case 'PATCH':
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
                curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
                break;
            case 'DELETE':
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
                break;
            case 'GET':
            default:
                // GET is the default
                break;
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($response === false) {
            throw new ApiException("cURL error: {$error}");
        }

        $decoded = json_decode($response, true);
        if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
            throw new ApiException("Invalid JSON response: " . json_last_error_msg());
        }

        if ($httpCode >= 400) {
            $errorMessage = $decoded['error'] ?? $decoded['message'] ?? "HTTP {$httpCode}";
            throw new ApiException($errorMessage, $httpCode, $decoded);
        }

        return $decoded;
    }

    /**
     * Make an authenticated HTTP request with raw body content (non-JSON).
     *
     * @param string $method HTTP method (PUT, POST, etc.)
     * @param string $path API endpoint path
     * @param string $publicKey API public key
     * @param string $secretKey API secret key
     * @param string $body Raw request body
     * @param string $contentType Content type header (e.g., 'text/plain')
     * @return array Decoded JSON response
     * @throws ApiException On network errors or non-2xx response
     */
    private function makeRequestRaw(string $method, string $path, string $publicKey, string $secretKey, string $body, string $contentType = 'text/plain'): array {
        $url = self::API_BASE . $path;
        $timestamp = time();

        $signature = $this->signRequest($secretKey, $timestamp, $method, $path, $body);

        $headers = [
            'Authorization: Bearer ' . $publicKey,
            'X-Timestamp: ' . $timestamp,
            'X-Signature: ' . $signature,
            'Content-Type: ' . $contentType,
        ];

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_TIMEOUT, 120);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($response === false) {
            throw new ApiException("cURL error: {$error}");
        }

        $decoded = json_decode($response, true);
        if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
            throw new ApiException("Invalid JSON response: " . json_last_error_msg());
        }

        if ($httpCode >= 400) {
            $errorMessage = $decoded['error'] ?? $decoded['message'] ?? "HTTP {$httpCode}";
            throw new ApiException($errorMessage, $httpCode, $decoded);
        }

        return $decoded;
    }

    /**
     * Get path to languages cache file.
     *
     * @return string Path to cache file
     */
    private function getLanguagesCachePath(): string {
        return $this->getUnsandboxDir() . '/languages.json';
    }

    /**
     * Load languages from cache if valid (< 1 hour old).
     *
     * @return array|null List of languages or null if cache invalid
     */
    private function loadLanguagesCache(): ?array {
        $cachePath = $this->getLanguagesCachePath();
        if (!file_exists($cachePath)) {
            return null;
        }

        $mtime = filemtime($cachePath);
        $ageSeconds = time() - $mtime;
        if ($ageSeconds >= self::LANGUAGES_CACHE_TTL) {
            return null;
        }

        $content = file_get_contents($cachePath);
        if ($content === false) {
            return null;
        }

        $data = json_decode($content, true);
        if ($data === null) {
            return null;
        }

        return $data['languages'] ?? null;
    }

    /**
     * Save languages to cache.
     *
     * @param array $languages List of languages
     */
    private function saveLanguagesCache(array $languages): void {
        $cachePath = $this->getLanguagesCachePath();
        $data = [
            'languages' => $languages,
            'timestamp' => time(),
        ];
        file_put_contents($cachePath, json_encode($data));
    }

    // =========================================================================
    // CLI Methods
    // =========================================================================

    /**
     * Main CLI entry point.
     *
     * @param array $argv Command line arguments
     */
    public function cliMain(array $argv): void {
        $script = array_shift($argv);

        if (empty($argv)) {
            $this->cliShowHelp();
            exit(0);
        }

        // Parse global options and determine command
        $globalOpts = $this->cliParseGlobalOptions($argv);
        $args = $globalOpts['args'];
        $opts = $globalOpts['opts'];

        // Handle help flag
        if ($opts['help']) {
            $this->cliShowHelp();
            exit(0);
        }

        // Set credentials from options
        if (!empty($opts['public_key'])) {
            $this->defaultPublicKey = $opts['public_key'];
        }
        if (!empty($opts['secret_key'])) {
            $this->defaultSecretKey = $opts['secret_key'];
        }

        // Determine the command
        if (empty($args)) {
            $this->cliShowHelp();
            exit(0);
        }

        $command = $args[0];

        try {
            switch ($command) {
                case 'session':
                    $this->cliHandleSession(array_slice($args, 1), $opts);
                    break;
                case 'service':
                    $this->cliHandleService(array_slice($args, 1), $opts);
                    break;
                case 'snapshot':
                    $this->cliHandleSnapshot(array_slice($args, 1), $opts);
                    break;
                case 'image':
                    $this->cliHandleImage(array_slice($args, 1), $opts);
                    break;
                case 'key':
                    $this->cliHandleKey($opts);
                    break;
                case 'languages':
                    $this->cliHandleLanguages(array_slice($args, 1), $opts);
                    break;
                case '-h':
                case '--help':
                case 'help':
                    $this->cliShowHelp();
                    break;
                default:
                    // Default: execute code file or inline code
                    $this->cliHandleExecute($args, $opts);
                    break;
            }
        } catch (CredentialsException $e) {
            $this->cliError("Authentication error: " . $e->getMessage());
            exit(3);
        } catch (ApiException $e) {
            $this->cliError("API error: " . $e->getMessage());
            exit(4);
        } catch (\Exception $e) {
            $this->cliError("Error: " . $e->getMessage());
            exit(1);
        }
    }

    /**
     * Parse global CLI options.
     *
     * @param array $argv Arguments to parse
     * @return array ['opts' => [...], 'args' => [...]]
     */
    private function cliParseGlobalOptions(array $argv): array {
        $opts = [
            'shell' => null,
            'env' => [],
            'files' => [],
            'files_path' => [],
            'artifacts' => false,
            'output' => null,
            'public_key' => null,
            'secret_key' => null,
            'network' => 'zerotrust',
            'vcpu' => 1,
            'yes' => false,
            'help' => false,
        ];
        $args = [];

        $i = 0;
        while ($i < count($argv)) {
            $arg = $argv[$i];

            if ($arg === '-s' || $arg === '--shell') {
                $i++;
                $opts['shell'] = $argv[$i] ?? null;
            } elseif ($arg === '-e' || $arg === '--env') {
                $i++;
                if (isset($argv[$i])) {
                    $opts['env'][] = $argv[$i];
                }
            } elseif ($arg === '-f' || $arg === '--file') {
                $i++;
                if (isset($argv[$i])) {
                    $opts['files'][] = $argv[$i];
                }
            } elseif ($arg === '-F' || $arg === '--file-path') {
                $i++;
                if (isset($argv[$i])) {
                    $opts['files_path'][] = $argv[$i];
                }
            } elseif ($arg === '-a' || $arg === '--artifacts') {
                $opts['artifacts'] = true;
            } elseif ($arg === '-o' || $arg === '--output') {
                $i++;
                $opts['output'] = $argv[$i] ?? null;
            } elseif ($arg === '-p' || $arg === '--public-key') {
                $i++;
                $opts['public_key'] = $argv[$i] ?? null;
            } elseif ($arg === '-k' || $arg === '--secret-key') {
                $i++;
                $opts['secret_key'] = $argv[$i] ?? null;
            } elseif ($arg === '-n' || $arg === '--network') {
                $i++;
                $opts['network'] = $argv[$i] ?? 'zerotrust';
            } elseif ($arg === '-v' || $arg === '--vcpu') {
                $i++;
                $opts['vcpu'] = (int)($argv[$i] ?? 1);
            } elseif ($arg === '-y' || $arg === '--yes') {
                $opts['yes'] = true;
            } elseif ($arg === '-h' || $arg === '--help') {
                $opts['help'] = true;
            } elseif (strpos($arg, '-') !== 0) {
                $args[] = $arg;
            }

            $i++;
        }

        return ['opts' => $opts, 'args' => $args];
    }

    /**
     * Handle execute command (default command).
     *
     * @param array $args Positional arguments
     * @param array $opts Options
     */
    private function cliHandleExecute(array $args, array $opts): void {
        $code = null;
        $language = null;

        // If shell option is set, treat first arg as code
        if (!empty($opts['shell'])) {
            $language = $opts['shell'];
            $code = $args[0] ?? '';
        } else {
            // First arg is a file
            $file = $args[0] ?? '';
            if (empty($file)) {
                $this->cliError("No source file specified");
                exit(2);
            }

            if (!file_exists($file)) {
                $this->cliError("File not found: {$file}");
                exit(2);
            }

            $code = file_get_contents($file);
            if ($code === false) {
                $this->cliError("Cannot read file: {$file}");
                exit(2);
            }

            $language = self::detectLanguage($file);
            if ($language === null) {
                $this->cliError("Cannot detect language from file extension: {$file}");
                exit(2);
            }
        }

        // Build request options
        $requestOpts = [
            'network_mode' => $opts['network'],
        ];
        if ($opts['vcpu'] > 1) {
            $requestOpts['vcpu'] = $opts['vcpu'];
        }

        // Handle environment variables
        $envVars = [];
        foreach ($opts['env'] as $envPair) {
            $pos = strpos($envPair, '=');
            if ($pos !== false) {
                $key = substr($envPair, 0, $pos);
                $val = substr($envPair, $pos + 1);
                $envVars[$key] = $val;
            }
        }

        // Handle input files
        $inputFiles = [];
        foreach ($opts['files'] as $filepath) {
            if (file_exists($filepath)) {
                $content = file_get_contents($filepath);
                $inputFiles[] = [
                    'name' => basename($filepath),
                    'content' => base64_encode($content),
                ];
            }
        }
        foreach ($opts['files_path'] as $filepath) {
            if (file_exists($filepath)) {
                $content = file_get_contents($filepath);
                $inputFiles[] = [
                    'name' => $filepath,
                    'content' => base64_encode($content),
                    'preserve_path' => true,
                ];
            }
        }

        $result = $this->executeCode($language, $code);

        // Output result
        if (isset($result['stdout'])) {
            echo $result['stdout'];
        }
        if (isset($result['stderr']) && !empty($result['stderr'])) {
            fwrite(STDERR, $result['stderr']);
        }
        echo "---\n";
        echo "Exit code: " . ($result['exit_code'] ?? 0) . "\n";
        if (isset($result['execution_time'])) {
            echo "Execution time: " . $result['execution_time'] . "ms\n";
        }

        exit($result['exit_code'] ?? 0);
    }

    /**
     * Handle session subcommand.
     *
     * @param array $args Positional arguments
     * @param array $opts Options
     */
    private function cliHandleSession(array $args, array $opts): void {
        // Parse session-specific options
        $sessionOpts = $this->cliParseSessionOptions($args);

        if ($sessionOpts['list']) {
            $sessions = $this->listSessions();
            $this->cliPrintList($sessions, 'session');
            return;
        }

        if ($sessionOpts['attach']) {
            $session = $this->getSession($sessionOpts['attach']);
            $this->cliPrintSessionInfo($session);
            echo "\nNote: Interactive attachment requires WebSocket support\n";
            return;
        }

        if ($sessionOpts['kill']) {
            $result = $this->deleteSession($sessionOpts['kill']);
            echo "Session terminated: " . $sessionOpts['kill'] . "\n";
            return;
        }

        if ($sessionOpts['freeze']) {
            $result = $this->freezeSession($sessionOpts['freeze']);
            echo "Session frozen: " . $sessionOpts['freeze'] . "\n";
            return;
        }

        if ($sessionOpts['unfreeze']) {
            $result = $this->unfreezeSession($sessionOpts['unfreeze']);
            echo "Session unfrozen: " . $sessionOpts['unfreeze'] . "\n";
            return;
        }

        if ($sessionOpts['boost']) {
            $result = $this->boostSession($sessionOpts['boost'], null, null, $opts['vcpu'] ?: 2);
            echo "Session boosted: " . $sessionOpts['boost'] . "\n";
            return;
        }

        if ($sessionOpts['unboost']) {
            $result = $this->unboostSession($sessionOpts['unboost']);
            echo "Session unboost: " . $sessionOpts['unboost'] . "\n";
            return;
        }

        if ($sessionOpts['snapshot']) {
            $snapshotId = $this->sessionSnapshot(
                $sessionOpts['snapshot'],
                null,
                null,
                $sessionOpts['snapshot_name'],
                $sessionOpts['hot']
            );
            echo "Snapshot created: {$snapshotId}\n";
            return;
        }

        // Create new session
        $createOpts = [
            'network_mode' => $opts['network'],
        ];
        if (!empty($sessionOpts['shell'])) {
            $createOpts['shell'] = $sessionOpts['shell'];
        }
        if ($sessionOpts['tmux']) {
            $createOpts['multiplexer'] = 'tmux';
        } elseif ($sessionOpts['screen']) {
            $createOpts['multiplexer'] = 'screen';
        }
        if ($opts['vcpu'] > 1) {
            $createOpts['vcpu'] = $opts['vcpu'];
        }

        $session = $this->createSession($sessionOpts['shell'] ?? 'bash', null, null, $createOpts);
        $this->cliPrintSessionInfo($session);
    }

    /**
     * Parse session-specific options.
     *
     * @param array $args Arguments to parse
     * @return array Parsed options
     */
    private function cliParseSessionOptions(array $args): array {
        $opts = [
            'list' => false,
            'attach' => null,
            'kill' => null,
            'freeze' => null,
            'unfreeze' => null,
            'boost' => null,
            'unboost' => null,
            'snapshot' => null,
            'snapshot_name' => null,
            'hot' => false,
            'tmux' => false,
            'screen' => false,
            'shell' => null,
            'audit' => false,
        ];

        $i = 0;
        while ($i < count($args)) {
            $arg = $args[$i];

            if ($arg === '--list' || $arg === '-l') {
                $opts['list'] = true;
            } elseif ($arg === '--attach') {
                $i++;
                $opts['attach'] = $args[$i] ?? null;
            } elseif ($arg === '--kill') {
                $i++;
                $opts['kill'] = $args[$i] ?? null;
            } elseif ($arg === '--freeze') {
                $i++;
                $opts['freeze'] = $args[$i] ?? null;
            } elseif ($arg === '--unfreeze') {
                $i++;
                $opts['unfreeze'] = $args[$i] ?? null;
            } elseif ($arg === '--boost') {
                $i++;
                $opts['boost'] = $args[$i] ?? null;
            } elseif ($arg === '--unboost') {
                $i++;
                $opts['unboost'] = $args[$i] ?? null;
            } elseif ($arg === '--snapshot') {
                $i++;
                $opts['snapshot'] = $args[$i] ?? null;
            } elseif ($arg === '--snapshot-name') {
                $i++;
                $opts['snapshot_name'] = $args[$i] ?? null;
            } elseif ($arg === '--hot') {
                $opts['hot'] = true;
            } elseif ($arg === '--tmux') {
                $opts['tmux'] = true;
            } elseif ($arg === '--screen') {
                $opts['screen'] = true;
            } elseif ($arg === '--shell') {
                $i++;
                $opts['shell'] = $args[$i] ?? null;
            } elseif ($arg === '--audit') {
                $opts['audit'] = true;
            }

            $i++;
        }

        return $opts;
    }

    /**
     * Handle service subcommand.
     *
     * @param array $args Positional arguments
     * @param array $opts Global options
     */
    private function cliHandleService(array $args, array $opts): void {
        // Check for env subcommand
        if (!empty($args) && $args[0] === 'env') {
            $this->cliHandleServiceEnv(array_slice($args, 1), $opts);
            return;
        }

        // Parse service-specific options
        $serviceOpts = $this->cliParseServiceOptions($args);

        if ($serviceOpts['list']) {
            $services = $this->listServices();
            $this->cliPrintList($services, 'service');
            return;
        }

        if ($serviceOpts['info']) {
            $service = $this->getService($serviceOpts['info']);
            $this->cliPrintServiceInfo($service);
            return;
        }

        if ($serviceOpts['logs']) {
            $logs = $this->getServiceLogs($serviceOpts['logs'], true);
            echo $logs['logs'] ?? '';
            return;
        }

        if ($serviceOpts['tail']) {
            $logs = $this->getServiceLogs($serviceOpts['tail'], false);
            echo $logs['logs'] ?? '';
            return;
        }

        if ($serviceOpts['freeze']) {
            $result = $this->freezeService($serviceOpts['freeze']);
            echo "Service frozen: " . $serviceOpts['freeze'] . "\n";
            return;
        }

        if ($serviceOpts['unfreeze']) {
            $result = $this->unfreezeService($serviceOpts['unfreeze']);
            echo "Service unfrozen: " . $serviceOpts['unfreeze'] . "\n";
            return;
        }

        if ($serviceOpts['destroy']) {
            if (!$opts['yes']) {
                fwrite(STDERR, "Warning: This will permanently destroy the service. Use -y to confirm.\n");
                exit(2);
            }
            $result = $this->deleteService($serviceOpts['destroy']);
            echo "Service destroyed: " . $serviceOpts['destroy'] . "\n";
            return;
        }

        if ($serviceOpts['lock']) {
            $result = $this->lockService($serviceOpts['lock']);
            echo "Service locked: " . $serviceOpts['lock'] . "\n";
            return;
        }

        if ($serviceOpts['unlock']) {
            $result = $this->unlockService($serviceOpts['unlock']);
            echo "Service unlocked: " . $serviceOpts['unlock'] . "\n";
            return;
        }

        if ($serviceOpts['execute']) {
            $result = $this->executeInService($serviceOpts['execute'], $serviceOpts['execute_cmd']);
            if (isset($result['stdout'])) {
                echo $result['stdout'];
            }
            if (isset($result['stderr']) && !empty($result['stderr'])) {
                fwrite(STDERR, $result['stderr']);
            }
            exit($result['exit_code'] ?? 0);
        }

        if ($serviceOpts['redeploy']) {
            $result = $this->redeployService($serviceOpts['redeploy']);
            echo "Service redeploying: " . $serviceOpts['redeploy'] . "\n";
            return;
        }

        if ($serviceOpts['snapshot']) {
            $snapshotId = $this->serviceSnapshot($serviceOpts['snapshot'], null, null, $serviceOpts['snapshot_name']);
            echo "Snapshot created: {$snapshotId}\n";
            return;
        }

        if ($serviceOpts['resize']) {
            $result = $this->updateService($serviceOpts['resize'], ['vcpu' => $opts['vcpu']]);
            echo "Service resized: " . $serviceOpts['resize'] . "\n";
            return;
        }

        if ($serviceOpts['set_unfreeze_on_demand']) {
            $enabled = in_array($serviceOpts['set_unfreeze_on_demand_enabled'], ['true', '1'], true);
            $result = $this->setUnfreezeOnDemand($serviceOpts['set_unfreeze_on_demand'], $enabled);
            $status = $enabled ? 'enabled' : 'disabled';
            echo "Unfreeze-on-demand {$status} for service: " . $serviceOpts['set_unfreeze_on_demand'] . "\n";
            return;
        }

        // Create new service
        if (!empty($serviceOpts['name'])) {
            $createOpts = [];
            if (!empty($opts['network'])) {
                $createOpts['network_mode'] = $opts['network'];
            }
            if ($opts['vcpu'] > 1) {
                $createOpts['vcpu'] = $opts['vcpu'];
            }
            if (!empty($serviceOpts['type'])) {
                $createOpts['service_type'] = $serviceOpts['type'];
            }
            if (!empty($serviceOpts['domains'])) {
                $createOpts['custom_domains'] = explode(',', $serviceOpts['domains']);
            }
            if ($serviceOpts['unfreeze_on_demand']) {
                $createOpts['unfreeze_on_demand'] = true;
            }

            // Handle bootstrap from file
            $bootstrap = $serviceOpts['bootstrap'] ?? '';
            if (!empty($serviceOpts['bootstrap_file'])) {
                if (file_exists($serviceOpts['bootstrap_file'])) {
                    $bootstrap = file_get_contents($serviceOpts['bootstrap_file']);
                } else {
                    $this->cliError("Bootstrap file not found: " . $serviceOpts['bootstrap_file']);
                    exit(2);
                }
            }

            // Handle env file
            if (!empty($serviceOpts['env_file'])) {
                if (file_exists($serviceOpts['env_file'])) {
                    // Will be applied after creation
                }
            }

            $service = $this->createService(
                $serviceOpts['name'],
                $serviceOpts['ports'] ?? '80',
                $bootstrap,
                null,
                null,
                $createOpts
            );
            $this->cliPrintServiceInfo($service);
            return;
        }

        $this->cliError("No action specified for service command. Use --list, --info, --name, etc.");
        exit(2);
    }

    /**
     * Parse service-specific options.
     *
     * @param array $args Arguments to parse
     * @return array Parsed options
     */
    private function cliParseServiceOptions(array $args): array {
        $opts = [
            'list' => false,
            'name' => null,
            'ports' => null,
            'domains' => null,
            'type' => null,
            'bootstrap' => null,
            'bootstrap_file' => null,
            'env_file' => null,
            'info' => null,
            'logs' => null,
            'tail' => null,
            'freeze' => null,
            'unfreeze' => null,
            'destroy' => null,
            'lock' => null,
            'unlock' => null,
            'resize' => null,
            'redeploy' => null,
            'execute' => null,
            'execute_cmd' => null,
            'snapshot' => null,
            'snapshot_name' => null,
            'unfreeze_on_demand' => false,
            'set_unfreeze_on_demand' => null,
            'set_unfreeze_on_demand_enabled' => null,
        ];

        $i = 0;
        while ($i < count($args)) {
            $arg = $args[$i];

            if ($arg === '--list' || $arg === '-l') {
                $opts['list'] = true;
            } elseif ($arg === '--name') {
                $i++;
                $opts['name'] = $args[$i] ?? null;
            } elseif ($arg === '--ports') {
                $i++;
                $opts['ports'] = $args[$i] ?? null;
            } elseif ($arg === '--domains') {
                $i++;
                $opts['domains'] = $args[$i] ?? null;
            } elseif ($arg === '--type') {
                $i++;
                $opts['type'] = $args[$i] ?? null;
            } elseif ($arg === '--bootstrap') {
                $i++;
                $opts['bootstrap'] = $args[$i] ?? null;
            } elseif ($arg === '--bootstrap-file') {
                $i++;
                $opts['bootstrap_file'] = $args[$i] ?? null;
            } elseif ($arg === '--env-file') {
                $i++;
                $opts['env_file'] = $args[$i] ?? null;
            } elseif ($arg === '--info') {
                $i++;
                $opts['info'] = $args[$i] ?? null;
            } elseif ($arg === '--logs') {
                $i++;
                $opts['logs'] = $args[$i] ?? null;
            } elseif ($arg === '--tail') {
                $i++;
                $opts['tail'] = $args[$i] ?? null;
            } elseif ($arg === '--freeze') {
                $i++;
                $opts['freeze'] = $args[$i] ?? null;
            } elseif ($arg === '--unfreeze') {
                $i++;
                $opts['unfreeze'] = $args[$i] ?? null;
            } elseif ($arg === '--destroy') {
                $i++;
                $opts['destroy'] = $args[$i] ?? null;
            } elseif ($arg === '--lock') {
                $i++;
                $opts['lock'] = $args[$i] ?? null;
            } elseif ($arg === '--unlock') {
                $i++;
                $opts['unlock'] = $args[$i] ?? null;
            } elseif ($arg === '--resize') {
                $i++;
                $opts['resize'] = $args[$i] ?? null;
            } elseif ($arg === '--redeploy') {
                $i++;
                $opts['redeploy'] = $args[$i] ?? null;
            } elseif ($arg === '--execute') {
                $i++;
                $opts['execute'] = $args[$i] ?? null;
                // Next argument is the command
                $i++;
                $opts['execute_cmd'] = $args[$i] ?? '';
            } elseif ($arg === '--snapshot') {
                $i++;
                $opts['snapshot'] = $args[$i] ?? null;
            } elseif ($arg === '--snapshot-name') {
                $i++;
                $opts['snapshot_name'] = $args[$i] ?? null;
            } elseif ($arg === '--unfreeze-on-demand') {
                $opts['unfreeze_on_demand'] = true;
            } elseif ($arg === '--set-unfreeze-on-demand') {
                $i++;
                $opts['set_unfreeze_on_demand'] = $args[$i] ?? null;
                $i++;
                $opts['set_unfreeze_on_demand_enabled'] = $args[$i] ?? null;
            }

            $i++;
        }

        return $opts;
    }

    /**
     * Handle service env subcommand.
     *
     * @param array $args Positional arguments
     * @param array $opts Global options
     */
    private function cliHandleServiceEnv(array $args, array $opts): void {
        if (empty($args)) {
            $this->cliError("Usage: service env <status|set|export|delete> <service_id>");
            exit(2);
        }

        $action = $args[0];
        $serviceId = $args[1] ?? null;

        if (empty($serviceId)) {
            $this->cliError("Service ID required");
            exit(2);
        }

        switch ($action) {
            case 'status':
                $result = $this->getServiceEnv($serviceId);
                echo "Has vault: " . ($result['has_vault'] ? 'yes' : 'no') . "\n";
                if (isset($result['count'])) {
                    echo "Variables: " . $result['count'] . "\n";
                }
                if (isset($result['updated_at'])) {
                    echo "Updated: " . $result['updated_at'] . "\n";
                }
                break;

            case 'set':
                // Read from stdin or --env-file
                $envContent = '';
                if (stream_isatty(STDIN)) {
                    $this->cliError("Provide env content via stdin or --env-file");
                    exit(2);
                } else {
                    $envContent = file_get_contents('php://stdin');
                }
                $result = $this->setServiceEnv($serviceId, $envContent);
                echo "Environment vault updated\n";
                break;

            case 'export':
                $result = $this->exportServiceEnv($serviceId);
                echo $result['env'] ?? '';
                break;

            case 'delete':
                if (!$opts['yes']) {
                    fwrite(STDERR, "Warning: This will delete the environment vault. Use -y to confirm.\n");
                    exit(2);
                }
                $result = $this->deleteServiceEnv($serviceId);
                echo "Environment vault deleted\n";
                break;

            default:
                $this->cliError("Unknown env action: {$action}");
                exit(2);
        }
    }

    /**
     * Handle snapshot subcommand.
     *
     * @param array $args Positional arguments
     * @param array $opts Global options
     */
    private function cliHandleSnapshot(array $args, array $opts): void {
        $snapshotOpts = $this->cliParseSnapshotOptions($args);

        if ($snapshotOpts['list']) {
            $snapshots = $this->listSnapshots();
            $this->cliPrintList($snapshots, 'snapshot');
            return;
        }

        if ($snapshotOpts['info']) {
            // Get snapshot info via restore endpoint without actually restoring
            $snapshots = $this->listSnapshots();
            foreach ($snapshots as $snapshot) {
                if (($snapshot['snapshot_id'] ?? $snapshot['id'] ?? '') === $snapshotOpts['info']) {
                    $this->cliPrintSnapshotInfo($snapshot);
                    return;
                }
            }
            $this->cliError("Snapshot not found: " . $snapshotOpts['info']);
            exit(4);
        }

        if ($snapshotOpts['delete']) {
            if (!$opts['yes']) {
                fwrite(STDERR, "Warning: This will permanently delete the snapshot. Use -y to confirm.\n");
                exit(2);
            }
            $result = $this->deleteSnapshot($snapshotOpts['delete']);
            echo "Snapshot deleted: " . $snapshotOpts['delete'] . "\n";
            return;
        }

        if ($snapshotOpts['lock']) {
            $result = $this->lockSnapshot($snapshotOpts['lock']);
            echo "Snapshot locked: " . $snapshotOpts['lock'] . "\n";
            return;
        }

        if ($snapshotOpts['unlock']) {
            $result = $this->unlockSnapshot($snapshotOpts['unlock']);
            echo "Snapshot unlocked: " . $snapshotOpts['unlock'] . "\n";
            return;
        }

        if ($snapshotOpts['clone']) {
            $cloneOpts = [];
            if (!empty($snapshotOpts['type'])) {
                $cloneOpts['type'] = $snapshotOpts['type'];
            }
            if (!empty($snapshotOpts['shell'])) {
                $cloneOpts['shell'] = $snapshotOpts['shell'];
            }
            if (!empty($snapshotOpts['ports'])) {
                $cloneOpts['ports'] = explode(',', $snapshotOpts['ports']);
            }

            $result = $this->cloneSnapshot($snapshotOpts['clone'], $snapshotOpts['name'], null, null, $cloneOpts);
            echo "Snapshot cloned:\n";
            echo json_encode($result, JSON_PRETTY_PRINT) . "\n";
            return;
        }

        $this->cliError("No action specified for snapshot command. Use --list, --info, --delete, --clone, etc.");
        exit(2);
    }

    /**
     * Parse snapshot-specific options.
     *
     * @param array $args Arguments to parse
     * @return array Parsed options
     */
    private function cliParseSnapshotOptions(array $args): array {
        $opts = [
            'list' => false,
            'info' => null,
            'delete' => null,
            'lock' => null,
            'unlock' => null,
            'clone' => null,
            'type' => null,
            'name' => null,
            'shell' => null,
            'ports' => null,
        ];

        $i = 0;
        while ($i < count($args)) {
            $arg = $args[$i];

            if ($arg === '--list' || $arg === '-l') {
                $opts['list'] = true;
            } elseif ($arg === '--info') {
                $i++;
                $opts['info'] = $args[$i] ?? null;
            } elseif ($arg === '--delete') {
                $i++;
                $opts['delete'] = $args[$i] ?? null;
            } elseif ($arg === '--lock') {
                $i++;
                $opts['lock'] = $args[$i] ?? null;
            } elseif ($arg === '--unlock') {
                $i++;
                $opts['unlock'] = $args[$i] ?? null;
            } elseif ($arg === '--clone') {
                $i++;
                $opts['clone'] = $args[$i] ?? null;
            } elseif ($arg === '--type') {
                $i++;
                $opts['type'] = $args[$i] ?? null;
            } elseif ($arg === '--name') {
                $i++;
                $opts['name'] = $args[$i] ?? null;
            } elseif ($arg === '--shell') {
                $i++;
                $opts['shell'] = $args[$i] ?? null;
            } elseif ($arg === '--ports') {
                $i++;
                $opts['ports'] = $args[$i] ?? null;
            }

            $i++;
        }

        return $opts;
    }

    /**
     * Handle image subcommand.
     *
     * @param array $args Command arguments
     * @param array $opts Global options
     */
    private function cliHandleImage(array $args, array $opts): void {
        $imageOpts = $this->cliParseImageOptions($args);

        if ($imageOpts['list']) {
            $images = $this->listImages();
            $this->cliPrintImageList($images);
            return;
        }

        if ($imageOpts['info']) {
            $image = $this->getImage($imageOpts['info']);
            $this->cliPrintImageInfo($image);
            return;
        }

        if ($imageOpts['delete']) {
            if (!$opts['yes']) {
                fwrite(STDERR, "Warning: This will permanently delete the image. Use -y to confirm.\n");
                exit(2);
            }
            $result = $this->deleteImage($imageOpts['delete']);
            echo "Image deleted: " . $imageOpts['delete'] . "\n";
            return;
        }

        if ($imageOpts['lock']) {
            $result = $this->lockImage($imageOpts['lock']);
            echo "Image locked: " . $imageOpts['lock'] . "\n";
            return;
        }

        if ($imageOpts['unlock']) {
            $result = $this->unlockImage($imageOpts['unlock']);
            echo "Image unlocked: " . $imageOpts['unlock'] . "\n";
            return;
        }

        if ($imageOpts['publish']) {
            if (empty($imageOpts['sourceType'])) {
                $this->cliError("--source-type required for --publish");
                exit(2);
            }
            $result = $this->imagePublish($imageOpts['sourceType'], $imageOpts['publish'], $imageOpts['name']);
            $imageId = $result['image_id'] ?? $result['id'] ?? '';
            echo "Image published: {$imageId}\n";
            return;
        }

        if ($imageOpts['visibility'] && $imageOpts['visibilityMode']) {
            if (!in_array($imageOpts['visibilityMode'], ['private', 'unlisted', 'public'])) {
                $this->cliError("visibility must be private, unlisted, or public");
                exit(2);
            }
            $result = $this->setImageVisibility($imageOpts['visibility'], $imageOpts['visibilityMode']);
            echo "Image {$imageOpts['visibility']} visibility set to {$imageOpts['visibilityMode']}\n";
            return;
        }

        if ($imageOpts['spawn']) {
            if (empty($imageOpts['name'])) {
                $this->cliError("--name required for --spawn");
                exit(2);
            }
            $ports = null;
            if (!empty($imageOpts['ports'])) {
                $ports = array_map('intval', explode(',', $imageOpts['ports']));
            }
            $result = $this->spawnFromImage($imageOpts['spawn'], $imageOpts['name'], $ports);
            $serviceId = $result['service_id'] ?? $result['id'] ?? '';
            echo "Service spawned: {$serviceId}\n";
            return;
        }

        if ($imageOpts['clone']) {
            $result = $this->cloneImage($imageOpts['clone'], $imageOpts['name']);
            $imageId = $result['image_id'] ?? $result['id'] ?? '';
            echo "Image cloned: {$imageId}\n";
            return;
        }

        $this->cliError("No action specified for image command. Use --list, --info, --delete, --publish, --spawn, --clone, etc.");
        exit(2);
    }

    /**
     * Parse image-specific options.
     *
     * @param array $args Arguments to parse
     * @return array Parsed options
     */
    private function cliParseImageOptions(array $args): array {
        $opts = [
            'list' => false,
            'info' => null,
            'delete' => null,
            'lock' => null,
            'unlock' => null,
            'publish' => null,
            'sourceType' => null,
            'visibility' => null,
            'visibilityMode' => null,
            'spawn' => null,
            'clone' => null,
            'name' => null,
            'ports' => null,
        ];

        $i = 0;
        while ($i < count($args)) {
            $arg = $args[$i];

            if ($arg === '--list' || $arg === '-l') {
                $opts['list'] = true;
            } elseif ($arg === '--info') {
                $i++;
                $opts['info'] = $args[$i] ?? null;
            } elseif ($arg === '--delete') {
                $i++;
                $opts['delete'] = $args[$i] ?? null;
            } elseif ($arg === '--lock') {
                $i++;
                $opts['lock'] = $args[$i] ?? null;
            } elseif ($arg === '--unlock') {
                $i++;
                $opts['unlock'] = $args[$i] ?? null;
            } elseif ($arg === '--publish') {
                $i++;
                $opts['publish'] = $args[$i] ?? null;
            } elseif ($arg === '--source-type') {
                $i++;
                $opts['sourceType'] = $args[$i] ?? null;
            } elseif ($arg === '--visibility') {
                $i++;
                $opts['visibility'] = $args[$i] ?? null;
                // Check if next arg is the mode (not a flag)
                if (isset($args[$i + 1]) && !str_starts_with($args[$i + 1], '-')) {
                    $i++;
                    $opts['visibilityMode'] = $args[$i];
                }
            } elseif ($arg === '--spawn') {
                $i++;
                $opts['spawn'] = $args[$i] ?? null;
            } elseif ($arg === '--clone') {
                $i++;
                $opts['clone'] = $args[$i] ?? null;
            } elseif ($arg === '--name') {
                $i++;
                $opts['name'] = $args[$i] ?? null;
            } elseif ($arg === '--ports') {
                $i++;
                $opts['ports'] = $args[$i] ?? null;
            }

            $i++;
        }

        return $opts;
    }

    /**
     * Print images list in tabular format.
     *
     * @param array $images Images to print
     */
    private function cliPrintImageList(array $images): void {
        if (empty($images)) {
            echo "No images found.\n";
            return;
        }

        printf("%-38s  %-20s  %-10s  %-10s  %s\n", 'ID', 'NAME', 'VISIBILITY', 'SOURCE', 'CREATED');

        foreach ($images as $image) {
            printf(
                "%-38s  %-20s  %-10s  %-10s  %s\n",
                substr($image['image_id'] ?? $image['id'] ?? '-', 0, 38),
                substr($image['name'] ?? '-', 0, 20),
                substr($image['visibility'] ?? 'private', 0, 10),
                substr($image['source_type'] ?? '-', 0, 10),
                $image['created_at'] ?? '-'
            );
        }
    }

    /**
     * Print image info.
     *
     * @param array $image Image data
     */
    private function cliPrintImageInfo(array $image): void {
        echo "ID: " . ($image['image_id'] ?? $image['id'] ?? '-') . "\n";
        if (!empty($image['name'])) {
            echo "Name: " . $image['name'] . "\n";
        }
        if (!empty($image['visibility'])) {
            echo "Visibility: " . $image['visibility'] . "\n";
        }
        if (!empty($image['source_type'])) {
            echo "Source Type: " . $image['source_type'] . "\n";
        }
        if (!empty($image['source_id'])) {
            echo "Source ID: " . $image['source_id'] . "\n";
        }
        if (!empty($image['size'])) {
            echo "Size: " . $image['size'] . "\n";
        }
        if (isset($image['locked'])) {
            echo "Locked: " . ($image['locked'] ? 'yes' : 'no') . "\n";
        }
        if (!empty($image['created_at'])) {
            echo "Created: " . $image['created_at'] . "\n";
        }
    }

    /**
     * Handle key subcommand.
     *
     * @param array $opts Global options
     */
    private function cliHandleKey(array $opts): void {
        $result = $this->validateKeys();
        echo "API Key Valid: " . ($result['valid'] ? 'yes' : 'no') . "\n";
        if (isset($result['account_id'])) {
            echo "Account ID: " . $result['account_id'] . "\n";
        }
        if (isset($result['email'])) {
            echo "Email: " . $result['email'] . "\n";
        }
    }

    /**
     * Handle languages command.
     *
     * @param array $args Command arguments
     * @param array $opts Global options
     */
    private function cliHandleLanguages(array $args, array $opts): void {
        // Check for --json flag
        $jsonOutput = in_array('--json', $args);

        $languages = $this->getLanguages();

        if ($jsonOutput) {
            // Output as JSON array
            echo json_encode($languages) . "\n";
        } else {
            // Output one language per line (pipe-friendly)
            foreach ($languages as $lang) {
                echo $lang . "\n";
            }
        }
    }

    /**
     * Print list of resources in tabular format.
     *
     * @param array $items Items to print
     * @param string $type Resource type (session, service, snapshot)
     */
    private function cliPrintList(array $items, string $type): void {
        if (empty($items)) {
            echo "No {$type}s found.\n";
            return;
        }

        // Print header
        printf("%-40s  %-20s  %-12s  %s\n", 'ID', 'NAME', 'STATUS', 'CREATED');
        echo str_repeat('-', 90) . "\n";

        foreach ($items as $item) {
            $id = $item["{$type}_id"] ?? $item['id'] ?? 'N/A';
            $name = $item['name'] ?? 'N/A';
            $status = $item['status'] ?? 'N/A';
            $created = $item['created_at'] ?? $item['created'] ?? 'N/A';

            printf("%-40s  %-20s  %-12s  %s\n", $id, $name, $status, $created);
        }
    }

    /**
     * Print session information.
     *
     * @param array $session Session data
     */
    private function cliPrintSessionInfo(array $session): void {
        echo "Session ID: " . ($session['session_id'] ?? $session['id'] ?? 'N/A') . "\n";
        if (isset($session['container_name'])) {
            echo "Container: " . $session['container_name'] . "\n";
        }
        if (isset($session['status'])) {
            echo "Status: " . $session['status'] . "\n";
        }
        if (isset($session['shell'])) {
            echo "Shell: " . $session['shell'] . "\n";
        }
        if (isset($session['network_mode'])) {
            echo "Network: " . $session['network_mode'] . "\n";
        }
        if (isset($session['websocket_url'])) {
            echo "WebSocket URL: " . $session['websocket_url'] . "\n";
        }
    }

    /**
     * Print service information.
     *
     * @param array $service Service data
     */
    private function cliPrintServiceInfo(array $service): void {
        echo "Service ID: " . ($service['service_id'] ?? $service['id'] ?? 'N/A') . "\n";
        if (isset($service['name'])) {
            echo "Name: " . $service['name'] . "\n";
        }
        if (isset($service['status'])) {
            echo "Status: " . $service['status'] . "\n";
        }
        if (isset($service['ports'])) {
            echo "Ports: " . (is_array($service['ports']) ? implode(', ', $service['ports']) : $service['ports']) . "\n";
        }
        if (isset($service['url'])) {
            echo "URL: " . $service['url'] . "\n";
        }
        if (isset($service['network_mode'])) {
            echo "Network: " . $service['network_mode'] . "\n";
        }
    }

    /**
     * Print snapshot information.
     *
     * @param array $snapshot Snapshot data
     */
    private function cliPrintSnapshotInfo(array $snapshot): void {
        echo "Snapshot ID: " . ($snapshot['snapshot_id'] ?? $snapshot['id'] ?? 'N/A') . "\n";
        if (isset($snapshot['name'])) {
            echo "Name: " . $snapshot['name'] . "\n";
        }
        if (isset($snapshot['type'])) {
            echo "Type: " . $snapshot['type'] . "\n";
        }
        if (isset($snapshot['status'])) {
            echo "Status: " . $snapshot['status'] . "\n";
        }
        if (isset($snapshot['size'])) {
            echo "Size: " . $snapshot['size'] . "\n";
        }
        if (isset($snapshot['locked'])) {
            echo "Locked: " . ($snapshot['locked'] ? 'yes' : 'no') . "\n";
        }
        if (isset($snapshot['created_at'])) {
            echo "Created: " . $snapshot['created_at'] . "\n";
        }
    }

    /**
     * Print error message to stderr.
     *
     * @param string $message Error message
     */
    private function cliError(string $message): void {
        fwrite(STDERR, "Error: {$message}\n");
    }

    /**
     * Show CLI help.
     */
    private function cliShowHelp(): void {
        $help = <<<HELP
Unsandbox PHP CLI

USAGE:
    php un.php [options] <source_file>
    php un.php -s <language> '<code>'
    php un.php session [options]
    php un.php service [options]
    php un.php snapshot [options]
    php un.php key
    php un.php languages [--json]

GLOBAL OPTIONS:
    -s, --shell LANG        Language for inline code execution
    -e, --env KEY=VAL       Set environment variable (can be repeated)
    -f, --file FILE         Add input file to /tmp/ (can be repeated)
    -F, --file-path FILE    Add input file preserving path (can be repeated)
    -a, --artifacts         Return compiled artifacts
    -o, --output DIR        Output directory for artifacts
    -p, --public-key KEY    API public key
    -k, --secret-key KEY    API secret key
    -n, --network MODE      Network mode: zerotrust (default) or semitrusted
    -v, --vcpu N            vCPU count (1-8)
    -y, --yes               Skip confirmation prompts
    -h, --help              Show this help

COMMANDS:
    <file>                  Execute code file (default command)
    session                 Manage interactive sessions
    service                 Manage persistent services
    snapshot                Manage snapshots
    key                     Validate API key
    languages [--json]      List available languages

SESSION OPTIONS:
    --list, -l              List active sessions
    --attach ID             Reconnect to session
    --kill ID               Terminate session
    --freeze ID             Pause session
    --unfreeze ID           Resume session
    --boost ID              Boost session resources
    --unboost ID            Remove session boost
    --snapshot ID           Create session snapshot
    --snapshot-name NAME    Name for snapshot
    --hot                   Hot snapshot (no freeze)
    --tmux                  Use tmux for persistence
    --screen                Use screen for persistence
    --shell SHELL           Shell/REPL to use

SERVICE OPTIONS:
    --list, -l              List all services
    --name NAME             Create service with name
    --ports PORTS           Ports to expose (comma-separated)
    --domains DOMAINS       Custom domains (comma-separated)
    --type TYPE             Service type (minecraft, tcp, udp)
    --bootstrap CMD         Bootstrap command
    --bootstrap-file FILE   Bootstrap from file
    --env-file FILE         Load environment from file
    --info ID               Get service details
    --logs ID               Get all service logs
    --tail ID               Get last 9000 lines of logs
    --freeze ID             Pause service
    --unfreeze ID           Resume service
    --destroy ID            Delete service (requires -y)
    --lock ID               Prevent service deletion
    --unlock ID             Allow service deletion
    --resize ID             Resize service (with --vcpu)
    --redeploy ID           Re-run bootstrap
    --execute ID 'cmd'      Execute command in service
    --snapshot ID           Create service snapshot

SERVICE ENV SUBCOMMAND:
    service env status ID   Show vault status
    service env set ID      Set vault (stdin or --env-file)
    service env export ID   Export vault to stdout
    service env delete ID   Delete vault (requires -y)

SNAPSHOT OPTIONS:
    --list, -l              List all snapshots
    --info ID               Get snapshot details
    --delete ID             Delete snapshot (requires -y)
    --lock ID               Prevent snapshot deletion
    --unlock ID             Allow snapshot deletion
    --clone ID              Clone snapshot
    --type TYPE             Clone type: session or service
    --name NAME             Name for cloned resource
    --shell SHELL           Shell for cloned session
    --ports PORTS           Ports for cloned service

EXAMPLES:
    php un.php script.py
    php un.php -s bash 'echo hello'
    php un.php -n semitrusted crawler.py
    php un.php session --shell python3 --tmux
    php un.php session --list
    php un.php service --name myapp --ports 80 --bootstrap 'python -m http.server 80'
    php un.php service --execute <id> 'ls -la'
    php un.php snapshot --list
    php un.php key

HELP;
        echo $help;
    }
}

// CLI entry point
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($argv[0])) {
    $client = new Unsandbox();
    $client->cliMain($argv);
}
