#!/usr/bin/env perl
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
#
# unsandbox.com Perl SDK (Synchronous)
# Full API with execution, sessions, services, snapshots, and images.
#
# Library Usage:
#     use Un;
#     my $result = Un::execute("python", "print(42)");
#     print $result->{stdout};
#
# CLI Usage:
#     perl un.pl script.py
#     perl un.pl -s python 'print(42)'
#     perl un.pl session --list
#     perl un.pl service --list
#     perl un.pl snapshot --list
#     perl un.pl image --list
#
# Authentication Priority (4-tier):
#     1. Function arguments (public_key, secret_key)
#     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)
#
# Copyright 2025 TimeHexOn & foxhop & russell@unturf

use strict;
use warnings;
use JSON::PP;
use LWP::UserAgent;
use HTTP::Request;
use Digest::SHA qw(hmac_sha256_hex);
use MIME::Base64;
use File::Basename;
use File::Path qw(make_path);
use Time::HiRes qw(time sleep);

package Un;

our $VERSION = "4.3.4";
our $API_BASE = 'https://api.unsandbox.com';
our $PORTAL_BASE = 'https://unsandbox.com';

# Thread-local error storage
our $LAST_ERROR = "";
our $ACCOUNT_INDEX = -1;  # -1 means not set; set to N to use accounts.csv row N

# Colors
my $BLUE = "\033[34m";
my $RED = "\033[31m";
my $GREEN = "\033[32m";
my $YELLOW = "\033[33m";
my $RESET = "\033[0m";

# Extension to language mapping
my %EXT_MAP = (
    '.py' => 'python', '.js' => 'javascript', '.ts' => 'typescript',
    '.rb' => 'ruby', '.php' => 'php', '.pl' => 'perl', '.lua' => 'lua',
    '.sh' => 'bash', '.go' => 'go', '.rs' => 'rust', '.c' => 'c',
    '.cpp' => 'cpp', '.cc' => 'cpp', '.cxx' => 'cpp',
    '.java' => 'java', '.kt' => 'kotlin', '.cs' => 'csharp', '.fs' => 'fsharp',
    '.hs' => 'haskell', '.ml' => 'ocaml', '.clj' => 'clojure', '.scm' => 'scheme',
    '.lisp' => 'commonlisp', '.erl' => 'erlang', '.ex' => 'elixir', '.exs' => 'elixir',
    '.jl' => 'julia', '.r' => 'r', '.R' => 'r', '.cr' => 'crystal',
    '.d' => 'd', '.nim' => 'nim', '.zig' => 'zig', '.v' => 'v',
    '.dart' => 'dart', '.groovy' => 'groovy', '.scala' => 'scala',
    '.f90' => 'fortran', '.f95' => 'fortran', '.cob' => 'cobol',
    '.pro' => 'prolog', '.forth' => 'forth', '.4th' => 'forth',
    '.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc'
);

# ============================================================================
# Utility Functions
# ============================================================================

sub version { return $VERSION; }

sub last_error { return $LAST_ERROR; }

sub set_error {
    my ($msg) = @_;
    $LAST_ERROR = $msg;
}

sub detect_language {
    my ($filename) = @_;
    return undef unless $filename;
    my ($name, $dir, $ext) = fileparse($filename, qr/\.[^.]*/);
    return $EXT_MAP{lc($ext)};
}

sub hmac_sign {
    my ($secret, $message) = @_;
    return undef unless defined $secret && defined $message;
    return hmac_sha256_hex($message, $secret);
}

# ============================================================================
# Credential Management
# ============================================================================

sub load_accounts_csv {
    my ($path) = @_;
    $path ||= "$ENV{HOME}/.unsandbox/accounts.csv";
    return [] unless -e $path;

    my @accounts;
    open my $fh, '<', $path or return [];
    while (my $line = <$fh>) {
        chomp $line;
        next if !$line || $line =~ /^\s*#/;
        my ($pk, $sk) = split /,/, $line, 2;
        push @accounts, [$pk, $sk] if $pk && $sk;
    }
    close $fh;
    return \@accounts;
}

sub get_credentials {
    my (%opts) = @_;

    # Tier 1: Arguments
    return ($opts{public_key}, $opts{secret_key}) if $opts{public_key} && $opts{secret_key};

    # Tier 2: --account N flag → bypass env vars, load CSV row N directly
    my $ai = exists $opts{account_index} ? $opts{account_index} : $Un::ACCOUNT_INDEX;
    if (defined $ai && $ai >= 0) {
        my $home_accounts = load_accounts_csv();
        return @{$home_accounts->[$ai]} if @$home_accounts > $ai;
        my $local_accounts = load_accounts_csv("./accounts.csv");
        return @{$local_accounts->[$ai]} if @$local_accounts > $ai;
        set_error("Account index $ai not found in accounts.csv");
        return (undef, undef);
    }

    # Tier 3: Environment
    if ($ENV{UNSANDBOX_PUBLIC_KEY} && $ENV{UNSANDBOX_SECRET_KEY}) {
        return ($ENV{UNSANDBOX_PUBLIC_KEY}, $ENV{UNSANDBOX_SECRET_KEY});
    }

    # Legacy fallback
    if ($ENV{UNSANDBOX_API_KEY}) {
        return ($ENV{UNSANDBOX_API_KEY}, '');
    }

    # Tier 4: Home directory
    my $home_accounts = load_accounts_csv();
    return @{$home_accounts->[0]} if @$home_accounts;

    # Tier 5: Local directory
    my $local_accounts = load_accounts_csv("./accounts.csv");
    return @{$local_accounts->[0]} if @$local_accounts;

    set_error("No credentials found");
    return (undef, undef);
}

# ============================================================================
# API Communication
# ============================================================================

sub api_request {
    my ($method, $endpoint, $body, %opts) = @_;
    my $extra_headers = delete $opts{extra_headers} || {};
    my $content_type = delete $opts{content_type} || 'application/json';
    my ($pk, $sk) = get_credentials(%opts);

    unless ($pk) {
        set_error("No credentials available");
        return undef;
    }

    my $timestamp = int(time);
    my $body_str = '';
    if ($body) {
        $body_str = ref($body) ? encode_json($body) : $body;
    }

    my $ua = LWP::UserAgent->new(timeout => 300);
    my $url = "$API_BASE$endpoint";
    my $req = HTTP::Request->new($method, $url);

    $req->header('Authorization' => "Bearer $pk");
    $req->header('Content-Type' => $content_type);

    # HMAC signature
    if ($sk) {
        my $sig_input = "$timestamp:$method:$endpoint:$body_str";
        my $signature = hmac_sign($sk, $sig_input);
        $req->header('X-Timestamp' => $timestamp);
        $req->header('X-Signature' => $signature);
    }

    # Add extra headers
    for my $key (keys %$extra_headers) {
        $req->header($key => $extra_headers->{$key});
    }

    $req->content($body_str) if $body_str;

    my $res = $ua->request($req);

    unless ($res->is_success) {
        set_error("API error (" . $res->code . "): " . $res->content);
        return undef;
    }

    return decode_json($res->content) if $res->content;
    return { success => 1 };
}

sub api_request_with_sudo {
    my ($method, $endpoint, $body, %opts) = @_;
    my ($pk, $sk) = get_credentials(%opts);

    my $timestamp = int(time);
    my $body_str = '';
    if ($body) {
        $body_str = ref($body) ? encode_json($body) : $body;
    }

    my $ua = LWP::UserAgent->new(timeout => 300);
    my $url = "$API_BASE$endpoint";
    my $req = HTTP::Request->new($method, $url);

    $req->header('Authorization' => "Bearer $pk");
    $req->header('Content-Type' => 'application/json');

    if ($sk) {
        my $sig_input = "$timestamp:$method:$endpoint:$body_str";
        my $signature = hmac_sign($sk, $sig_input);
        $req->header('X-Timestamp' => $timestamp);
        $req->header('X-Signature' => $signature);
    }

    $req->content($body_str) if $body_str;

    my $res = $ua->request($req);

    # Handle 428 - Sudo OTP required
    if ($res->code == 428) {
        my $response_data = eval { decode_json($res->content) } || {};
        my $challenge_id = $response_data->{challenge_id} || '';

        print STDERR "${YELLOW}Confirmation required. Check your email for a one-time code.${RESET}\n";
        print STDERR "Enter OTP: ";

        my $otp = <STDIN>;
        return undef unless defined $otp;
        chomp $otp;
        $otp =~ s/\r//g;

        if ($otp eq '') {
            set_error("Operation cancelled");
            return undef;
        }

        my $extra = { 'X-Sudo-OTP' => $otp };
        $extra->{'X-Sudo-Challenge'} = $challenge_id if $challenge_id;

        return api_request($method, $endpoint, $body, %opts, extra_headers => $extra);
    }

    unless ($res->is_success) {
        set_error("API error (" . $res->code . "): " . $res->content);
        return undef;
    }

    return decode_json($res->content) if $res->content;
    return { success => 1 };
}

# ============================================================================
# Execution Functions (8)
# ============================================================================

sub execute {
    my ($language, $code, %opts) = @_;
    my $body = {
        language => $language,
        code => $code,
        network_mode => $opts{network_mode} || 'zerotrust',
        ttl => $opts{ttl} || 60
    };
    $body->{env} = $opts{env} if $opts{env};
    $body->{input_files} = $opts{input_files} if $opts{input_files};
    $body->{return_artifacts} = JSON::PP::true if $opts{return_artifacts};
    return api_request('POST', '/execute', $body, %opts);
}

sub execute_async {
    my ($language, $code, %opts) = @_;
    my $body = {
        language => $language,
        code => $code,
        network_mode => $opts{network_mode} || 'zerotrust',
        ttl => $opts{ttl} || 300
    };
    return api_request('POST', '/execute/async', $body, %opts);
}

sub wait_job {
    my ($job_id, %opts) = @_;
    my $timeout = $opts{timeout} || 120;
    my @delays = (300, 450, 700, 900, 650, 1600, 2000);

    for my $i (0..119) {
        my $job = get_job($job_id, %opts);
        return $job if $job && $job->{status} eq 'completed';
        if ($job && $job->{status} eq 'failed') {
            set_error("Job failed: " . ($job->{error} || 'unknown'));
            return undef;
        }

        my $delay = $delays[$i % 7] || 2000;
        sleep($delay / 1000);
    }

    set_error("Max polls exceeded");
    return undef;
}

sub get_job {
    my ($job_id, %opts) = @_;
    return api_request('GET', "/jobs/$job_id", undef, %opts);
}

sub cancel_job {
    my ($job_id, %opts) = @_;
    return api_request('DELETE', "/jobs/$job_id", undef, %opts);
}

sub list_jobs {
    my (%opts) = @_;
    return api_request('GET', '/jobs', undef, %opts);
}

sub get_languages {
    my (%opts) = @_;
    my $cache_ttl = $opts{cache_ttl} || 3600;
    my $cache_path = "$ENV{HOME}/.unsandbox/languages.json";

    if (-e $cache_path) {
        my $age = time - (stat $cache_path)[9];
        if ($age < $cache_ttl) {
            open my $fh, '<', $cache_path;
            my $content = do { local $/; <$fh> };
            close $fh;
            return decode_json($content);
        }
    }

    my $result = api_request('GET', '/languages', undef, %opts);
    my $langs = $result->{languages} || [];

    my $cache_dir = "$ENV{HOME}/.unsandbox";
    mkdir $cache_dir unless -d $cache_dir;
    if (open my $fh, '>', $cache_path) {
        print $fh encode_json($langs);
        close $fh;
    }

    return $langs;
}

# ============================================================================
# Session Functions (9)
# ============================================================================

sub session_list {
    my (%opts) = @_;
    return api_request('GET', '/sessions', undef, %opts);
}

sub session_get {
    my ($session_id, %opts) = @_;
    return api_request('GET', "/sessions/$session_id", undef, %opts);
}

sub session_create {
    my (%opts) = @_;
    my $body = {
        shell => $opts{shell} || 'bash',
    };
    $body->{network} = $opts{network} if $opts{network};
    $body->{vcpu} = $opts{vcpu} if $opts{vcpu};
    $body->{input_files} = $opts{input_files} if $opts{input_files};
    $body->{persistence} = $opts{persistence} if $opts{persistence};
    return api_request('POST', '/sessions', $body, %opts);
}

sub session_destroy {
    my ($session_id, %opts) = @_;
    return api_request('DELETE', "/sessions/$session_id", undef, %opts);
}

sub session_freeze {
    my ($session_id, %opts) = @_;
    return api_request('POST', "/sessions/$session_id/freeze", {}, %opts);
}

sub session_unfreeze {
    my ($session_id, %opts) = @_;
    return api_request('POST', "/sessions/$session_id/unfreeze", {}, %opts);
}

sub session_boost {
    my ($session_id, $vcpu, %opts) = @_;
    return api_request('POST', "/sessions/$session_id/boost", { vcpu => $vcpu }, %opts);
}

sub session_unboost {
    my ($session_id, %opts) = @_;
    return api_request('POST', "/sessions/$session_id/unboost", {}, %opts);
}

sub session_execute {
    my ($session_id, $command, %opts) = @_;
    return api_request('POST', "/sessions/$session_id/execute", { command => $command }, %opts);
}

# ============================================================================
# Service Functions (17)
# ============================================================================

sub service_list {
    my (%opts) = @_;
    return api_request('GET', '/services', undef, %opts);
}

sub service_get {
    my ($service_id, %opts) = @_;
    return api_request('GET', "/services/$service_id", undef, %opts);
}

sub service_create {
    my (%opts) = @_;
    my $body = { name => $opts{name} };
    $body->{ports} = $opts{ports} if $opts{ports};
    $body->{domains} = $opts{domains} if $opts{domains};
    $body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
    $body->{bootstrap_content} = $opts{bootstrap_content} if $opts{bootstrap_content};
    $body->{network} = $opts{network} if $opts{network};
    $body->{vcpu} = $opts{vcpu} if $opts{vcpu};
    $body->{service_type} = $opts{service_type} if $opts{service_type};
    $body->{input_files} = $opts{input_files} if $opts{input_files};
    $body->{unfreeze_on_demand} = JSON::PP::true if $opts{unfreeze_on_demand};
    return api_request('POST', '/services', $body, %opts);
}

sub service_destroy {
    my ($service_id, %opts) = @_;
    return api_request_with_sudo('DELETE', "/services/$service_id", undef, %opts);
}

sub service_freeze {
    my ($service_id, %opts) = @_;
    return api_request('POST', "/services/$service_id/freeze", {}, %opts);
}

sub service_unfreeze {
    my ($service_id, %opts) = @_;
    return api_request('POST', "/services/$service_id/unfreeze", {}, %opts);
}

sub service_lock {
    my ($service_id, %opts) = @_;
    return api_request('POST', "/services/$service_id/lock", {}, %opts);
}

sub service_unlock {
    my ($service_id, %opts) = @_;
    return api_request_with_sudo('POST', "/services/$service_id/unlock", {}, %opts);
}

sub service_set_unfreeze_on_demand {
    my ($service_id, $enabled, %opts) = @_;
    my $body = { unfreeze_on_demand => ($enabled ? JSON::PP::true : JSON::PP::false) };
    return api_request('PATCH', "/services/$service_id", $body, %opts);
}

sub service_redeploy {
    my ($service_id, %opts) = @_;
    my $body = {};
    $body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
    $body->{input_files} = $opts{input_files} if $opts{input_files};
    return api_request('POST', "/services/$service_id/redeploy", $body, %opts);
}

sub service_logs {
    my ($service_id, %opts) = @_;
    my $endpoint = "/services/$service_id/logs";
    $endpoint .= "?lines=$opts{lines}" if $opts{lines};
    return api_request('GET', $endpoint, undef, %opts);
}

sub service_execute {
    my ($service_id, $command, %opts) = @_;
    my $body = { command => $command };
    $body->{timeout} = $opts{timeout} if $opts{timeout};
    return api_request('POST', "/services/$service_id/execute", $body, %opts);
}

sub service_env_get {
    my ($service_id, %opts) = @_;
    return api_request('GET', "/services/$service_id/env", undef, %opts);
}

sub service_env_set {
    my ($service_id, $env_content, %opts) = @_;
    return api_request('PUT', "/services/$service_id/env", $env_content,
                       %opts, content_type => 'text/plain');
}

sub service_env_delete {
    my ($service_id, %opts) = @_;
    return api_request('DELETE', "/services/$service_id/env", undef, %opts);
}

sub service_env_export {
    my ($service_id, %opts) = @_;
    return api_request('POST', "/services/$service_id/env/export", {}, %opts);
}

sub service_resize {
    my ($service_id, $vcpu, %opts) = @_;
    return api_request('PATCH', "/services/$service_id", { vcpu => $vcpu }, %opts);
}

# ============================================================================
# Snapshot Functions (9)
# ============================================================================

sub snapshot_list {
    my (%opts) = @_;
    return api_request('GET', '/snapshots', undef, %opts);
}

sub snapshot_get {
    my ($snapshot_id, %opts) = @_;
    return api_request('GET', "/snapshots/$snapshot_id", undef, %opts);
}

sub snapshot_session {
    my ($session_id, %opts) = @_;
    my $body = {};
    $body->{name} = $opts{name} if $opts{name};
    $body->{hot} = JSON::PP::true if $opts{hot};
    return api_request('POST', "/sessions/$session_id/snapshot", $body, %opts);
}

sub snapshot_service {
    my ($service_id, %opts) = @_;
    my $body = {};
    $body->{name} = $opts{name} if $opts{name};
    $body->{hot} = JSON::PP::true if $opts{hot};
    return api_request('POST', "/services/$service_id/snapshot", $body, %opts);
}

sub snapshot_restore {
    my ($snapshot_id, %opts) = @_;
    return api_request('POST', "/snapshots/$snapshot_id/restore", {}, %opts);
}

sub snapshot_delete {
    my ($snapshot_id, %opts) = @_;
    return api_request_with_sudo('DELETE', "/snapshots/$snapshot_id", undef, %opts);
}

sub snapshot_lock {
    my ($snapshot_id, %opts) = @_;
    return api_request('POST', "/snapshots/$snapshot_id/lock", {}, %opts);
}

sub snapshot_unlock {
    my ($snapshot_id, %opts) = @_;
    return api_request_with_sudo('POST', "/snapshots/$snapshot_id/unlock", {}, %opts);
}

sub snapshot_clone {
    my ($snapshot_id, %opts) = @_;
    my $body = { clone_type => $opts{clone_type} || 'session' };
    $body->{name} = $opts{name} if $opts{name};
    $body->{ports} = $opts{ports} if $opts{ports};
    $body->{shell} = $opts{shell} if $opts{shell};
    return api_request('POST', "/snapshots/$snapshot_id/clone", $body, %opts);
}

# ============================================================================
# Image Functions (13)
# ============================================================================

sub image_list {
    my (%opts) = @_;
    my $endpoint = '/images';
    $endpoint .= "?filter=$opts{filter}" if $opts{filter};
    return api_request('GET', $endpoint, undef, %opts);
}

sub image_get {
    my ($image_id, %opts) = @_;
    return api_request('GET', "/images/$image_id", undef, %opts);
}

sub image_publish {
    my (%opts) = @_;
    my $body = {
        source_type => $opts{source_type},
        source_id => $opts{source_id}
    };
    $body->{name} = $opts{name} if $opts{name};
    $body->{description} = $opts{description} if $opts{description};
    return api_request('POST', '/images/publish', $body, %opts);
}

sub image_delete {
    my ($image_id, %opts) = @_;
    return api_request_with_sudo('DELETE', "/images/$image_id", undef, %opts);
}

sub image_lock {
    my ($image_id, %opts) = @_;
    return api_request('POST', "/images/$image_id/lock", {}, %opts);
}

sub image_unlock {
    my ($image_id, %opts) = @_;
    return api_request_with_sudo('POST', "/images/$image_id/unlock", {}, %opts);
}

sub image_set_visibility {
    my ($image_id, $visibility, %opts) = @_;
    return api_request('POST', "/images/$image_id/visibility", { visibility => $visibility }, %opts);
}

sub image_grant_access {
    my ($image_id, $trusted_api_key, %opts) = @_;
    return api_request('POST', "/images/$image_id/access", { api_key => $trusted_api_key }, %opts);
}

sub image_revoke_access {
    my ($image_id, $trusted_api_key, %opts) = @_;
    return api_request('DELETE', "/images/$image_id/access/$trusted_api_key", undef, %opts);
}

sub image_list_trusted {
    my ($image_id, %opts) = @_;
    return api_request('GET', "/images/$image_id/access", undef, %opts);
}

sub image_transfer {
    my ($image_id, $to_api_key, %opts) = @_;
    return api_request('POST', "/images/$image_id/transfer", { to_api_key => $to_api_key }, %opts);
}

sub image_spawn {
    my ($image_id, %opts) = @_;
    my $body = {};
    $body->{name} = $opts{name} if $opts{name};
    $body->{ports} = $opts{ports} if $opts{ports};
    $body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
    $body->{network_mode} = $opts{network_mode} if $opts{network_mode};
    return api_request('POST', "/images/$image_id/spawn", $body, %opts);
}

sub image_clone {
    my ($image_id, %opts) = @_;
    my $body = {};
    $body->{name} = $opts{name} if $opts{name};
    $body->{description} = $opts{description} if $opts{description};
    return api_request('POST', "/images/$image_id/clone", $body, %opts);
}

# ============================================================================
# PaaS Logs Functions (2)
# ============================================================================

sub logs_fetch {
    my (%opts) = @_;
    my $body = {
        source => $opts{source} || 'all',
        lines => $opts{lines} || 100,
        since => $opts{since} || '1h'
    };
    $body->{grep} = $opts{grep} if $opts{grep};
    return api_request('POST', '/paas/logs', $body, %opts);
}

sub logs_stream {
    my (%opts) = @_;
    # SSE streaming - returns immediately, callback for each line
    my $callback = $opts{callback};
    return undef unless $callback;

    # SSE streaming not easily supported in sync Perl, return placeholder
    set_error("logs_stream requires async support");
    return undef;
}

# ============================================================================
# Key Validation
# ============================================================================

sub validate_keys {
    my (%opts) = @_;
    my ($pk, $sk) = get_credentials(%opts);

    my $ua = LWP::UserAgent->new(timeout => 30);
    my $url = "$PORTAL_BASE/keys/validate";
    my $req = HTTP::Request->new('POST', $url);

    $req->header('Authorization' => "Bearer $pk");
    $req->header('Content-Type' => 'application/json');

    if ($sk) {
        my $timestamp = int(time);
        my $sig_input = "$timestamp:POST:/keys/validate:";
        my $signature = hmac_sign($sk, $sig_input);
        $req->header('X-Timestamp' => $timestamp);
        $req->header('X-Signature' => $signature);
    }

    my $res = $ua->request($req);
    return decode_json($res->content) if $res->content;
    return undef;
}

sub health_check {
    my (%opts) = @_;
    my $ua = LWP::UserAgent->new(timeout => 10);
    my $res = $ua->get("$API_BASE/health");
    return $res->is_success ? 1 : 0;
}

# ============================================================================
# CLI Implementation
# ============================================================================

package main;

sub build_input_files {
    my @files = @_;
    my @input_files;
    foreach my $filepath (@files) {
        unless (-e $filepath) {
            print STDERR "${RED}Error: Input file not found: $filepath${RESET}\n";
            exit 1;
        }
        open my $f, '<:raw', $filepath or die "Cannot read file: $!";
        my $content = do { local $/; <$f> };
        close $f;
        push @input_files, {
            filename => basename($filepath),
            content_base64 => encode_base64($content, '')
        };
    }
    return \@input_files;
}

sub build_env_content {
    my ($envs, $env_file) = @_;
    my @parts;

    if ($env_file && -e $env_file) {
        open my $fh, '<', $env_file or die "Cannot read file: $!";
        my $content = do { local $/; <$fh> };
        close $fh;
        push @parts, $content;
    }

    foreach my $e (@$envs) {
        push @parts, $e if $e =~ /=/;
    }

    return join("\n", @parts);
}

sub cmd_execute {
    my ($options) = @_;
    my $source = $options->{source_file};
    my $inline_code = $options->{inline_code};
    my $language = $options->{language};

    my $code;
    if ($inline_code) {
        $code = $inline_code;
    } else {
        unless (-e $source) {
            print STDERR "${RED}Error: File not found: $source${RESET}\n";
            exit 1;
        }
        open my $fh, '<', $source or die "Cannot read file: $!";
        $code = do { local $/; <$fh> };
        close $fh;
        $language ||= Un::detect_language($source);
    }

    unless ($language) {
        print STDERR "${RED}Error: Could not detect language${RESET}\n";
        exit 1;
    }

    my %opts = (network_mode => $options->{network} || 'zerotrust');
    $opts{vcpu} = $options->{vcpu} if $options->{vcpu};

    if ($options->{env} && @{$options->{env}}) {
        my %env_vars;
        foreach my $e (@{$options->{env}}) {
            if ($e =~ /^([^=]+)=(.*)$/) {
                $env_vars{$1} = $2;
            }
        }
        $opts{env} = \%env_vars if %env_vars;
    }

    if ($options->{files} && @{$options->{files}}) {
        $opts{input_files} = build_input_files(@{$options->{files}});
    }

    $opts{return_artifacts} = 1 if $options->{artifacts};

    my $result = Un::execute($language, $code, %opts);

    unless ($result) {
        print STDERR "${RED}Error: " . Un::last_error() . "${RESET}\n";
        exit 1;
    }

    print $result->{stdout} if $result->{stdout};
    print STDERR $result->{stderr} if $result->{stderr};

    if ($options->{artifacts} && $result->{artifacts}) {
        my $out_dir = $options->{output_dir} || '.';
        make_path($out_dir) unless -d $out_dir;
        foreach my $artifact (@{$result->{artifacts}}) {
            my $filename = $artifact->{filename} || 'artifact';
            my $content = decode_base64($artifact->{content_base64});
            my $filepath = "$out_dir/$filename";
            open my $f, '>:raw', $filepath or die "Cannot write file: $!";
            print $f $content;
            close $f;
            chmod 0755, $filepath;
            print STDERR "${GREEN}Saved: $filepath${RESET}\n";
        }
    }

    exit($result->{exit_code} || 0);
}

sub cmd_session {
    my ($options) = @_;

    if ($options->{list}) {
        my $result = Un::session_list();
        my $sessions = $result->{sessions} || [];
        if (@$sessions == 0) {
            print "No active sessions\n";
        } else {
            printf "%-40s %-10s %-10s %s\n", 'ID', 'Shell', 'Status', 'Created';
            foreach my $s (@$sessions) {
                printf "%-40s %-10s %-10s %s\n",
                    $s->{id} // 'N/A', $s->{shell} // 'N/A',
                    $s->{status} // 'N/A', $s->{created_at} // 'N/A';
            }
        }
        return;
    }

    if ($options->{kill}) {
        Un::session_destroy($options->{kill});
        print "${GREEN}Session terminated: $options->{kill}${RESET}\n";
        return;
    }

    if ($options->{info}) {
        my $result = Un::session_get($options->{info});
        print encode_json($result) . "\n";
        return;
    }

    if ($options->{freeze}) {
        Un::session_freeze($options->{freeze});
        print "${GREEN}Session frozen: $options->{freeze}${RESET}\n";
        return;
    }

    if ($options->{unfreeze}) {
        Un::session_unfreeze($options->{unfreeze});
        print "${GREEN}Session unfreezing: $options->{unfreeze}${RESET}\n";
        return;
    }

    if ($options->{boost}) {
        my $vcpu = $options->{vcpu} || 2;
        Un::session_boost($options->{boost}, $vcpu);
        print "${GREEN}Session boosted: $options->{boost}${RESET}\n";
        return;
    }

    if ($options->{unboost}) {
        Un::session_unboost($options->{unboost});
        print "${GREEN}Session unboosted: $options->{unboost}${RESET}\n";
        return;
    }

    if ($options->{execute}) {
        my $result = Un::session_execute($options->{execute}, $options->{command});
        print $result->{stdout} if $result->{stdout};
        print STDERR $result->{stderr} if $result->{stderr};
        return;
    }

    if ($options->{snapshot}) {
        my $result = Un::snapshot_session($options->{snapshot},
            name => $options->{snapshot_name}, hot => $options->{hot});
        print "${GREEN}Snapshot created${RESET}\n";
        print encode_json($result) . "\n";
        return;
    }

    # Create new session
    my %opts = (shell => $options->{shell} || 'bash');
    $opts{network} = $options->{network} if $options->{network};
    $opts{vcpu} = $options->{vcpu} if $options->{vcpu};
    $opts{persistence} = 'tmux' if $options->{tmux};
    $opts{persistence} = 'screen' if $options->{screen};

    if ($options->{files} && @{$options->{files}}) {
        $opts{input_files} = build_input_files(@{$options->{files}});
    }

    print "${YELLOW}Creating session...${RESET}\n";
    my $result = Un::session_create(%opts);
    print "${GREEN}Session created: " . ($result->{id} // 'N/A') . "${RESET}\n";
    print "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}\n";
}

sub cmd_service {
    my ($options) = @_;

    if ($options->{list}) {
        my $result = Un::service_list();
        my $services = $result->{services} || [];
        if (@$services == 0) {
            print "No services\n";
        } else {
            printf "%-20s %-15s %-10s %-15s %s\n", 'ID', 'Name', 'Status', 'Ports', 'Domains';
            foreach my $s (@$services) {
                my $ports = join(',', @{$s->{ports} || []});
                my $domains = join(',', @{$s->{domains} || []});
                printf "%-20s %-15s %-10s %-15s %s\n",
                    $s->{id} // 'N/A', $s->{name} // 'N/A',
                    $s->{status} // 'N/A', $ports, $domains;
            }
        }
        return;
    }

    if ($options->{info}) {
        my $result = Un::service_get($options->{info});
        print encode_json($result) . "\n";
        return;
    }

    if ($options->{logs}) {
        my $result = Un::service_logs($options->{logs}, lines => $options->{lines});
        print $result->{logs} // '';
        return;
    }

    if ($options->{freeze}) {
        Un::service_freeze($options->{freeze});
        print "${GREEN}Service frozen: $options->{freeze}${RESET}\n";
        return;
    }

    if ($options->{unfreeze}) {
        Un::service_unfreeze($options->{unfreeze});
        print "${GREEN}Service unfreezing: $options->{unfreeze}${RESET}\n";
        return;
    }

    if ($options->{lock}) {
        Un::service_lock($options->{lock});
        print "${GREEN}Service locked: $options->{lock}${RESET}\n";
        return;
    }

    if ($options->{unlock}) {
        Un::service_unlock($options->{unlock});
        print "${GREEN}Service unlocked: $options->{unlock}${RESET}\n";
        return;
    }

    if ($options->{destroy}) {
        Un::service_destroy($options->{destroy});
        print "${GREEN}Service destroyed: $options->{destroy}${RESET}\n";
        return;
    }

    if ($options->{resize}) {
        unless ($options->{vcpu}) {
            print STDERR "${RED}Error: --vcpu is required with --resize${RESET}\n";
            exit 1;
        }
        Un::service_resize($options->{resize}, $options->{vcpu});
        my $ram = $options->{vcpu} * 2;
        print "${GREEN}Service resized to $options->{vcpu} vCPU, $ram GB RAM${RESET}\n";
        return;
    }

    if ($options->{redeploy}) {
        my %opts;
        $opts{bootstrap} = $options->{bootstrap} if $options->{bootstrap};
        if ($options->{files} && @{$options->{files}}) {
            $opts{input_files} = build_input_files(@{$options->{files}});
        }
        Un::service_redeploy($options->{redeploy}, %opts);
        print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n";
        return;
    }

    if ($options->{execute}) {
        my $result = Un::service_execute($options->{execute}, $options->{command});
        print $result->{stdout} if $result->{stdout};
        print STDERR $result->{stderr} if $result->{stderr};
        return;
    }

    if ($options->{set_unfreeze_on_demand}) {
        my $enabled = $options->{set_unfreeze_on_demand_value};
        Un::service_set_unfreeze_on_demand($options->{set_unfreeze_on_demand}, $enabled);
        my $status = $enabled ? 'enabled' : 'disabled';
        print "${GREEN}Unfreeze-on-demand $status${RESET}\n";
        return;
    }

    if ($options->{snapshot}) {
        my $result = Un::snapshot_service($options->{snapshot},
            name => $options->{snapshot_name}, hot => $options->{hot});
        print "${GREEN}Snapshot created${RESET}\n";
        print encode_json($result) . "\n";
        return;
    }

    if ($options->{name}) {
        my %opts = (name => $options->{name});
        if ($options->{ports}) {
            $opts{ports} = [map { int($_) } split(',', $options->{ports})];
        }
        if ($options->{domains}) {
            $opts{domains} = [split(',', $options->{domains})];
        }
        $opts{service_type} = $options->{type} if $options->{type};
        $opts{bootstrap} = $options->{bootstrap} if $options->{bootstrap};

        if ($options->{bootstrap_file}) {
            if (! -e $options->{bootstrap_file}) {
                print STDERR "${RED}Error: Bootstrap file not found${RESET}\n";
                exit 1;
            }
            open my $fh, '<', $options->{bootstrap_file};
            $opts{bootstrap_content} = do { local $/; <$fh> };
            close $fh;
        }

        if ($options->{files} && @{$options->{files}}) {
            $opts{input_files} = build_input_files(@{$options->{files}});
        }

        $opts{network} = $options->{network} if $options->{network};
        $opts{vcpu} = $options->{vcpu} if $options->{vcpu};
        $opts{unfreeze_on_demand} = 1 if $options->{unfreeze_on_demand};

        my $result = Un::service_create(%opts);
        print "${GREEN}Service created: " . ($result->{id} // 'N/A') . "${RESET}\n";
        print "Name: " . ($result->{name} // 'N/A') . "\n";
        print "URL: $result->{url}\n" if $result->{url};

        # Auto-set vault
        my $env_content = build_env_content($options->{env} || [], $options->{env_file});
        if ($env_content && $result->{id}) {
            Un::service_env_set($result->{id}, $env_content);
            print "${GREEN}Vault configured${RESET}\n";
        }
        return;
    }

    print STDERR "${RED}Error: Specify --name to create or use --list, --info, etc.${RESET}\n";
    exit 1;
}

sub cmd_service_env {
    my ($action, $target, $envs, $env_file) = @_;

    unless ($target) {
        print STDERR "${RED}Error: Service ID required${RESET}\n";
        exit 1;
    }

    if ($action eq 'status') {
        my $result = Un::service_env_get($target);
        print encode_json($result) . "\n";
    } elsif ($action eq 'set') {
        my $content = build_env_content($envs, $env_file);
        unless ($content) {
            print STDERR "${RED}Error: No env content provided${RESET}\n";
            exit 1;
        }
        Un::service_env_set($target, $content);
        print "${GREEN}Vault updated${RESET}\n";
    } elsif ($action eq 'export') {
        my $result = Un::service_env_export($target);
        print $result->{env} // $result->{content} // '';
    } elsif ($action eq 'delete') {
        Un::service_env_delete($target);
        print "${GREEN}Vault deleted${RESET}\n";
    } else {
        print STDERR "${RED}Error: Unknown env action '$action'${RESET}\n";
        exit 1;
    }
}

sub cmd_snapshot {
    my ($options) = @_;

    if ($options->{list}) {
        my $result = Un::snapshot_list();
        my $snapshots = $result->{snapshots} || [];
        if (@$snapshots == 0) {
            print "No snapshots\n";
        } else {
            printf "%-40s %-20s %-10s %s\n", 'ID', 'Name', 'Type', 'Created';
            foreach my $s (@$snapshots) {
                printf "%-40s %-20s %-10s %s\n",
                    $s->{id} // 'N/A', $s->{name} // 'N/A',
                    $s->{type} // 'N/A', $s->{created_at} // 'N/A';
            }
        }
        return;
    }

    if ($options->{info}) {
        my $result = Un::snapshot_get($options->{info});
        print encode_json($result) . "\n";
        return;
    }

    if ($options->{delete}) {
        Un::snapshot_delete($options->{delete});
        print "${GREEN}Snapshot deleted: $options->{delete}${RESET}\n";
        return;
    }

    if ($options->{restore}) {
        Un::snapshot_restore($options->{restore});
        print "${GREEN}Snapshot restored${RESET}\n";
        return;
    }

    if ($options->{lock}) {
        Un::snapshot_lock($options->{lock});
        print "${GREEN}Snapshot locked: $options->{lock}${RESET}\n";
        return;
    }

    if ($options->{unlock}) {
        Un::snapshot_unlock($options->{unlock});
        print "${GREEN}Snapshot unlocked: $options->{unlock}${RESET}\n";
        return;
    }

    if ($options->{clone}) {
        my $result = Un::snapshot_clone($options->{clone},
            clone_type => $options->{clone_type} || 'session',
            name => $options->{clone_name});
        print "${GREEN}Snapshot cloned${RESET}\n";
        print encode_json($result) . "\n";
        return;
    }

    print STDERR "${RED}Error: Use --list, --info, --delete, --restore, --lock, --unlock, or --clone${RESET}\n";
    exit 1;
}

sub cmd_image {
    my ($options) = @_;

    if ($options->{list}) {
        my $result = Un::image_list(filter => $options->{filter});
        my $images = $result->{images} || [];
        if (@$images == 0) {
            print "No images\n";
        } else {
            printf "%-40s %-20s %-10s %s\n", 'ID', 'Name', 'Visibility', 'Created';
            foreach my $i (@$images) {
                printf "%-40s %-20s %-10s %s\n",
                    $i->{id} // 'N/A', $i->{name} // 'N/A',
                    $i->{visibility} // 'N/A', $i->{created_at} // 'N/A';
            }
        }
        return;
    }

    if ($options->{info}) {
        my $result = Un::image_get($options->{info});
        print encode_json($result) . "\n";
        return;
    }

    if ($options->{delete}) {
        Un::image_delete($options->{delete});
        print "${GREEN}Image deleted: $options->{delete}${RESET}\n";
        return;
    }

    if ($options->{lock}) {
        Un::image_lock($options->{lock});
        print "${GREEN}Image locked: $options->{lock}${RESET}\n";
        return;
    }

    if ($options->{unlock}) {
        Un::image_unlock($options->{unlock});
        print "${GREEN}Image unlocked: $options->{unlock}${RESET}\n";
        return;
    }

    if ($options->{publish}) {
        unless ($options->{source_type}) {
            print STDERR "${RED}Error: --source-type required${RESET}\n";
            exit 1;
        }
        my $result = Un::image_publish(
            source_type => $options->{source_type},
            source_id => $options->{publish},
            name => $options->{pub_name});
        print "${GREEN}Image published${RESET}\n";
        print encode_json($result) . "\n";
        return;
    }

    if ($options->{visibility_id} && $options->{visibility_mode}) {
        Un::image_set_visibility($options->{visibility_id}, $options->{visibility_mode});
        print "${GREEN}Visibility set to $options->{visibility_mode}${RESET}\n";
        return;
    }

    if ($options->{grant_access}) {
        Un::image_grant_access($options->{grant_access}, $options->{trusted_key});
        print "${GREEN}Access granted${RESET}\n";
        return;
    }

    if ($options->{revoke_access}) {
        Un::image_revoke_access($options->{revoke_access}, $options->{trusted_key});
        print "${GREEN}Access revoked${RESET}\n";
        return;
    }

    if ($options->{list_trusted}) {
        my $result = Un::image_list_trusted($options->{list_trusted});
        print encode_json($result) . "\n";
        return;
    }

    if ($options->{transfer}) {
        Un::image_transfer($options->{transfer}, $options->{to_key});
        print "${GREEN}Image transferred${RESET}\n";
        return;
    }

    if ($options->{spawn}) {
        my $result = Un::image_spawn($options->{spawn},
            name => $options->{spawn_name},
            ports => $options->{spawn_ports} ? [map { int($_) } split(',', $options->{spawn_ports})] : undef);
        print "${GREEN}Service spawned from image${RESET}\n";
        print encode_json($result) . "\n";
        return;
    }

    if ($options->{clone}) {
        my $result = Un::image_clone($options->{clone}, name => $options->{clone_name});
        print "${GREEN}Image cloned${RESET}\n";
        print encode_json($result) . "\n";
        return;
    }

    print STDERR "${RED}Error: Use --list, --info, --delete, etc.${RESET}\n";
    exit 1;
}

sub cmd_key {
    my ($options) = @_;
    my $result = Un::validate_keys();

    if ($options->{extend}) {
        my $pk = $result->{public_key};
        if ($pk) {
            my $url = "$Un::PORTAL_BASE/keys/extend?pk=$pk";
            print "${BLUE}Opening browser to extend key...${RESET}\n";
            system("xdg-open '$url' 2>/dev/null || open '$url' 2>/dev/null &");
        }
        return;
    }

    if ($result->{expired}) {
        print "${RED}Expired${RESET}\n";
        print "Public Key: " . ($result->{public_key} // 'N/A') . "\n";
        print "Tier: " . ($result->{tier} // 'N/A') . "\n";
        print "${YELLOW}To renew: Visit $Un::PORTAL_BASE/keys/extend${RESET}\n";
        exit 1;
    }

    print "${GREEN}Valid${RESET}\n";
    print "Public Key: " . ($result->{public_key} // 'N/A') . "\n";
    print "Tier: " . ($result->{tier} // 'N/A') . "\n";
    print "Status: " . ($result->{status} // 'N/A') . "\n";
    print "Expires: " . ($result->{expires_at} // 'N/A') . "\n";
    print "Time Remaining: " . ($result->{time_remaining} // 'N/A') . "\n";
}

sub cmd_languages {
    my ($options) = @_;
    my $langs = Un::get_languages();

    if ($options->{json}) {
        print encode_json($langs) . "\n";
    } else {
        foreach my $lang (@$langs) {
            print "$lang\n";
        }
    }
}

sub show_help {
    print <<"HELP";
Unsandbox CLI - Execute code in secure sandboxes

Usage:
  perl un.pl [options] <source_file>
  perl un.pl -s <language> '<code>'
  perl un.pl session [options]
  perl un.pl service [options]
  perl un.pl service env <action> <service_id> [options]
  perl un.pl snapshot [options]
  perl un.pl image [options]
  perl un.pl languages [--json]
  perl un.pl key [--extend]

Execute options:
  -e KEY=VALUE      Environment variable (multiple allowed)
  -f FILE           Input file (multiple allowed)
  -a                Return artifacts
  -o DIR            Output directory for artifacts
  -n MODE           Network mode (zerotrust|semitrusted)
  -v N              vCPU count (1-8)
  -s LANG           Language for inline code

Session options:
  --list            List sessions
  --info ID         Get session details
  --kill ID         Terminate session
  --freeze ID       Freeze session
  --unfreeze ID     Unfreeze session
  --boost ID        Boost session (with -v)
  --unboost ID      Unboost session
  --execute ID      Execute command (with --command)
  --snapshot ID     Create snapshot
  --shell SHELL     Shell/REPL (default: bash)
  --tmux            Enable tmux persistence
  --screen          Enable screen persistence

Service options:
  --list            List services
  --info ID         Get service details
  --name NAME       Create service with name
  --ports PORTS     Comma-separated ports
  --domains DOMS    Custom domains
  --bootstrap CMD   Bootstrap command
  --bootstrap-file  Bootstrap script file
  --logs ID         Get logs
  --freeze ID       Freeze service
  --unfreeze ID     Unfreeze service
  --lock ID         Lock service
  --unlock ID       Unlock service
  --destroy ID      Destroy service
  --resize ID       Resize (with -v)
  --redeploy ID     Redeploy service
  --execute ID      Execute command (with --command)
  --snapshot ID     Create snapshot

Service env commands:
  env status ID     Check vault status
  env set ID        Set vault (use -e or --env-file)
  env export ID     Export vault contents
  env delete ID     Delete vault

Snapshot options:
  --list            List snapshots
  --info ID         Get snapshot details
  --delete ID       Delete snapshot
  --restore ID      Restore from snapshot
  --lock ID         Lock snapshot
  --unlock ID       Unlock snapshot
  --clone ID        Clone snapshot (with --clone-type, --clone-name)

Image options:
  --list            List images (with optional --filter)
  --info ID         Get image details
  --delete ID       Delete image
  --lock ID         Lock image
  --unlock ID       Unlock image
  --publish ID      Publish from service/snapshot (--source-type)
  --visibility ID MODE  Set visibility (private|unlisted|public)
  --grant-access ID     Grant access (with --trusted-key)
  --revoke-access ID    Revoke access (with --trusted-key)
  --list-trusted ID     List trusted keys
  --transfer ID         Transfer ownership (with --to-key)
  --spawn ID            Spawn service from image
  --clone ID            Clone image
HELP
    exit 1;
}

sub main {
    my %options = (
        env => [],
        files => []
    );

    my $i = 0;
    while ($i < @ARGV) {
        my $arg = $ARGV[$i];

        if ($arg eq 'session' || $arg eq 'service' || $arg eq 'snapshot' ||
            $arg eq 'image' || $arg eq 'key' || $arg eq 'languages') {
            $options{command} = $arg;
        } elsif ($arg eq '-e') {
            push @{$options{env}}, $ARGV[++$i];
        } elsif ($arg eq '-f') {
            push @{$options{files}}, $ARGV[++$i];
        } elsif ($arg eq '-a') {
            $options{artifacts} = 1;
        } elsif ($arg eq '-o') {
            $options{output_dir} = $ARGV[++$i];
        } elsif ($arg eq '-n') {
            $options{network} = $ARGV[++$i];
        } elsif ($arg eq '-v') {
            $options{vcpu} = int($ARGV[++$i]);
        } elsif ($arg eq '-s' || $arg eq '--shell') {
            $options{language} = $ARGV[++$i] if !$options{command};
            $options{shell} = $ARGV[$i] if $options{command} && $options{command} eq 'session';
        } elsif ($arg eq '-l' || $arg eq '--list') {
            $options{list} = 1;
        } elsif ($arg eq '--info') {
            $options{info} = $ARGV[++$i];
        } elsif ($arg eq '--kill') {
            $options{kill} = $ARGV[++$i];
        } elsif ($arg eq '--freeze') {
            $options{freeze} = $ARGV[++$i];
        } elsif ($arg eq '--unfreeze') {
            $options{unfreeze} = $ARGV[++$i];
        } elsif ($arg eq '--lock') {
            $options{lock} = $ARGV[++$i];
        } elsif ($arg eq '--unlock') {
            $options{unlock} = $ARGV[++$i];
        } elsif ($arg eq '--boost') {
            $options{boost} = $ARGV[++$i];
        } elsif ($arg eq '--unboost') {
            $options{unboost} = $ARGV[++$i];
        } elsif ($arg eq '--destroy') {
            $options{destroy} = $ARGV[++$i];
        } elsif ($arg eq '--resize') {
            $options{resize} = $ARGV[++$i];
        } elsif ($arg eq '--redeploy') {
            $options{redeploy} = $ARGV[++$i];
        } elsif ($arg eq '--logs') {
            $options{logs} = $ARGV[++$i];
        } elsif ($arg eq '--lines') {
            $options{lines} = $ARGV[++$i];
        } elsif ($arg eq '--execute') {
            $options{execute} = $ARGV[++$i];
        } elsif ($arg eq '--command') {
            $options{command_str} = $ARGV[++$i];
        } elsif ($arg eq '--name') {
            $options{name} = $ARGV[++$i];
        } elsif ($arg eq '--ports') {
            $options{ports} = $ARGV[++$i];
        } elsif ($arg eq '--domains') {
            $options{domains} = $ARGV[++$i];
        } elsif ($arg eq '--type') {
            $options{type} = $ARGV[++$i];
        } elsif ($arg eq '--bootstrap') {
            $options{bootstrap} = $ARGV[++$i];
        } elsif ($arg eq '--bootstrap-file') {
            $options{bootstrap_file} = $ARGV[++$i];
        } elsif ($arg eq '--env-file') {
            $options{env_file} = $ARGV[++$i];
        } elsif ($arg eq '--snapshot') {
            $options{snapshot} = $ARGV[++$i];
        } elsif ($arg eq '--snapshot-name') {
            $options{snapshot_name} = $ARGV[++$i];
        } elsif ($arg eq '--hot') {
            $options{hot} = 1;
        } elsif ($arg eq '--restore') {
            $options{restore} = $ARGV[++$i];
        } elsif ($arg eq '--delete') {
            $options{delete} = $ARGV[++$i];
        } elsif ($arg eq '--clone') {
            $options{clone} = $ARGV[++$i];
        } elsif ($arg eq '--clone-type') {
            $options{clone_type} = $ARGV[++$i];
        } elsif ($arg eq '--clone-name') {
            $options{clone_name} = $ARGV[++$i];
        } elsif ($arg eq '--publish') {
            $options{publish} = $ARGV[++$i];
        } elsif ($arg eq '--source-type') {
            $options{source_type} = $ARGV[++$i];
        } elsif ($arg eq '--visibility') {
            $options{visibility_id} = $ARGV[++$i];
            $options{visibility_mode} = $ARGV[++$i];
        } elsif ($arg eq '--spawn') {
            $options{spawn} = $ARGV[++$i];
        } elsif ($arg eq '--grant-access') {
            $options{grant_access} = $ARGV[++$i];
        } elsif ($arg eq '--revoke-access') {
            $options{revoke_access} = $ARGV[++$i];
        } elsif ($arg eq '--list-trusted') {
            $options{list_trusted} = $ARGV[++$i];
        } elsif ($arg eq '--trusted-key') {
            $options{trusted_key} = $ARGV[++$i];
        } elsif ($arg eq '--transfer') {
            $options{transfer} = $ARGV[++$i];
        } elsif ($arg eq '--to-key') {
            $options{to_key} = $ARGV[++$i];
        } elsif ($arg eq '--filter') {
            $options{filter} = $ARGV[++$i];
        } elsif ($arg eq '--tmux') {
            $options{tmux} = 1;
        } elsif ($arg eq '--screen') {
            $options{screen} = 1;
        } elsif ($arg eq '--unfreeze-on-demand') {
            $options{unfreeze_on_demand} = 1;
        } elsif ($arg eq '--set-unfreeze-on-demand') {
            $options{set_unfreeze_on_demand} = $ARGV[++$i];
            $options{set_unfreeze_on_demand_value} = ($ARGV[++$i] =~ /^(true|1)$/i);
        } elsif ($arg eq '--json') {
            $options{json} = 1;
        } elsif ($arg eq '--extend') {
            $options{extend} = 1;
        } elsif ($arg eq 'env' && $options{command} && $options{command} eq 'service') {
            $options{env_action} = $ARGV[++$i];
            $options{env_target} = $ARGV[++$i] if defined $ARGV[$i+1] && $ARGV[$i+1] !~ /^-/;
        } elsif ($arg eq '--account') {
            $Un::ACCOUNT_INDEX = int($ARGV[++$i]);
        } elsif ($arg eq '--help' || $arg eq '-h') {
            show_help();
        } elsif ($arg =~ /^-/) {
            print STDERR "${RED}Unknown option: $arg${RESET}\n";
            exit 1;
        } else {
            if ($options{language} && !$options{inline_code}) {
                $options{inline_code} = $arg;
            } else {
                $options{source_file} = $arg;
            }
        }
        $i++;
    }

    # Handle commands
    if ($options{command}) {
        if ($options{command} eq 'session') {
            cmd_session(\%options);
        } elsif ($options{command} eq 'service') {
            if ($options{env_action}) {
                cmd_service_env($options{env_action}, $options{env_target},
                    $options{env}, $options{env_file});
            } else {
                $options{command} = $options{command_str} if $options{command_str};
                cmd_service(\%options);
            }
        } elsif ($options{command} eq 'snapshot') {
            cmd_snapshot(\%options);
        } elsif ($options{command} eq 'image') {
            cmd_image(\%options);
        } elsif ($options{command} eq 'languages') {
            cmd_languages(\%options);
        } elsif ($options{command} eq 'key') {
            cmd_key(\%options);
        }
    } elsif ($options{source_file} || $options{inline_code}) {
        cmd_execute(\%options);
    } else {
        show_help();
    }
}

main() unless caller;
1;
