#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use Getopt::Long qw(GetOptionsFromArray);
use Cwd qw(abs_path);
use File::Basename qw(dirname basename);
use File::Spec;
use Encode qw(encode FB_CROAK LEAVE_SRC);
use Pod::Usage qw(pod2usage);
use LogTool::Course qw(import_records read_jsonl jsonl);
use LogTool::Course::Filter qw(make_filter);
use LogTool::Course::Aggregator;
use LogTool::Report qw(save_report);

sub destination {
    my ($path) = @_;
    return abs_path($path) if -e $path;
    my $parent = abs_path(dirname($path));
    die "destination parent does not exist\n" unless defined($parent);
    return File::Spec->catfile($parent, basename($path));
}

my $success = eval {
    binmode STDOUT, ':raw' or die "cannot set stdout: $!\n";
    local $SIG{PIPE} = 'IGNORE';
    if (@ARGV == 1 && $ARGV[0] eq '--version') {
        print STDOUT "LogTool::Course $LogTool::Course::VERSION Perl $^V\n" or die "cannot write stdout: $!\n";
    } elsif (@ARGV == 1 && $ARGV[0] eq '--help') {
        pod2usage(-verbose => 2, -noperldoc => 1, -exitval => 'NOEXIT', -output => \*STDOUT);
    } else {
        my $command = shift @ARGV // '';
        die "expected import, query or report\n" unless $command =~ /\A(?:import|query|report)\z/;
        Getopt::Long::Configure(qw(no_auto_abbrev no_ignore_case require_order));
        my ($input, $output, $rejects, $level, $minimum, $contains);
        my @options = ('input=s' => \$input);
        push @options, ('output=s' => \$output) if $command ne 'query';
        push @options, ('rejects=s' => \$rejects) if $command eq 'import';
        push @options, ('level=s' => \$level, 'min-duration=s' => \$minimum, 'contains=s' => \$contains) if $command eq 'query';
        GetOptionsFromArray(\@ARGV, @options) or die "invalid options\n";
        die "unexpected positional arguments\n" if @ARGV;
        die "--input is required\n" unless defined($input) && length($input);
        my @destinations = grep { defined($_) } ($output, $rejects);
        my %seen;
        $seen{destination($input)} = 1;
        my %identities;
        my @initial = stat($input);
        $identities{"$initial[0]:$initial[1]"} = 1 if @initial;
        for my $path (@destinations) {
            die "empty destination\n" unless length($path);
            die "input/output paths must be distinct\n" if $seen{destination($path)}++;
            if (-e $path) {
                my @out = stat($path);
                die "destination aliases another file\n" if $identities{"$out[0]:$out[1]"}++;
            }
        }
        if ($command eq 'import') {
            die "--output and --rejects are required\n" unless defined($output) && defined($rejects);
            my $batch = import_records($input);
            save_report(jsonl($batch->{rejected}), $rejects);
            save_report(jsonl($batch->{accepted}), $output);
            print STDOUT "INPUT\t$batch->{input}\nACCEPTED\t", scalar(@{$batch->{accepted}}), "\nREJECTED\t", scalar(@{$batch->{rejected}}), "\n" or die "cannot write stdout: $!\n";
        } else {
            my $records = read_jsonl($input);
            if ($command eq 'query') {
                die "invalid level\n" if defined($level) && $level !~ /\A(?:INFO|WARN|ERROR)\z/;
                die "invalid minimum duration\n" if defined($minimum) && $minimum !~ /\A(?:0|[1-9][0-9]{0,8})\z/;
                my %conditions;
                $conditions{level} = $level if defined($level);
                $conditions{min_duration_ms} = 0 + $minimum if defined($minimum);
                $conditions{contains} = $contains if defined($contains);
                my $filter = make_filter(%conditions);
                print STDOUT jsonl([grep { $filter->($_) } @$records]) or die "cannot write stdout: $!\n";
            } else {
                my $aggregator = LogTool::Course::Aggregator->new;
                $aggregator->add($_) for @$records;
                my $text = "TOTAL\t" . scalar(@$records) . "\n";
                $text .= "$_->{level}\t$_->{count}\n" for @{$aggregator->rows};
                my $bytes = encode('UTF-8', $text, FB_CROAK | LEAVE_SRC);
                if (defined($output)) {
                    save_report($bytes, $output);
                    print STDOUT "SAVED\n" or die "cannot write stdout: $!\n";
                } else {
                    print STDOUT $bytes or die "cannot write stdout: $!\n";
                }
            }
        }
    }
    close STDOUT or die "cannot close stdout: $!\n";
    1;
};
if (!$success) {
    my $error = $@;
    print STDERR $error;
    exit 2;
}

__END__

=head1 NAME

logtool - import five-field TSV into typed UTF-8 JSON Lines

=head1 SYNOPSIS

  perl bin/logtool --version
  perl bin/logtool import --input fixtures/course.tsv --output /tmp/accepted.jsonl --rejects /tmp/rejected.jsonl
  perl bin/logtool query --input /tmp/accepted.jsonl --min-duration 0
  perl bin/logtool query --input /tmp/accepted.jsonl --level INFO --contains queued
  perl bin/logtool report --input /tmp/accepted.jsonl
  perl bin/logtool report --input /tmp/accepted.jsonl --output /tmp/report.tsv
  perl bin/logtool --help

=head1 CONTRACT

Fields are timestamp, level, service, duration and escaped message. Input
accepts LF, CRLF and a final unterminated line. A line exceeding 65536 bytes
is rejected; total input is limited to 16777216 bytes. Bad UTF-8 and I/O
fail the batch. Syntax errors enter the reject list. Empty duration is
JSON null; a valid duration is a number. NFC applies only to message;
raw_message preserves decoded text and service identifiers retain case.

Each output file is replaced after its own successful close. The reject
file is published first; two files are not a joint atomic transaction.
If accepted output publication fails, the reject file may have changed.
Locks are cooperative and rename does not establish power-loss durability.
Errors exit 2; successful empty queries exit 0. STDOUT is explicitly closed.

=cut
