#!/usr/bin/env perl

use strict;
use warnings;
use JSON::PP;
use IO::File;
use FindBin;
use Term::ANSIColor;
use Getopt::Long qw(GetOptions :config no_ignore_case bundling no_pass_through);
use lib "$FindBin::Bin/../lib";
use JQ::Lite;
use JQ::Lite::Util ();

my $decoder;
my $decoder_module;
my $decoder_debug   = 0;
my $decoder_choice;
my $raw_input       = 0;
my $raw_output      = 0;
my $compact_output  = 0;
my $ascii_output    = 0;
my $color_output    = 0;
my $null_input      = 0;
my $slurp_input     = 0;
my @query_files;
my $force_yaml      = 0;
my $exit_status     = 0;
my $yaml_module;
my $yaml_loader;
my %arg_vars;
my @slurpfiles;
my $slurpfile_probably_missing_file = 0;

sub _usage_error {
    my ($message) = @_;
    $message ||= 'invalid usage';
    $message =~ s/\s+\z//;
    warn "[USAGE]$message\n";
    exit 5;
}

sub _input_error {
    my ($message) = @_;
    $message ||= 'input error';
    $message =~ s/\s+\z//;
    warn "[INPUT]$message\n";
    exit 4;
}

sub _compile_error {
    my ($message) = @_;
    $message ||= 'compile error';
    $message =~ s/\s+\z//;
    warn "[COMPILE]$message\n";
    exit 2;
}

sub _runtime_error {
    my ($message) = @_;
    $message ||= 'runtime error';
    $message =~ s/\s+\z//;
    warn "[RUNTIME]$message\n";
    exit 3;
}

sub _validate_query_syntax {
    my ($query) = @_;

    return if !defined $query || $query eq '';

    my @stack;
    my $string;
    my $escape = 0;

    for my $char (split //, $query) {
        if (defined $string) {
            if ($escape) {
                $escape = 0;
                next;
            }

            if ($char eq '\\') {
                $escape = 1;
                next;
            }

            if ($char eq $string) {
                undef $string;
            }
            next;
        }

        if ($char eq "'" || $char eq '"') {
            $string = $char;
            next;
        }

        if ($char eq '(' || $char eq '[' || $char eq '{') {
            push @stack, $char;
            next;
        }

        if ($char eq ')' || $char eq ']' || $char eq '}') {
            my $open = pop @stack;
            my %pairs = (
                ')' => '(',
                ']' => '[',
                '}' => '{',
            );
            if (!defined $open || $open ne $pairs{$char}) {
                _compile_error('Invalid query syntax: unmatched brackets');
            }
        }
    }

    if (defined $string || @stack) {
        _compile_error('Invalid query syntax: unmatched brackets');
    }

    my @pipeline_parts = JQ::Lite::Util::_split_top_level_pipes($query);
    if (grep { !defined $_ || $_ !~ /\S/ } @pipeline_parts) {
        _compile_error('Invalid query syntax: empty filter segment');
    }
}

sub _is_truthy {
    my ($value) = @_;

    return 0 unless defined $value;

    if (ref($value) eq 'JSON::PP::Boolean') {
        return $value ? 1 : 0;
    }

    return 1;
}

# ---------- Help text ----------
my $USAGE = <<'USAGE';
jq-lite - minimal jq-style JSON filter (pure Perl)

Usage:
  jq-lite [options] '.query' [file.json]
  jq-lite [options] -f query.jq [file.json]

Options:
  -R, --raw-input     Read input as raw text instead of JSON (one filter run per line)
  -r, --raw-output     Print raw strings instead of JSON-encoded values
  -c, --compact-output Print JSON results on a single line (no pretty-printing)
  -a, --ascii-output   Escape all non-ASCII characters in JSON output
  --color              Colorize JSON output (keys, strings, numbers, booleans)
  --use <Module>       Force JSON decoder module (e.g. JSON::PP, JSON::XS, Cpanel::JSON::XS)
  --debug              Show which JSON module is being used
  -e, --exit-status    Set exit code to 1 when the final result is false, null, or empty
  --arg NAME VALUE     Set jq-style variable $NAME to the string VALUE
  --rawfile NAME FILE  Set jq-style variable $NAME to the raw contents of FILE
  --slurpfile NAME FILE Set jq-style variable $NAME to an array of JSON values from FILE
  --argjson NAME JSON  Set jq-style variable $NAME to a JSON-decoded value
  --argfile NAME FILE  Set jq-style variable $NAME to a JSON-decoded value from FILE
  -f, --from-file FILE Read jq filter from FILE instead of the command line
                          (use '-' to read the filter from STDIN)
  -n, --null-input     Use null as input instead of reading JSON data
  -s, --slurp          Read entire input stream as an array of JSON values
  --yaml               Parse input as YAML (auto-detected for .yml/.yaml files)
  -h, --help           Show this help message
  --help-functions     Show list of all supported functions
  -v, --version        Show version information

Examples:
  cat users.json | jq-lite '.users[].name'
  jq-lite '.users[] | select(.age > 25)' users.json
  jq-lite -r '.users[] | .name' users.json
  jq-lite '.meta has "version"' config.json
  jq-lite --color '.items | sort | reverse | first' data.json

Homepage:
  https://metacpan.org/pod/JQ::Lite
USAGE

# ---------- Option parsing (Getopt::Long) ----------
my ($want_help, $want_version, $help_functions) = (0, 0, 0);

my @getopt_errors;
my $getopt_ok;
{
    my $orig_warn = $SIG{__WARN__};
    local $SIG{__WARN__} = sub {
        my ($msg) = @_;
        if ($msg =~ /^\[(?:COMPILE|RUNTIME|INPUT|USAGE)\]/) {
            if ($orig_warn) {
                $orig_warn->($msg);
            }
            else {
                CORE::warn($msg);
            }
            return;
        }

        push @getopt_errors, $msg;
    };

    $getopt_ok = GetOptions(
    'raw-input|R'    => \$raw_input,
    'raw-output|r'    => \$raw_output,
    'compact-output|c'=> \$compact_output,
    'ascii-output|a'  => \$ascii_output,
    'color'           => \$color_output,
    'use=s'           => \$decoder_choice,
    'debug'           => \$decoder_debug,
    'exit-status|e'   => \$exit_status,
    'null-input|n'    => \$null_input,
    'slurp|s'         => \$slurp_input,
    'yaml'            => \$force_yaml,
    'help|h'          => \$want_help,
    'help-functions'  => \$help_functions,
    'version|v'       => \$want_version,
    'from-file|f=s'   => \@query_files,
    'slurpfile=s'     => sub {
        my ($opt_name, $var_name) = @_;
        _usage_error('--slurpfile requires a variable name') if !defined $var_name || $var_name eq '';
        if ($var_name !~ /^[A-Za-z_]\w*$/) {
            _usage_error("Invalid variable name '$var_name' for --slurpfile");
        }
        _usage_error('--slurpfile requires a file path') if !@ARGV;
        my $file_path = shift @ARGV;
        $file_path = '' unless defined $file_path;

        $slurpfile_probably_missing_file = 1 if @ARGV == 0 && $file_path =~ /^\./;

        push @slurpfiles, $var_name, $file_path;
    },
    'arg=s'           => sub {
        my ($opt_name, $var_name) = @_;
        _usage_error('--arg requires a variable name') if !defined $var_name || $var_name eq '';
        if ($var_name !~ /^[A-Za-z_]\w*$/) {
            _usage_error("Invalid variable name '$var_name' for --arg");
        }
        _usage_error('--arg requires a value') if !@ARGV;
        my $value = shift @ARGV;
        $value = '' unless defined $value;
        $arg_vars{$var_name} = "$value";
    },
    'argjson=s'       => sub {
        my ($opt_name, $var_name) = @_;
        _usage_error('--argjson requires a variable name') if !defined $var_name || $var_name eq '';
        if ($var_name !~ /^[A-Za-z_]\w*$/) {
            _usage_error("Invalid variable name '$var_name' for --argjson");
        }
        _usage_error('--argjson requires a value') if !@ARGV;
        my $value_text = shift @ARGV;
        $value_text = '' unless defined $value_text;

        my $decoded = eval { JQ::Lite::Util::_decode_json($value_text) };
        if (my $err = $@) {
            $err =~ s/\s+\z//;
            _usage_error("invalid JSON for --argjson $var_name: $err");
        }

        $arg_vars{$var_name} = $decoded;
    },
    'argfile=s'       => sub {
        my ($opt_name, $var_name) = @_;
        _usage_error('--argfile requires a variable name') if !defined $var_name || $var_name eq '';
        if ($var_name !~ /^[A-Za-z_]\w*$/) {
            _usage_error("Invalid variable name '$var_name' for --argfile");
        }
        _usage_error('--argfile requires a file path') if !@ARGV;
        my $file_path = shift @ARGV;
        $file_path = '' unless defined $file_path;

        my $fh = IO::File->new($file_path, 'r')
          or _usage_error("Cannot open file '$file_path' for --argfile $var_name: $!");
        local $/;
        my $json_text = <$fh>;
        $json_text = '' unless defined $json_text;
        $fh->close;

        my $decoded = eval { JQ::Lite::Util::_decode_json($json_text) };
        if (my $err = $@) {
            $err =~ s/\s+\z//;
            _usage_error("invalid JSON in --argfile $var_name: $err");
        }

        $arg_vars{$var_name} = $decoded;
    },
    'rawfile=s'        => sub {
        my ($opt_name, $var_name) = @_;
        _usage_error('--rawfile requires a variable name') if !defined $var_name || $var_name eq '';
        if ($var_name !~ /^[A-Za-z_]\w*$/) {
            _usage_error("Invalid variable name '$var_name' for --rawfile");
        }
        _usage_error('--rawfile requires a file path') if !@ARGV;
        my $file_path = shift @ARGV;
        $file_path = '' unless defined $file_path;

        open my $fh, '<', $file_path
          or do {
            _usage_error("Cannot open file '$file_path' for --rawfile $var_name: $!");
          };
        local $/;
        my $content = <$fh>;
        close $fh;
        $content = '' unless defined $content;

        $arg_vars{$var_name} = $content;
    },
    );
}

if (!$getopt_ok) {
    my $message = @getopt_errors ? $getopt_errors[0] : 'invalid option(s)';
    $message =~ s/\s+\z//;
    _usage_error($message);
}

if ($help_functions) {
    print_supported_functions();
    exit 0;
}

if ($want_help) {
    print $USAGE;
    exit 0;
}

if ($want_version) {
    print "jq-lite $JQ::Lite::VERSION\n";
    exit 0;
}

$SIG{PIPE} = sub { exit 0 };

END {
    return unless defined fileno(STDOUT);

    if (!close STDOUT) {
        # On older perls a closed pipeline can report EINVAL instead of
        # EPIPE. Both conditions indicate the writer has no consumer, so do
        # not emit a warning in either case.
        return if $!{EPIPE} || $!{EINVAL};
        warn "Unable to flush stdout: $!";
    }
}

# ---------- Positional args: '.query' and [file.json] ----------
# Unknown options are already rejected above; only query and file remain.
my ($query, $filename);

my $non_help_option_used = $raw_input
  || $raw_output
  || $compact_output
  || $ascii_output
  || $color_output
  || defined $decoder_choice
  || $decoder_debug
  || $exit_status
  || $null_input
  || $slurp_input
  || $force_yaml
  || @query_files
  || keys(%arg_vars)
  || @slurpfiles;

if (@query_files) {
    _usage_error('--from-file may only be specified once') if @query_files > 1;

    my $file = $query_files[0];
    if (defined $file && $file eq '-') {
        if (!$null_input && !@ARGV) {
            _usage_error('Cannot use --from-file - when reading JSON from STDIN. Provide input file or use --null-input.');
        }
    }

    if (defined $file && $file eq '-') {
        local $/;
        $query = <STDIN>;
        $query = '' unless defined $query;
    }
    else {
        my $fh = IO::File->new($file, 'r')
          or _input_error("Cannot open query file '$file': $!");
        local $/;
        $query = <$fh> // '';
        $fh->close;
    }
}

if (@ARGV == 0) {
    if (!defined $query) {
        if ($null_input) {
            $query = '.';
        } else {
            if ($non_help_option_used) {
                if ($slurpfile_probably_missing_file) {
                    _usage_error('--slurpfile requires a file path');
                }
                _usage_error('filter expression is required');
            }

            # If no args and no options, show help
            print $USAGE;
            exit 0;
        }
    }
}
elsif (@ARGV == 1) {
    # Single arg: query or file
    if (!$null_input && -f $ARGV[0] && !defined $query) {
        $filename = $ARGV[0];
        # No query -> go to interactive mode (handled later)
    }
    elsif (!defined $query) {
        $query = $ARGV[0];
    }
    else {
        # Query already provided via -f; treat arg as filename if it exists
        my $f = $ARGV[0];
        if ($null_input) {
            _usage_error('--null-input cannot be combined with file input');
        }
        if (-f $f) {
            $filename = $f;
        } else {
            _input_error("Cannot open file '$f': $!");
        }
    }
}
elsif (@ARGV == 2) {
    # Two args: query + file (in this order)
    _usage_error('--null-input cannot be combined with file input') if $null_input;

    if (!defined $query) {
        $query = $ARGV[0];
    } else {
        _usage_error('Cannot provide both --from-file and a query argument');
    }

    my $f = $ARGV[1];
    if (-f $f) {
        $filename = $f;
    } else {
        _input_error("Cannot open file '$f': $!");
    }
}
else {
    _usage_error("Usage: jq-lite [options] '.query' [file.json]\n" .
                 "       jq-lite [options] -f query.jq [file.json]");
}

# ---------- JSON decoder selection ----------
if ($decoder_choice) {
    if ($decoder_choice eq 'JSON::MaybeXS') {
        require JSON::MaybeXS;
        $decoder = \&JSON::MaybeXS::decode_json;
        $decoder_module = 'JSON::MaybeXS';
    }
    elsif ($decoder_choice eq 'Cpanel::JSON::XS') {
        require Cpanel::JSON::XS;
        $decoder = \&Cpanel::JSON::XS::decode_json;
        $decoder_module = 'Cpanel::JSON::XS';
    }
    elsif ($decoder_choice eq 'JSON::XS') {
        require JSON::XS;
        $decoder = \&JSON::XS::decode_json;
        $decoder_module = 'JSON::XS';
    }
    elsif ($decoder_choice eq 'JSON::PP') {
        require JSON::PP;
        $decoder = \&JQ::Lite::Util::_decode_json;
        $decoder_module = 'JSON::PP';
    }
    else {
        _usage_error("Unknown JSON module: $decoder_choice");
    }
}
else {
    if (eval { require JSON::MaybeXS; 1 }) {
        $decoder = \&JSON::MaybeXS::decode_json;
        $decoder_module = 'JSON::MaybeXS';
    }
    elsif (eval { require Cpanel::JSON::XS; 1 }) {
        $decoder = \&Cpanel::JSON::XS::decode_json;
        $decoder_module = 'Cpanel::JSON::XS';
    }
    elsif (eval { require JSON::XS; 1 }) {
        $decoder = \&JSON::XS::decode_json;
        $decoder_module = 'JSON::XS';
    }
    else {
        require JSON::PP;
        $decoder = \&JQ::Lite::Util::_decode_json;
        $decoder_module = 'JSON::PP';
    }
}

warn "[DEBUG] Using $decoder_module\n" if $decoder_debug;

# ---------- --slurpfile handling ----------
if (@slurpfiles) {
    for (my $i = 0; $i < @slurpfiles; $i += 2) {
        my ($var_name, $file_path) = @slurpfiles[$i, $i + 1];

        _usage_error('--slurpfile requires a variable name') if !defined $var_name || $var_name eq '';
        if ($var_name !~ /^[A-Za-z_]\w*$/) {
            _usage_error("Invalid variable name '$var_name' for --slurpfile");
        }

        _usage_error('--slurpfile requires a file path') if !defined $file_path || $file_path eq '';

        my $fh = IO::File->new($file_path, 'r')
          or _usage_error("cannot read file: $file_path");
        local $/;
        my $json_text = <$fh>;
        $json_text = '' unless defined $json_text;
        $fh->close;

        my $decoded = eval { JQ::Lite::Util::_decode_json($json_text) };
        if ($@) {
            _usage_error("invalid JSON in slurpfile $var_name");
        }

        $arg_vars{$var_name} = [$decoded];
    }
}

# ---------- Validate query before reading input ----------
if (defined $query) {
    _validate_query_syntax($query);
}

# ---------- Read JSON input ----------
my $json_text;
my $use_yaml = 0;
if ($null_input) {
    $json_text = $slurp_input ? '[]' : 'null';
}
elsif (defined $filename) {
    open my $fh, '<', $filename or _input_error("Cannot open file '$filename': $!");
    local $/;
    $json_text = <$fh>;
    close $fh;
    $use_yaml = 1 if !$force_yaml && $filename =~ /\.ya?ml\z/i;
}
else {
    # When no file is given, if STDIN is a TTY, error out to avoid blocking
    if (-t STDIN) {
        _input_error('No input provided. Pass a file or pipe JSON via STDIN.');
    }
    local $/;
    $json_text = <STDIN>;
}

$use_yaml = 0 if $raw_input;
$use_yaml = 1 if $force_yaml && !$null_input;

if ($use_yaml) {
    $json_text = convert_yaml_to_json($json_text);
    warn "[DEBUG] Parsing YAML with $yaml_module\n" if $decoder_debug && $yaml_module;
}

# ---------- JQ::Lite core ----------
my $jq = JQ::Lite->new(raw => $raw_output, vars => \%arg_vars);

if ($raw_input) {
    $use_yaml = 0;    # Raw input is plain text; ignore YAML handling
}

my @raw_lines;
if ($raw_input) {
    my $text    = $json_text // '';
    my $encoder = JSON::PP->new->utf8->allow_nonref;

    if ($slurp_input) {
        $json_text = $encoder->encode($text);
    }
    else {
        @raw_lines = split /\r?\n/, $text, -1;
        if (@raw_lines && $raw_lines[-1] eq '' && $text =~ /\r?\n\z/) {
            pop @raw_lines;
        }
    }
}

if (!$raw_input && $slurp_input && !$null_input) {
    my $text   = $json_text // '';
    my $length = length $text;
    my $offset = 0;
    my @values;
    my $decoder = JSON::PP->new->utf8->allow_nonref;

    while (1) {
        while ($offset < $length && substr($text, $offset, 1) =~ /\s/) {
            $offset++;
        }
        last if $offset >= $length;

        my ($value, $consumed);
        my $ok = eval { ($value, $consumed) = $decoder->decode_prefix(substr($text, $offset)); 1 };
        if (!$ok) {
            _input_error("Failed to decode JSON stream for --slurp: $@");
        }

        push @values, $value;
        $offset += $consumed;
    }

    $json_text = JSON::PP->new->encode(\@values);
}

if (!$raw_input) {
    eval { JQ::Lite::Util::_decode_json($json_text); 1 }
      or _input_error("Failed to parse JSON input: $@");
}

sub convert_yaml_to_json {
    my ($text) = @_;
    $text = '' unless defined $text;

    ($yaml_loader, $yaml_module) = _build_yaml_loader() unless $yaml_loader;

    my @docs = eval { $yaml_loader->($text) };
    if (my $err = $@) {
        $err =~ s/\s+\z//;
        _input_error("Failed to parse YAML input: $err");
    }

    my $data;
    if (!@docs) {
        $data = undef;
    }
    elsif (@docs == 1) {
        $data = $docs[0];
    }
    else {
        $data = \@docs;
    }

    return JSON::PP->new->allow_nonref->encode($data);
}

sub _build_yaml_loader {
    if (eval { require YAML::XS; 1 }) {
        return (sub { YAML::XS::Load($_[0]) }, 'YAML::XS');
    }
    elsif (eval { require YAML::PP; 1 }) {
        require YAML::PP;
        my $yp = YAML::PP->new(boolean => 'JSON');
        return (sub { $yp->load_string($_[0]) }, 'YAML::PP');
    }
    else {
        require CPAN::Meta::YAML;
        return (
            sub {
                my $string = shift // '';
                my $docs   = CPAN::Meta::YAML->read_string($string);
                if (!defined $docs) {
                    my $err;
                    {
                        no warnings 'once';
                        $err = $CPAN::Meta::YAML::errstr;
                    }
                    $err ||= 'Unknown YAML parsing error';
                    die $err;
                }
                return @{$docs};
            },
            'CPAN::Meta::YAML'
        );
    }
}

# ---------- Colorization ----------
sub colorize_json {
    my $json = shift;

    $json =~ s/"([^"]+)"(?=\s*:)/color("cyan")."\"$1\"".color("reset")/ge;
    $json =~ s/(:\s*)"([^"]*)"/$1.color("green")."\"$2\"".color("reset")/ge;
    $json =~ s/(:\s*)(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/$1.color("yellow")."$2".color("reset")/ge;
    $json =~ s/(:\s*)(true|false|null)/$1.color("magenta")."$2".color("reset")/ge;

    return $json;
}

# ---------- Output ----------
sub print_results {
    my @results = @_;
    my $pp = JSON::PP->new->utf8->allow_nonref->canonical;
    $pp->pretty unless $compact_output;
    $pp->ascii($ascii_output);

    for my $r (@results) {
        if (!defined $r) {
            print "null\n";
        }
        elsif ($raw_output && !ref($r)) {
            print "$r\n";
        }
        else {
            my $json = $pp->encode($r);
            $json = colorize_json($json) if $color_output;
            print $json;
            print "\n" unless $json =~ /\n\z/;
        }
    }
}

# ---------- Interactive mode ----------
if ($raw_input && !$slurp_input && !defined $query) {
    _usage_error('--raw-input requires a query when not using --slurp.');
}

if (!defined $query) {
    system("stty -icanon -echo");

    $SIG{INT} = sub {
        system("stty sane");
        print "\n[EXIT]\n";
        exit 0;
    };

    my $input = '';
    my @last_results;

    my $ok = eval {
        @last_results = $jq->run_query($json_text, '.');
        1;
    };
    if (!$ok || !@last_results) {
        my $data = eval { JQ::Lite::Util::_decode_json($json_text) };
        if ($data) {
            @last_results = ($data);
        }
    }

    system("clear");
    if (@last_results) {
        print_results(@last_results);
    } else {
        print "[INFO] Failed to load initial JSON data.\n";
    }

    print "\nType query (ESC to quit):\n";
    print "> $input\n";

    while (1) {
        my $char;
        sysread(STDIN, $char, 1);

        my $ord = ord($char);
        last if $ord == 27; # ESC

        if ($ord == 127 || $char eq "\b") {
            chop $input if length($input);
        } else {
            $input .= $char;
        }

        system("clear");

        my @results;
        my $ok = eval {
            @results = $jq->run_query($json_text, $input);
            1;
        };

        if ($ok && @results) {
            @last_results = @results;
        }

        if (!$ok) {
            print "[INFO] Invalid or partial query. Showing last valid results.\n";
        } elsif (!@results) {
            print "[INFO] Query returned no results. Showing last valid results.\n";
        }

        if (@last_results) {
            eval {
                print_results(@last_results);
                1;
            } or do {
                my $e = $@ || 'Unknown error';
                print "[RUNTIME]Failed to print: $e\n";
            };
        } else {
            print "[INFO] No previous valid results.\n";
        }

        print "\n> $input\n";
    }

    system("stty sane");
    print "\nGoodbye.\n";
    exit 0;
}

# ---------- One-shot mode ----------
my @results;

if ($raw_input && !$slurp_input) {
    my $encoder = JSON::PP->new->utf8->allow_nonref;
    for my $line (@raw_lines) {
        my $encoded_line = $encoder->encode($line);
        my @line_results = eval { $jq->run_query($encoded_line, $query) };
        if ($@) {
            my $err = $@ || 'Unknown error';
            $err =~ s/\s+\z//;
            _runtime_error($err);
        }
        push @results, @line_results;
    }
}
else {
    @results = eval { $jq->run_query($json_text, $query) };
    if ($@) {
        my $err = $@ || 'Unknown error';
        $err =~ s/\s+\z//;
        _runtime_error($err);
    }
}

my $status = 0;
if ($exit_status) {
    my $last = @results ? $results[-1] : undef;
    $status = _is_truthy($last) ? 0 : 1;
}

sub print_supported_functions {
    print <<'EOF';

Supported Functions:
  length           - Count array elements, hash keys, or characters in scalars
  keys             - Extract sorted keys from a hash or indexes from an array
  keys_unsorted    - Extract object keys without sorting (jq-compatible)
  values           - Extract values from a hash (v0.34)
  leaf_paths()     - Emit only terminal paths to leaf values
  sort             - Sort array items
  sort_desc        - Sort array items in descending order
  sort_by(KEY)     - Sort array of objects by key
  pluck(KEY)       - Collect a key's value from each object in an array
  pick(KEYS...)    - Build new objects containing only the supplied keys (arrays handled element-wise)
  merge_objects()  - Merge arrays of objects into a single hash (last-write-wins)
  unique           - Remove duplicate values
  unique_by(KEY)   - Remove duplicates by projecting each item on KEY
  reverse          - Reverse an array
  first / last     - Get first / last element of an array
  limit(N)         - Limit array to first N elements
  drop(N)          - Skip the first N elements of an array
  rest             - Drop the first element of an array
  tail(N)          - Return the final N elements of an array
  chunks(N)        - Split array into subarrays each containing up to N items
  range(START; END[, STEP])
                   - Emit numbers from START (default 0) up to but not including END using STEP (default 1)
  enumerate()      - Pair each array element with its zero-based index
  transpose()      - Rotate arrays-of-arrays from rows into columns
  scalars          - Pass through only scalar inputs (string/number/bool/null)
  objects          - Pass through only object inputs
  count            - Count total number of matching items
  map(EXPR)        - Map/filter array items with a subquery
  map_values(FILTER)
                   - Apply FILTER to each value in an object (dropping keys when FILTER yields no result)
  if COND then A [elif COND then B ...] [else Z] end
                  - jq-style conditional branching across optional elif/else chains
  foreach(EXPR as $var (init; update [; extract]))
                   - jq-compatible streaming reducer with lexical bindings and optional emitters
  walk(FILTER)     - Recursively apply FILTER to every value in arrays and objects
  recurse([FILTER])
                   - Emit the current value and depth-first descendants using optional FILTER for children
  add / sum        - Sum all numeric values in an array
  sum_by(KEY)      - Sum numeric values projected from each array item
  avg_by(KEY)      - Average numeric values projected from each array item
  median_by(KEY)   - Return the median of numeric values projected from each array item
  min_by(PATH)     - Return the element with the smallest projected value
  max_by(PATH)     - Return the element with the largest projected value
  product          - Multiply all numeric values in an array
  min / max        - Return minimum / maximum numeric value in an array
  avg              - Return the average of numeric values in an array
  median           - Return the median of numeric values in an array
  mode             - Return the most frequent value in an array (ties pick earliest occurrence)
  percentile(P)    - Return the requested percentile (0-100 or 0-1) of numeric array values
  variance         - Return the variance of numeric values in an array
  stddev           - Return the standard deviation of numeric values in an array
  abs              - Convert numbers (and array elements) to their absolute value
  ceil()           - Round numbers up to the nearest integer
  floor()          - Round numbers down to the nearest integer
  round()          - Round numbers to the nearest integer (half-up semantics)
  clamp(MIN, MAX)  - Clamp numeric values within an inclusive range
  tostring()       - Convert values into their JSON string representation
  @json            - Format the input as JSON text (jq-style formatter)
  @csv             - Format arrays/scalars as a single CSV row
  @tsv             - Format arrays/scalars as a single TSV row
  @base64          - Encode input as a Base64 string
  @base64d         - Decode Base64-encoded text into strings/arrays
  @uri             - Percent-encode text (URL-safe)
  tojson()         - Encode values as JSON text regardless of type
  fromjson()       - Decode JSON text into native values (arrays handled element-wise)
  tonumber() / to_number()
                   - Coerce numeric-looking strings/booleans into numbers
  nth(N)           - Get the Nth element of an array (zero-based index)
  index(VALUE)     - Return the zero-based index of VALUE within arrays or strings
  rindex(VALUE)    - Return the zero-based index of the last VALUE within arrays or strings
  indices(VALUE)   - Return every index where VALUE occurs within arrays or strings
  group_by(KEY)    - Group array items by field
  group_count(KEY) - Count grouped items by field
  join(SEPARATOR)  - Join array elements with a string
  split(SEPARATOR) - Split string values (and arrays of strings) by a literal separator
  substr(START[, LENGTH])
                   - Extract substring using zero-based indices (arrays handled element-wise)
  slice(START[, LENGTH])
                  - Return a subarray using zero-based indices (negative starts count from the end)
  replace(OLD; NEW)
                   - Replace literal substrings (arrays handled element-wise)
  to_entries       - Convert objects/arrays into [{"key","value"}, ...] pairs
  from_entries     - Convert entry arrays back into an object
  with_entries(FILTER)
                   - Transform entries using FILTER and rebuild an object
  delpaths(PATHS)  - Delete keys at the supplied PATHS array (jq-compatible)
  has              - Check if objects contain a key or arrays expose an index
  contains         - Check if strings include a fragment, arrays contain an element, or hashes have a key
  contains_subset(VALUE)
                   - jq-style subset containment for arrays (order-insensitive)
  inside(CONTAINER) - Check if the input value is contained within CONTAINER
  any([FILTER])    - Return true if any input (optionally filtered) is truthy
  all([FILTER])    - Return true if every input (optionally filtered) is truthy
  not              - Logical negation following jq truthiness rules
  test("pattern"[, "flags"]) - Check strings against regexes (flags: i, m, s, x)
  explode()        - Convert strings to arrays of Unicode code points
  implode()        - Turn arrays of code points back into strings
  flatten          - Explicitly flatten arrays (same as .[])
  flatten_all()    - Recursively flatten nested arrays into a single array
  flatten_depth(N) - Flatten nested arrays up to N levels deep
  arrays           - Pass through inputs that are arrays, discarding others
  del(KEY)         - Remove a key from objects in the result
  compact          - Remove undefined values from arrays
  path             - Return available keys for objects or indexes for arrays
  paths            - Emit every path to nested values as an array of keys/indices
  getpath(PATH)    - Retrieve the value(s) at the supplied path array or expression
  setpath(PATH; VALUE)
                   - Set or create value(s) at a path using literal or filter input
  is_empty         - Check if an array or object is empty
  upper()          - Convert scalars and array elements to uppercase
  lower()          - Convert scalars and array elements to lowercase
  ascii_upcase()   - ASCII-only uppercase conversion (leaves non-ASCII untouched)
  ascii_downcase() - ASCII-only lowercase conversion (leaves non-ASCII untouched)
  titlecase()      - Convert scalars and array elements to title case
  trim()           - Strip leading/trailing whitespace from strings (recurses into arrays)
  ltrimstr(PFX)    - Remove PFX when it appears at the start of strings (arrays handled element-wise)
  rtrimstr(SFX)    - Remove SFX when it appears at the end of strings (arrays handled element-wise)
  startswith(PFX)  - Check if a string (or array of strings) begins with PFX
  endswith(SFX)    - Check if a string (or array of strings) ends with SFX
  empty            - Discard all output (for side-effect use)
  type()           - Return the type of value ("string", "number", "boolean", "array", "object", "null")
  lhs // rhs       - jq-style alternative operator yielding rhs when lhs is null/missing
  default(VALUE)   - Substitute VALUE when result is undefined

EOF
}

print_results(@results);

exit $status;

__END__

=encoding UTF-8

=head1 NAME

jq-lite - minimal jq-style JSON filter (pure Perl)

=head1 SYNOPSIS

  jq-lite [options] filter [file ...]

  cat users.json | jq-lite '.users[].name'
  jq-lite '.items | sort | first' data.json
  jq-lite -R -s -c 'split("\n")' logfile.txt

=head1 DESCRIPTION

B<jq-lite> is a jq-compatible JSON processor written entirely in
pure Perl.

It is intended for use on systems where B<jq> is unavailable but Perl
is present, providing awk/sed-like convenience for processing JSON
and YAML data.

Features include:

=over 4

=item *

jq-style filters: dot traversal, pipes, basic selectors,
and commonly used built-in functions

=item *

Command-line options such as C<-R>, C<-r>, C<-c>, C<-a>, C<--color>,
C<--use>, C<--debug>, C<-e>, C<--slurp>, C<--null-input>, C<-s>,
C<--from-file>, C<--arg>, C<--argfile>, C<--argjson>, C<--rawfile>,
C<--slurpfile>, C<--help-functions>, C<-h>, and C<-v>

=item *

Pure Perl implementation with no XS, no external binaries,
and no runtime dependencies beyond core modules

=back

B<jq-lite> aims to be compatible with B<jq> where practical,
but does not implement all jq features.

It is not a drop-in replacement for jq, but a pragmatic alternative
for constrained or minimal environments.

In particular, some advanced expressions and comparisons may behave
differently from jq.

=head1 OPTIONS

=over 4

=item B<-R>, B<--raw-input>

Read input as raw text instead of JSON, running the filter once per line. Use
B<-s> with B<-R> to process the entire input as a single string.

=item B<-r>, B<--raw-output>

Print raw strings instead of JSON-encoded values.

=item B<-c>, B<--compact-output>

Emit JSON on a single line without pretty-printing whitespace.

=item B<-a>, B<--ascii-output>

Escape all non-ASCII characters in JSON output.

=item B<--color>

Colorize JSON output (keys, strings, numbers, booleans).

=item B<--use> I<Module>

Force the JSON decoder module (for example C<JSON::PP>, C<JSON::XS>,
or C<Cpanel::JSON::XS>).

=item B<--debug>

Show which JSON module is being used.

=item B<-e>, B<--exit-status>

Set exit code to 1 when the final result is false, null, or empty.

=item B<--arg> I<name> I<value>

Define a string variable.

=item B<--rawfile> I<name> I<file>

Read I<file> as raw text and bind it to the variable.

=item B<--slurpfile> I<name> I<file>

Read I<file> as JSON and bind it to the variable as an array of values.

=item B<--argjson> I<name> I<value>

Define a JSON variable.

=item B<--argfile> I<name> I<file>

Read I<file> as JSON and bind it to the variable.

=item B<-f>, B<--from-file> I<file>

Read the jq filter from I<file> instead of the command line (use C<-> for
STDIN).

=item B<-n>, B<--null-input>

Use C<null> as input instead of reading JSON data.

=item B<-s>, B<--slurp>

Read the entire input stream as an array of JSON values.

=item B<--yaml>

Parse input as YAML (auto-detected for C<.yml> / C<.yaml> files).

=item B<-h>, B<--help>

Display help and exit.

=item B<--help-functions>

Show the list of supported built-in functions.

=item B<-v>, B<--version>

Display version information.

=back

=head1 SEE ALSO

L<JQ::Lite>,
B<jq>(1),
B<awk>(1),
B<sed>(1),
B<perl>(1)

=head1 HOMEPAGE

https://kawamurashingo.github.io/JQ-Lite/

=head1 AUTHOR

Shingo Kawamura

=head1 COPYRIGHT AND LICENSE

This software is released under the same terms as Perl itself.

=head1 DISCLAIMER

jq-lite is provided "as is", without warranty of any kind.

