#!/usr/bin/env perl
# Evaluate the alert rules and write what they say to the database.
#
# THIS IS A SEPARATE PROCESS ON PURPOSE, and it is the shape of the thing it
# stands in for. A real deployment runs this from cron, from Punk::Queue, or
# from a leader-elected worker - never on the request that draws the screen.
# Evaluating on a page load means the states an operator sees depend on who
# last looked at them, two people refreshing produce two different histories,
# and nothing is recorded when nobody is watching. Which is precisely when an
# incident happens.
#
#   perl example/bin/evaluate --once      one pass and exit
#   perl example/bin/evaluate             every 15 seconds until interrupted
use strict;
use warnings;
use FindBin ();
use lib "$FindBin::Bin/../observe/lib";
use Getopt::Long qw(GetOptions);

use Punk::Observe ();
use Punk::Observe::Store ();
use Punk::Observe::Alert ();
use Demo::DB ();

my ($once, $every, $quiet) = (0, 15, 0);
GetOptions('once' => \$once, 'every=i' => \$every, 'quiet' => \$quiet)
    or die "usage: $0 [--once] [--every SECONDS] [--quiet]\n";

$ENV{DEMO_STORE} ||= 'var/store';
Demo::DB::seed();

# A bucketed answer is series -> points; an evaluation is a point in time with
# every series' value at it. So the result is transposed, which is the whole
# of the adapter between the query engine and the rule engine.
sub ticks {
    my ($res) = @_;
    my (%rows, %val);
    for my $s (@{ $res->{series} || [] }) {
        for my $p (@{ $s->{points} || [] }) {
            push @{ $rows{ $p->[0] } }, $s->{key}, $p->[1];
            $val{ $s->{key} } = $p->[1];       # the latest, for the table
        }
    }
    # Sorted as STRINGS by width then value: a nanosecond instant past 2^53
    # does not survive a numeric comparison.
    my @at = sort { length($a) <=> length($b) || $a cmp $b } keys %rows;
    return ([ map { { at => $_, rows => $rows{$_} } } @at ], \%val);
}

sub value_of {
    my ($rule, $v) = @_;
    return undef unless defined $v;
    # A p95 of a duration is nanoseconds; the screen shows milliseconds.
    return $v / 1_000_000 if ($rule->{unit} || '') eq 'p95';
    return $v;
}

sub pass {
    my $store = eval {
        Punk::Observe::Store->new(dir => $ENV{DEMO_STORE}, tenant => 'default')
    } or return 0;

    my $now = Punk::Observe::now_ns();
    # AN HOUR, so the story outlives the run that produced it. At fifteen
    # minutes the incident aged out a few minutes after the traffic stopped
    # and the screen went quiet - correct, and useless to look at.
    my $from = Punk::Observe::Store::nsub($now, 3_600 * 1_000_000_000);

    my $moved = 0;
    for my $r (@{ Demo::DB::rules() }) {
        # The rule as the evaluator wants it. The database column is `for_ns`
        # because that is what it holds; the evaluator's key is `for`.
        my %rule = (
            op => $r->{op}, threshold => $r->{threshold},
            for => $r->{for_ns}, every => $r->{every_ns},
        );

        my $res = eval { $store->query($r->{query}, from => $from, to => $now) };
        if (!$res || !$res->{ok} || ($res->{shape} || '') ne 'buckets') {
            # A rule that could not be EVALUATED is not a rule that is fine.
            # Recording the error state is what puts it at the top of the
            # screen instead of leaving it looking quietly healthy.
            for my $s (@{ Demo::DB::state_for($r->{id}) }) {
                $moved += Demo::DB::record($r->{id}, $s->{series}, 'error',
                                           $s->{since}, $s->{last_value}, $now);
            }
            next;
        }

        my ($ticks, $latest) = ticks($res);
        next unless @$ticks;

        my $run = eval { Punk::Observe::Alert::run(\%rule, $ticks) } or next;
        my $last = $run->[-1] or next;

        for my $s (@{ $last->{states} || [] }) {
            # An ungrouped query answers for one series whose key is the empty
            # string. It still has to be called something, or the screen shows
            # a rule watching a blank.
            my $key = (defined $s->{series} && length $s->{series})
                        ? $s->{series} : ($r->{series_label} || 'all');
            $moved += Demo::DB::record(
                $r->{id}, $key, $s->{state}, $s->{since},
                value_of($r, $latest->{ $s->{series} }), $now);
        }
    }
    return $moved;
}

while (1) {
    my $moved = eval { pass() };
    warn "evaluate: $@" if $@;
    printf "evaluated%s\n", $moved ? " ($moved transitions)" : ''
        unless $quiet;
    last if $once;
    sleep $every;
}
