use warnings;
use strict;
use feature qw{ say };

use JSON::PP;
use Time::Piece;

use constant {
    NAME     => 0,
    CATEGORY => 1,
    DATE     => 2,
    DB       => 'simple-db',
};

my $operation = shift // "";

my %dispatch = (
    n => \&add_new,
    l => \&print_latest,
    L => \&print_latest_for_categories,
    a => \&print_all,
);

if ($dispatch{$operation}) {
    $dispatch{$operation}->(@ARGV);
} else {
    die "Invalid option. Use one of n, l, L, a.\n"
}

sub add_new {
    my ($name, $category, $date) = @_;
    my $db = eval { load() } || {};
    if (defined $date) {
        eval { 'Time::Piece'->strptime($date, '%Y-%m-%d'); 1 }
            or die "Invalid date format: YYYY-MM-DD.\n";

    } else {
        $date //= localtime->ymd;
    }

    my @ids = keys %{ $db->{by_id} };
    my $max_id = max(num => @ids) || 0;
    $db->{by_id}{ ++$max_id } = [ $name, $category, $date ];
    save($db);
}

sub print_latest {
    build_indexes( my $db = load(), 0, 1 );
    _print_latest($db);
}

sub _print_latest {
    my ($db, $category) = @_;
    my @dates = keys %{ $db->{by_date} };
    @dates = grep {
        grep $db->{by_id}{$_}[CATEGORY] eq $category,
            @{ $db->{by_date}{$_} };
    } @dates if defined $category;

    my $last_date = max(str => @dates);
    say for map $db->{by_id}{$_}[NAME],
            grep ! defined $category
                 || $db->{by_id}{$_}[CATEGORY] eq $category,
            @{ $db->{by_date}{$last_date} };
}

sub max {
    my $type = shift;
    my $max = $_[0];
    { num => sub { $_ >  $max },
      str => sub { $_ gt $max},
    }->{$type}->() and $max = $_
        for @_[ 1 .. $
    return $max
}

sub print_latest_for_categories {
    build_indexes( my $db = load(), 1, 1 );

    for my $category (sort keys %{ $db->{by_category} }){
        say "* $category";
        _print_latest($db, $category);
    }
}

sub print_all {
    build_indexes( my $db = load(), 0, 1 );

    for my $date (sort keys %{ $db->{by_date} }) {
        for my $id (@{ $db->{by_date}{$date} }) {
            say $db->{by_id}{$id}[NAME];
        }
    }
}

sub load {
    open my $in, '<', DB or die "Can't open database: $!\n";
    local $/;
    return { by_id => decode_json(<$in>) };
}

sub save {
    my ($db) = @_;
    open my $out, '>', DB or die "Can't save database: $!\n";
    print {$out} encode_json($db->{by_id});
    close $out;
}

sub build_indexes {
    my ($db, $by_category, $by_date) = @_;
    for my $id (keys %{ $db->{by_id} }) {
        push @{ $db->{by_category}{ $db->{by_id}{$id}[CATEGORY] } }, $id
            if $by_category;
        push @{ $db->{by_date}{ $db->{by_id}{$id}[DATE] } }, $id
            if $by_date;
    }
}