#!/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;

my $decoder;
my $decoder_module;
my $decoder_debug   = 0;
my $decoder_choice;
my $raw_output      = 0;
my $color_output    = 0;

# ---------- Help text ----------
my $USAGE = <<'USAGE';
jq-lite - A lightweight jq-like JSON query tool written in pure Perl

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

Options:
  -r, --raw-output     Print raw strings instead of JSON-encoded values
  --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
  -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);

GetOptions(
    'raw-output|r'    => \$raw_output,
    'color'           => \$color_output,
    'use=s'           => \$decoder_choice,
    'debug'           => \$decoder_debug,
    'help|h'          => \$want_help,
    'help-functions'  => \$help_functions,
    'version|v'       => \$want_version,
) or die "[ERROR] Unknown option(s)\n";

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

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

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

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

if (@ARGV == 0) {
    # If no args, show help
    print $USAGE;
    exit 0;
}
elsif (@ARGV == 1) {
    # Single arg: query or file
    if (-f $ARGV[0]) {
        $filename = $ARGV[0];
        # No query -> go to interactive mode (handled later)
    } else {
        $query = $ARGV[0];
    }
}
elsif (@ARGV == 2) {
    # Two args: query + file (in this order)
    $query = $ARGV[0];
    my $f = $ARGV[1];
    if (-f $f) {
        $filename = $f;
    } else {
        die "Cannot open file '$f': $!\n";
    }
}
else {
    die "Usage: jq-lite [options] '.query' [file.json]\n";
}

# ---------- 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 = \&JSON::PP::decode_json;
        $decoder_module = 'JSON::PP';
    }
    else {
        die "[ERROR] Unknown JSON module: $decoder_choice\n";
    }
}
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 = \&JSON::PP::decode_json;
        $decoder_module = 'JSON::PP';
    }
}

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

# ---------- Read JSON input ----------
my $json_text;
if (defined $filename) {
    open my $fh, '<', $filename or die "Cannot open file '$filename': $!\n";
    local $/;
    $json_text = <$fh>;
    close $fh;
}
else {
    # When no file is given, if STDIN is a TTY, error out to avoid blocking
    if (-t STDIN) {
        die "[ERROR] No input provided. Pass a file or pipe JSON via STDIN.\n";
    }
    local $/;
    $json_text = <STDIN>;
}

# ---------- JQ::Lite core ----------
my $jq = JQ::Lite->new(raw => $raw_output);

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

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

    return $json;
}

# ---------- Output ----------
sub print_results {
    my @results = @_;
    my $pp = JSON::PP->new->utf8->canonical->pretty;

    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;
        }
    }
}

# ---------- Interactive mode ----------
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 { JSON::PP->new->decode($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 "[ERROR] 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 = eval { $jq->run_query($json_text, $query) };
if ($@) {
    die "[ERROR] Invalid query: $@\n";
}

if (!@results) {
    warn "[INFO] No results returned for query.\n";
    exit 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
  keys_unsorted    - Extract object keys without sorting (jq-compatible)
  values           - Extract values from a hash (v0.34)
  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
  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
  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)
  walk(FILTER)     - Recursively apply FILTER to every value in arrays and objects
  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
  tojson()         - Encode values as JSON text regardless of type
  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
  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
  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
  any([FILTER])    - Return true if any input (optionally filtered) is truthy
  all([FILTER])    - Return true if every input (optionally filtered) is truthy
  match("pattern") - Match string using regex
  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
  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
  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
  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")
  default(VALUE)   - Substitute VALUE when result is undefined

EOF
}

print_results(@results);
