#!/usr/bin/env perl
#ABSTRACT: List your jobs (or others), and delete them if you wish
#PODNAME: lsjobs

use v5.12;
use warnings;
use utf8;
use Getopt::Long;
use FindBin qw($RealBin);
use Data::Dumper;
use Term::ANSIColor qw(:constants);
use File::Basename;
use Text::ASCIITable;
$Data::Dumper::Sortkeys = 1;

# Enable UTF-8 output
binmode(STDOUT, ':utf8');
binmode(STDERR, ':utf8');

# Box drawing characters for modern table rendering
my $BOX = {
    tl => '┌',  tr => '┐',  bl => '└',  br => '┘',  # corners
    h  => '─',  v  => '│',                           # lines
    t  => '┬',  b  => '┴',  l  => '├',  r  => '┤',  # T-junctions
    c  => '┼',                                       # cross
};

if (-e "$RealBin/../dist.ini") {
    say STDERR "[dev mode] Using local lib"  if ($ENV{"DEBUG"});
    use lib "$RealBin/../lib";
} 

use NBI::Slurm;
use Cwd;




my $current_slurm_jobid = $ENV{SLURM_JOBID} // -1;
my $unix_username = $ENV{USER};
my $user_home_dir = $ENV{HOME};

my $opt_user = $unix_username;
my $opt_status = '.+';
my $opt_running_bool = 0;
my $opt_pending_bool = 0;
my $opt_delete_bool = 0;
my $opt_verbose_bool = 0;
my $opt_queue = '.+';
my $opt_name = '.+';
my $opt_tab = 0;
my @opt_hide = ();
GetOptions(
    'u|user=s'  => \$opt_user,
    'n|name=s'  => \$opt_name,
    's|status=s'=> \$opt_status,
    'r|running' => \$opt_running_bool,
    'p|pending' => \$opt_pending_bool,
    'd|delete'  => \$opt_delete_bool,
    't|tab'     => \$opt_tab,
    'hide=s'    => \@opt_hide,
    'verbose'   => \$opt_verbose_bool,
    'version'   => sub { say "lsjobs v", $NBI::Slurm::VERSION; exit },
    'help'      => sub { usage() },
);

if (not NBI::Slurm::has_squeue()) {
    say STDERR RED, "Error: ", RESET, "squeue not found in PATH. Are you in the cluster?";
    exit 1;
}

my $jobs = getjobs();

# Check if we got any data
if (not defined $jobs or ref($jobs) ne 'HASH') {
    say STDERR RED, "Error: ", RESET, "Failed to retrieve job information";
    exit 1;
}
my @ids = ();
for my $positional (@ARGV) {
    if ($positional =~ /^(\d+)$/) {
        push(@ids, $1);
    } else {
        if ($opt_name eq '.+') {
            $opt_name = $positional;
        } else {
            say STDERR "Error: unknown positional argument: $positional";
            usage();
        }
    }
}

if ($opt_user eq 'ALL' or $opt_user eq 'all') {
    $opt_user = '.+';
}
if ($opt_verbose_bool) {
    say STDERR "User: $opt_user";
    say STDERR "Jobs: ", scalar(keys %{$jobs});
}

my $selected_jobs = {};
my $selected_arrays = [['JobID', 'User', 'Queue', 'Name', 'State', 'Time', 'TotalTime', 'NodeList', 'CPUS', 'Memory', 'Reason']];

if ($opt_tab) {
    # Add "#" prefix for TSV mode
    $selected_arrays->[0]->[0] = "#" . $selected_arrays->[0]->[0];
}

for my $job (sort keys %{$jobs}) {
    # Check user (full match)
    if ($jobs->{$job}->{USER} !~ /^$opt_user$/) {
        next;
    }
    # Check queue (partial match ok)
    if ($jobs->{$job}->{PARTITION} !~ /$opt_queue/) {
        next;
    }

    # Check name
    if ($jobs->{$job}->{NAME} !~ /$opt_name/) {
        next;
    }
    # Check status (general pattern match)
    if ($jobs->{$job}->{STATE} !~ /$opt_status/) {
        next;
    }
    # Check specific status flags (these override general status pattern)
    if ($opt_pending_bool  and $jobs->{$job}->{STATE} ne 'PENDING') {
        next;
    }
    if ($opt_running_bool  and $jobs->{$job}->{STATE} ne 'RUNNING') {
        next;
    }
    if (scalar @ids > 0 and not grep {$_ eq $job} @ids) {
        next;
    }

    # Format job ID for display (handle array jobs)
    my $display_jobid = $jobs->{$job}->{JOBID};
    if (defined $display_jobid && $display_jobid =~ /_/) {
        # Array job format: convert "12345_1" to "12345#"
        $display_jobid =~ s/_.*/#/;
    }

    my $array = [$display_jobid // 'N/A',
                 $jobs->{$job}->{USER} // 'N/A',
                 $jobs->{$job}->{PARTITION} // 'N/A',
                 $jobs->{$job}->{NAME} // 'N/A',
                 $opt_tab ? ($jobs->{$job}->{STATE} // 'N/A') : state_string($jobs->{$job}->{STATE} // 'UNKNOWN'),
                 $jobs->{$job}->{TIME} // '0:00',
                 $jobs->{$job}->{TIME_LIMIT} // 'N/A',
                 $jobs->{$job}->{NODELIST} // 'N/A',
                 $jobs->{$job}->{"CPUS"} // 'N/A',
                 $jobs->{$job}->{"MIN_MEMORY"} // 'N/A',
                 $opt_tab ? ($jobs->{$job}->{"REASON"} // 'N/A')  :  reason_string($jobs->{$job}->{"REASON"} // 'None')
                 ];
    push(@{$selected_arrays}, $array); 


}
# Apply column hiding if requested
if (@opt_hide && !$opt_tab) {
    $selected_arrays = hide_columns($selected_arrays, \@opt_hide);
}

# Print summary header (not in tab mode)
if (!$opt_tab) {
    print_summary($jobs, $selected_arrays);
}

if ($opt_tab) {
    for my $array (@{$selected_arrays}) {
        say join("\t", @{$array});
    }
} else {
    # Render modern table with Unicode box drawing
    render_table_modern(@{$selected_arrays});
    print RESET, "\n";
}

## Print single job
if ($opt_verbose_bool and scalar @{$selected_arrays} == 3) {
    my $job = extractJobByID($jobs, $selected_arrays->[2]->[0]);
    for my $key (sort keys %{$job}) {
        # Filter useless values
        if ($job->{$key} =~ /^$/ or $job->{$key} =~ /^(\(null\)|\*)$/) {
            next;
        }
        if ($key =~/(S_C_T|USER|ACCOUNT)/) {
            next;
        }
        say YELLOW, sprintf("%-20s", $key), RESET, $job->{$key};
    }
}   

my @selected_ids = joblist_to_ids(@{$selected_arrays});


if ($opt_delete_bool and (scalar @selected_ids)) {
    say RED "\nDELETE JOBS:", RESET;
    if (prompt("Delete " . scalar(@selected_ids) . " jobs?", 'n') =~ /^(y|Y|yes|YES)$/) {
        my $command = "scancel " . join(" ", @selected_ids);
        my $result = system($command);
        if ($result == -1) {
            say RED, "ERROR", RESET, ": Failed to execute scancel: $!";
        } elsif ($result & 127) {
            say RED, "ERROR", RESET, ": scancel died with signal ", ($result & 127);
        } elsif (my $exit_code = $result >> 8) {
            say RED, "ERROR", RESET, ": scancel exited with status $exit_code";
        } else {
            say GREEN, "Success", RESET, ": Deleted ", scalar(@selected_ids), " job(s)";
        }
    } else {
        say "Deletion cancelled.";
    }

} elsif ($opt_delete_bool) {
    say STDERR YELLOW, "Warning: ", RESET, "No jobs selected for deletion";
}

sub state_string {
    my $s = shift // 'UNKNOWN';
    my ($icon, $color, $text);

    if ($s =~ /^R/i) {
        $icon = '▶';
        $color = GREEN . BOLD;
        $text = 'RUN';
    } elsif ($s =~ /^P/i) {
        $icon = '◷';
        $color = YELLOW . BOLD;
        $text = 'PND';
    } elsif ($s =~ /^C/i) {
        $icon = '✓';
        $color = CYAN . BOLD;
        $text = 'CMP';
    } elsif ($s =~ /^F/i) {
        $icon = '✗';
        $color = RED . BOLD;
        $text = 'FLD';
    } elsif ($s =~ /^S/i) {
        $icon = '■';
        $color = RED . BOLD;
        $text = 'STP';
    } else {
        $icon = '?';
        $color = WHITE;
        $text = substr($s, 0, 3);
    }

    return $color . $icon . ' ' . $text . RESET;
}
sub reason_string {
    my $s = shift // 'None';
    my ($icon, $color);

    if ($s =~ /^None/i) {
        $icon = '✓';
        $color = CYAN;
        return $color . $icon . ' ' . $s . RESET;
    } elsif ($s =~ /^Priority/i) {
        $icon = '↑';    # was ⏳ — 2-wide emoji on modern terminals
        $color = YELLOW;
    } elsif ($s =~ /^Resources/i) {
        $icon = '↯';    # was ⚡ — 2-wide emoji on modern terminals
        $color = YELLOW;
    } elsif ($s =~ /^Dependency/i) {
        $icon = '→';    # was ⛓ — 2-wide emoji on modern terminals
        $color = BLUE;
    } elsif ($s =~ /^(Bad|Error|Invalid)/i) {
        $icon = '✗';
        $color = RED . BOLD;
    } elsif ($s =~ /^QOS/i) {
        $icon = '!';    # was ⚠ — ambiguous-width on modern terminals
        $color = YELLOW;
    } else {
        $icon = '•';
        $color = WHITE;
    }

    return $color . $icon . ' ' . $s . RESET;
}
sub joblist_to_ids {
    # Receive a list of lists (all same length) and returns a list of jobids
    my @rows = @_;
    my @ids = ();
    # remove first two rows
    
    for my $row (@rows) {
        # Skip non numeric values
        
        next if ($row->[0] !~ /^\d+$/);

        push @ids, $row->[0];
        
       
    }

    return @ids;
    
}
sub short_job {
    # Print a line of minimal information about a job
    my $line_width = get_terminal_width();
    my $job = shift;
    my $jobid = $job->{JOBID};
    my $name = $job->{NAME};
    my $state = $job->{STATE};
    my $user = $job->{USER};
    my $queue = $job->{PARTITION};
    my $time = $job->{TIME};
    # Return a string sorther than $line_width
    my $line = sprintf("%-10s %-10s %-10s %-10s %-10s %-10s", $jobid, $name, $state, $user, $queue, $time);
    return $line;
}

sub print_summary {
    my ($jobs, $selected_arrays) = @_;
    my %stats = (running => 0, pending => 0, completed => 0, failed => 0, other => 0);

    # Count job states
    for my $job (values %$jobs) {
        my $state = $job->{STATE} // '';
        if ($state =~ /^R/i) {
            $stats{running}++;
        } elsif ($state =~ /^P/i) {
            $stats{pending}++;
        } elsif ($state =~ /^C/i) {
            $stats{completed}++;
        } elsif ($state =~ /^F/i) {
            $stats{failed}++;
        } else {
            $stats{other}++;
        }
    }

    my $total = scalar(keys %$jobs);
    my $showing = scalar(@$selected_arrays) - 1;  # -1 for header row

    say '';
    say BOLD . CYAN . '╔════════════════════════════════════════════════════════════════╗' . RESET;
    say BOLD . CYAN . '║' . RESET . '  ' . BOLD . 'SLURM Job Queue Summary' . RESET . ' ' x 38 . BOLD . CYAN . '║' . RESET;
    say BOLD . CYAN . '╚════════════════════════════════════════════════════════════════╝' . RESET;
    say '';

    # Stats line
    my @stat_parts;
    push @stat_parts, sprintf('%s %-9s %s%3d%s',
        GREEN . BOLD . '▶' . RESET, 'Running:', GREEN . BOLD, $stats{running}, RESET) if $stats{running};
    push @stat_parts, sprintf('%s %-9s %s%3d%s',
        YELLOW . BOLD . '◷' . RESET, 'Pending:', YELLOW . BOLD, $stats{pending}, RESET) if $stats{pending};
    push @stat_parts, sprintf('%s %-9s %s%3d%s',
        CYAN . '✓' . RESET, 'Complete:', CYAN, $stats{completed}, RESET) if $stats{completed};
    push @stat_parts, sprintf('%s %-9s %s%3d%s',
        RED . BOLD . '✗' . RESET, 'Failed:', RED . BOLD, $stats{failed}, RESET) if $stats{failed};
    push @stat_parts, sprintf('%s %-9s %s%3d%s',
        WHITE . '•' . RESET, 'Other:', WHITE, $stats{other}, RESET) if $stats{other};

    if (@stat_parts) {
        say '  ' . join('  ', @stat_parts);
    }

    say '';
    say sprintf('  %s Showing: %s%d%s of %s%d%s total jobs',
        CYAN . BOLD . '→' . RESET,
        BOLD . CYAN, $showing, RESET,
        BOLD, $total, RESET
    );
    say '';
}

sub hide_columns {
    my ($arrays_ref, $hide_patterns) = @_;
    return $arrays_ref unless @$hide_patterns;

    my @arrays = @$arrays_ref;
    return \@arrays unless @arrays;

    my $header = $arrays[0];
    my @keep_indices;

    # Determine which columns to keep
    for my $i (0..$#{$header}) {
        my $col_name = $header->[$i];
        my $should_hide = 0;

        for my $pattern (@$hide_patterns) {
            # Case insensitive substring match (minimum 3 chars)
            if (length($pattern) >= 3 && $col_name =~ /\Q$pattern\E/i) {
                $should_hide = 1;
                last;
            }
        }

        push @keep_indices, $i unless $should_hide;
    }

    # If all columns would be hidden, keep them all
    return \@arrays unless @keep_indices;

    # Filter each row to keep only non-hidden columns
    my @filtered_arrays;
    for my $row (@arrays) {
        my @filtered_row = map { $row->[$_] } @keep_indices;
        push @filtered_arrays, \@filtered_row;
    }

    return \@filtered_arrays;
}

sub ansi_first_char {
    # Return the first visible glyph of an ANSI-coloured string,
    # preserving any leading colour escape sequences.
    my $s = shift;
    my $prefix = '';
    while ($s =~ s/^(\e\[[0-9;]*m)//) {
        $prefix .= $1;
    }
    return $prefix . substr($s, 0, 1) . RESET;
}

sub fit_columns_to_terminal {
    # Given natural column widths and the header row, return:
    #   ($fitted_widths_ref, $hidden_hashref, $icon_only_hashref)
    # Strategy (in order):
    #   1. Shrink flexible cols (name, nodelist, reason) down to their minimums
    #   2. Hide NodeList entirely  (optional column, first to go)
    #   3. Hide Reason entirely    (optional column, second to go)
    #   4. Collapse State to icon-only glyph
    #   5. Last-resort: truncate whatever is still widest
    my ($widths, $header) = @_;
    my $terminal_width = get_terminal_width();

    my @fitted    = @$widths;
    my %hidden    = ();
    my %icon_only = ();

    # Map normalised column name -> index
    my %col_idx;
    for my $i (0..$#$header) {
        my $name = lc($header->[$i]);
        $name =~ s/[^a-z0-9]//g;
        $col_idx{$name} = $i;
    }

    # Rendered width: left border + each visible col (content + 2 spaces + right border)
    my $calc_width = sub {
        my $w = 1;
        for my $i (0..$#fitted) {
            next if $hidden{$i};
            $w += $fitted[$i] + 3;
        }
        return $w;
    };

    return (\@fitted, \%hidden, \%icon_only) if $calc_width->() <= $terminal_width;

    # Step 1: Shrink Name/NodeList/Reason down to minimums
    my %min_widths = (name => 10, nodelist => 8, reason => 8);
    my $progress = 1;
    while ($calc_width->() > $terminal_width && $progress) {
        $progress = 0;
        my ($best, $best_w) = (undef, 0);
        for my $name (qw(name nodelist reason)) {
            my $i = $col_idx{$name};
            next unless defined $i;
            next if $hidden{$i};
            my $min = $min_widths{$name};
            if ($fitted[$i] > $min && $fitted[$i] > $best_w) {
                $best   = $name;
                $best_w = $fitted[$i];
            }
        }
        if (defined $best) {
            my $i      = $col_idx{$best};
            my $min    = $min_widths{$best};
            my $excess = $calc_width->() - $terminal_width;
            my $cut    = $fitted[$i] - $min;
            $cut = $excess if $excess < $cut;
            $fitted[$i] -= $cut;
            $progress = 1 if $cut > 0;
        }
    }

    # Step 2: Hide NodeList (optional, first to go)
    if ($calc_width->() > $terminal_width) {
        my $i = $col_idx{nodelist};
        $hidden{$i} = 1 if defined $i;
    }

    # Step 3: Hide Reason (optional, second to go)
    if ($calc_width->() > $terminal_width) {
        my $i = $col_idx{reason};
        $hidden{$i} = 1 if defined $i;
    }

    # Step 4: Collapse State to a single icon glyph
    if ($calc_width->() > $terminal_width) {
        my $i = $col_idx{state};
        if (defined $i && !$hidden{$i}) {
            $icon_only{$i} = 1;
            $fitted[$i]    = 1;
        }
    }

    # Step 5: Last-resort generic truncation of whatever is still widest
    $progress = 1;
    while ($calc_width->() > $terminal_width && $progress) {
        $progress = 0;
        my ($best_i, $best_w) = (-1, 0);
        for my $i (0..$#fitted) {
            next if $hidden{$i} || $icon_only{$i};
            if ($fitted[$i] > 4 && $fitted[$i] > $best_w) {
                $best_i = $i;
                $best_w = $fitted[$i];
            }
        }
        if ($best_i >= 0) {
            my $excess = $calc_width->() - $terminal_width;
            my $cut    = $fitted[$best_i] - 4;
            $cut = $excess if $excess < $cut;
            $fitted[$best_i] -= $cut;
            $progress = 1 if $cut > 0;
        }
    }

    return (\@fitted, \%hidden, \%icon_only);
}

sub render_table_modern {
    # Receive a list of lists (all same length) and print a modern table with Unicode borders
    my @rows = @_;
    return unless @rows;

    my $n_cols = scalar(@{$rows[0]});

    # Calculate maximum widths for each column
    my @max_widths = ();
    for my $col (0..$n_cols-1) {
        my $max_width = 0;
        for my $row (@rows) {
            my $width = ascii_len($row->[$col]);
            $max_width = $width if ($width > $max_width);
        }
        push(@max_widths, $max_width);
    }

    # Fit columns to terminal: truncate, hide optional cols, collapse State to icon
    my ($fitted_widths, $hidden_cols, $icon_only_cols) =
        fit_columns_to_terminal(\@max_widths, $rows[0]);

    # Print top border
    print_border_line('top', $fitted_widths, $hidden_cols);

    # Print header row (first row)
    if (@rows > 0) {
        print_table_row($rows[0], $fitted_widths, 'header', $hidden_cols, $icon_only_cols);
        print_border_line('middle', $fitted_widths, $hidden_cols);
    }

    # Print data rows
    for my $i (1..$#rows) {
        print_table_row($rows[$i], $fitted_widths, 'data', $hidden_cols, $icon_only_cols);
    }

    # Print bottom border
    print_border_line('bottom', $fitted_widths, $hidden_cols);
}

sub print_border_line {
    my ($type, $widths, $hidden) = @_;
    $hidden //= {};
    my ($left, $mid, $right, $fill);

    if ($type eq 'top') {
        ($left, $mid, $right, $fill) = ($BOX->{tl}, $BOX->{t}, $BOX->{tr}, $BOX->{h});
    } elsif ($type eq 'middle') {
        ($left, $mid, $right, $fill) = ($BOX->{l}, $BOX->{c}, $BOX->{r}, $BOX->{h});
    } else {  # bottom
        ($left, $mid, $right, $fill) = ($BOX->{bl}, $BOX->{b}, $BOX->{br}, $BOX->{h});
    }

    my @visible = grep { !$hidden->{$_} } 0..$#{$widths};
    print CYAN . $left . RESET;
    for my $j (0..$#visible) {
        my $i = $visible[$j];
        print CYAN . ($fill x ($widths->[$i] + 2)) . RESET;
        print CYAN . ($j == $#visible ? $right : $mid) . RESET;
    }
    say '';
}

sub print_table_row {
    my ($row, $widths, $style, $hidden, $icon_only) = @_;
    $hidden    //= {};
    $icon_only //= {};

    print CYAN . $BOX->{v} . RESET;
    for my $i (0..$#{$row}) {
        next if $hidden->{$i};

        my $cell  = $row->[$i];
        my $width = $widths->[$i];

        # Icon-only mode: show just the leading glyph (used for State when space is tight)
        if ($icon_only->{$i}) {
            my $icon = ansi_first_char($cell);
            print " $icon " . RESET;
            print CYAN . $BOX->{v} . RESET;
            next;
        }

        my $stripped = $cell;
        $stripped =~ s/\e\[[0-9;]*m//g;  # Strip ANSI codes

        my $prefix = ($style eq 'header') ? BOLD . WHITE : '';

        if (length($stripped) > $width) {
            # Truncate and mark with a coloured '>' ellipsis
            my $visible = substr($stripped, 0, $width - 1);
            print " $prefix$visible" . YELLOW . BOLD . '>' . RESET . ' ';
        } else {
            # Normal padded cell — preserve ANSI codes in the original value
            my $tmpline = sprintf(" %s%-*s ", $prefix, $width, $stripped);
            my $index = index($tmpline, $stripped);
            if ($index >= 0) {
                substr($tmpline, $index, length($stripped), $cell);
            }
            print $tmpline . RESET;
        }
        print CYAN . $BOX->{v} . RESET;
    }
    say '';
}

sub render_table {
    # Receive a list of lists (all same length) and print a table not larger than $line_width
    
    # @_ is an array of array references
    my @rows = @_;
    my $n_cols = scalar(@{$rows[0]});
    my $line_width = get_terminal_width() - $n_cols - 1;
    # For each column, evaluate the maximum string contained in that column
    my @max_widths = ();
    for my $col (0..$n_cols-1) {
        my $max_width = 0;
        for my $row (@rows) {
            my $width = ascii_len($row->[$col]);
            $max_width = $width if ($width > $max_width);
        }
        push(@max_widths, $max_width);
    }
    # Now print the table
    for my $row (@rows) {
        my $line = WHITE . ON_BLACK;
        for my $col (0..$n_cols-1) {
            my $width = $max_widths[$col];
            my $cell = $row->[$col];
            my $stripped = $cell;
            $stripped  =~  s/\e\[[0-9;]*m//g;
            my $tmpline = sprintf("|%-${width}s ", $stripped);
            # In tmpline replace $stripped with $cell, without using regex
            my $index = index($tmpline, $stripped);
            substr($tmpline, $index, length($stripped), $cell);
            $line .= $tmpline;
        }
        say $line, "|";
    }
    print RESET;
}

sub ascii_len {
    my $string = shift;
    # Return legnth excluding ANSI escape sequences
    $string =~ s/\e\[[0-9;]*m//g;
    return length($string);
}

sub extractJobByID {
    my ($jobs, $id) = @_;
    my $job = {};
    for my $jobid (keys %{$jobs}) {
        if ($jobid eq $id) {
            $job = $jobs->{$jobid};
            last;
        }
    }
    return $job;
}
sub getjobs {
    # Create an anonymous hash, and return it
    my $jobs = {};
    # Use specific format for efficiency - only get fields we need
    my $cmd = q(squeue --format='%i|%u|%P|%j|%T|%M|%l|%N|%C|%m|%r');
    my @output = `$cmd 2>&1`;

    # Check if command failed
    if ($? != 0) {
        say STDERR RED, "Error: ", RESET, "Failed to execute squeue command";
        say STDERR "Output: ", join("\n", @output) if @output;
        return $jobs;
    }

    my $c = 0;
    my @header = ();
    for my $line (@output) {
        chomp $line;
        next if $line =~ /^\s*$/;  # Skip empty lines

        my @fields = split(/\|/, $line);
        $c++;
        if ($c == 1 ) {
            # Field names
            for my $field (@fields) {
                push(@header, stripchars($field));
            }
        } else {
            # Job info
            my $job = {};
            if (scalar(@fields) != scalar(@header)) {
                say STDERR YELLOW, "Warning: ", RESET, "Skipping malformed line (field count mismatch)";
                if ($ENV{DEBUG}) {
                    say STDERR "  Expected: ", scalar(@header), " fields";
                    say STDERR "  Got: ", scalar(@fields), " fields";
                    say STDERR "  Line: $line";
                }
                next;  # Skip this line instead of exiting
            }
            for my $i (0..$#header) {
                $job->{"$header[$i]"} = $fields[$i] if (not defined $job->{"$header[$i]"});
            }

            # Ensure we have a valid JOBID before adding
            if (defined $job->{JOBID} && $job->{JOBID} ne '') {
                $jobs->{$job->{JOBID}} = $job;
            } else {
                say STDERR YELLOW, "Warning: ", RESET, "Skipping job with missing JOBID" if $ENV{DEBUG};
            }

        }

    }

    return $jobs;
}


sub get_terminal_width {
    my $terminal_width = `tput cols 2>/dev/null`;
    chomp($terminal_width) if defined $terminal_width;

    # Validate that we got a positive integer
    if (defined $terminal_width && $terminal_width =~ /^\d+$/ && $terminal_width > 20) {
        return $terminal_width;
    }

    # Fallback to default width
    return 80;
}

sub stripchars {
    my $string = shift;
    # replace non alphanumeric characters with _
    $string =~ s/[^A-Za-z0-9]/_/g;
    return $string;
}

sub prompt {
    my ($message, $default) = @_;
    my $prompt = "$message [$default]: ";
    print $prompt;
    my $answer = <STDIN>;
    chomp $answer;
    $answer = $default if ($answer eq '');
    return $answer;
}

sub usage {
    say <<END;
  Usage: lsjobs [options] [jobid ... | pattern ]
  ----------------------------------------------
  Options:
   -u, --user <username>   Show only jobs from this user [default: $unix_username]
                           Type 'all' to show all users

   -n, --name <pattern>    Show only jobs with this name (regex) [default: .+]

   -s, --status <pattern>  Show only jobs matching this status (regex) [default: .+]
   -r, --running           Show only running jobs
   -p, --pending           Show only pending jobs
   -t, --tab               Output in simple TSV format (pipe to vd for interactive table)

   --hide <column>         Hide column from display (case insensitive substring match,
                           minimum 3 characters). Can be specified multiple times.
                           Examples: --hide time --hide memory --hide nodelist

   -d, --delete            Delete the selected jobs (with confirmation prompt)
   --verbose               Show verbose output
   --version               Show version and exit
   --help                  Show this help message
END
    exit;

}

__END__

=pod

=encoding UTF-8

=head1 NAME

lsjobs - List your jobs (or others), and delete them if you wish

=head1 VERSION

version 0.16.1

=head1 SYNOPSIS

  lsjobs [options] [jobid ... | pattern]

=head1 DESCRIPTION

This script lists the jobs and provides the option to delete them. 
It allows filtering the jobs based on various criteria such as user, name, and status.

=head1 OPTIONS

=over 4

=item B<-u, --user <username>>

Show only jobs from the specified user. Default: current user.

=item B<-n, --name <pattern>>

Show only jobs with the specified name pattern. Default: .+ (matches any name).

=item B<-s, --status <pattern>>

Show only jobs with the specified status pattern (regex). Default: .+ (matches any status).

=item B<-r, --running>

Show only running jobs. This is a convenience flag equivalent to C<-s RUNNING>.

=item B<-p, --pending>

Show only pending jobs. This is a convenience flag equivalent to C<-s PENDING>.

=item B<-t, --tab>

Output in simple TSV format (tip: pipe to C<vd> for interactive table)

=item B<--hide <column>>

Hide specified column from display. Uses case-insensitive substring matching with a minimum of 3 characters. Can be specified multiple times to hide multiple columns. Examples: C<--hide time --hide memory --hide nodelist>

=item B<-d, --delete>

Delete the selected jobs. This option must be used with caution, but an interactive prompt is provided

=item B<--verbose>

Display verbose output.

If a single job is selected, for example by giving a precise ID, the full job details will be displayed.

=item B<--help>

Print the help message and exit.

=back

=head1 ARGUMENTS

=over 4

=item B<jobid ... | pattern>

Optional. Specify either job IDs (many) or a pattern (single) to filter the jobs based on their names.

=back

=head1 EXAMPLES

=over 4

=item B<Example 1:>

List all jobs:

  lsjobs

=item B<Example 2:>

List jobs with the name "myjob":

  lsjobs -n myjob

=item B<Example 3:>

List running jobs of a specific user:

  lsjobs -r -u username

=item B<Example 4:>

Delete some of my jobs (only pending, and with name containing MEGAHIT):

  lsjobs -d --pending MEGAHIT

=item B<Example 5:>

List jobs with a simplified view (hide some columns):

  lsjobs --hide nodelist --hide reason --hide totaltime

=back

=head1 AUTHOR

Andrea Telatin <proch@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2023-2025 by Andrea Telatin.

This is free software, licensed under:

  The MIT (X11) License

=cut
