#!/usr/bin/env perl

use 5.14.2;
use warnings;

use Zonemaster::Backend::TestAgent;
use Zonemaster::Backend::Config;
use Zonemaster::Backend::Metrics;

use Parallel::ForkManager;
use Daemon::Control;
use Log::Any qw( $log );
use Log::Any::Adapter;

use English;
use Pod::Usage;
use Getopt::Long;
use POSIX;
use Time::HiRes qw[time sleep gettimeofday tv_interval];
use sigtrap qw(die normal-signals);

###
### Compile-time stuff.
###

BEGIN {
    $ENV{PERL_JSON_BACKEND} = 'JSON::PP';
    undef $ENV{LANGUAGE};
}

# Enable immediate flush to stdout and stderr
$|++;

###
### More global variables, and initialization.
###

my $pidfile;
my $user;
my $group;
my $logfile;
my $loglevel;
my $logjson;
my $opt_outfile;
my $opt_help;
GetOptions(
    'help!'      => \$opt_help,
    'pidfile=s'  => \$pidfile,
    'user=s'     => \$user,
    'group=s'    => \$group,
    'logfile=s'  => \$logfile,
    'loglevel=s' => \$loglevel,
    'logjson!'  => \$logjson,
    'outfile=s'  => \$opt_outfile,
) or pod2usage( "Try '$0 --help' for more information." );

pod2usage( -verbose => 1 ) if $opt_help;

$pidfile     //= '/tmp/zonemaster_backend_testagent.pid';
$logfile     //= '/var/log/zonemaster/zonemaster_backend_testagent.log';
$opt_outfile //= '/var/log/zonemaster/zonemaster_backend_testagent.out';
$loglevel    //= 'info';
$loglevel = lc $loglevel;

Log::Any::Adapter->set(
    '+Zonemaster::Backend::Log',
    log_level => $loglevel,
    json => $logjson,
    file => $logfile,
);

$SIG{__WARN__} = sub {
    $log->warning(map s/^\s+|\s+$//gr, map s/\n/ /gr, @_);
};

###
### Actual functionality
###

sub main {
    my $self = shift;

    my $caught_sigterm = 0;
    my $catch_sigterm;
    $catch_sigterm = sub {
        $SIG{TERM} = $catch_sigterm;
        $caught_sigterm = 1;
        $log->notice( "Daemon caught SIGTERM" );
        return;
    };
    local $SIG{TERM} = $catch_sigterm;

    my $agent = Zonemaster::Backend::TestAgent->new( { config => $self->config } );

    while ( !$caught_sigterm ) {
        my $cleanup_timer = [ gettimeofday ];

        $self->pm->reap_finished_children();    # Reaps terminated child processes
        $self->pm->on_wait();                   # Sends SIGKILL to overdue child processes

        Zonemaster::Backend::Metrics::gauge("zonemaster.testagent.maximum_processes", $self->pm->max_procs);
        Zonemaster::Backend::Metrics::gauge("zonemaster.testagent.running_processes", scalar($self->pm->running_procs));

        Zonemaster::Backend::Metrics::timing("zonemaster.testagent.cleanup_duration_seconds", tv_interval($cleanup_timer) * 1000);

        my $fetch_test_timer = [ gettimeofday ];

        my ( $test_id, $batch_id );
        eval {
            $self->db->process_unfinished_tests(
                $self->config->ZONEMASTER_lock_on_queue,
                $self->config->ZONEMASTER_max_zonemaster_execution_time,
            );

            ( $test_id, $batch_id ) = $self->db->get_test_request( $self->config->ZONEMASTER_lock_on_queue );

            Zonemaster::Backend::Metrics::timing("zonemaster.testagent.fetchtests_duration_seconds", tv_interval($fetch_test_timer) * 1000);
        };
        if ( $@ ) {
            $log->error( $@ );
        }

        my $show_progress = defined $batch_id ? 0 : 1;

        if ( $test_id ) {
            $log->infof( "Test found: %s", $test_id );
            if ( $self->pm->start( $test_id ) == 0 ) {    # Forks off child process
                $log->infof( "Test starting: %s", $test_id );
                Zonemaster::Backend::Metrics::increment("zonemaster.testagent.tests_started");
                my $start_time = [ gettimeofday ];
                eval { $agent->run( $test_id, $show_progress ) };
                if ( $@ ) {
                    chomp $@;
                    Zonemaster::Backend::Metrics::increment("zonemaster.testagent.tests_died");
                    $log->errorf( "Test died: %s: %s", $test_id, $@ );
                    $self->db->process_dead_test( $test_id )
                }
                else {
                    Zonemaster::Backend::Metrics::increment("zonemaster.testagent.tests_completed");
                    $log->infof( "Test completed: %s", $test_id );
                }
                Zonemaster::Backend::Metrics::timing("zonemaster.testagent.tests_duration_seconds", tv_interval($start_time) * 1000);
                $agent->reset();
                $self->pm->finish;                   # Terminates child process
            }
        }
        else {
            sleep $self->config->DB_polling_interval;
        }
    }

    $log->notice( "Daemon entered graceful shutdown" );

    $self->pm->wait_all_children();    # Includes SIGKILLing overdue child processes

    return;
}

sub preflight_checks {
    # Make sure we can load the configuration file
    $log->debug("Starting pre-flight check");
    my $initial_config = Zonemaster::Backend::Config->load_config();

    Zonemaster::Backend::Metrics->setup($initial_config->METRICS_statsd_host, $initial_config->METRICS_statsd_port);

    # Validate the Zonemaster-Engine profile
    Zonemaster::Backend::TestAgent->new( { config => $initial_config } );

    # Connect to the database
    $initial_config->new_DB();
    $log->debug("Completed pre-flight check");

    return $initial_config;
}



my $initial_config;

# Make sure the environment is alright before forking (only on startup)
if ( grep /^foreground$|^restart$|^start$/, @ARGV ) {
    eval {
        $initial_config = preflight_checks();
    };
    if ( $@ ) {
        $log->critical( "Aborting startup: $@" );
        print STDERR "Aborting startup: $@";
        exit 1;
    }
}

###
### Daemon Control stuff.
###

my $daemon = Daemon::Control->with_plugins( qw( +Zonemaster::Backend::Config::DCPlugin ) )->new(
    {
        name    => 'zonemaster-testagent',
        program => sub {
            my $self = shift;
            $log->notice( "Daemon spawned" );

            $self->init_backend_config( $initial_config );
            undef $initial_config;

            eval { main( $self ) };
            if ( $@ ) {
                chomp $@;
                $log->critical( $@ );
            }
            $log->notice( "Daemon terminating" );
        },
        pid_file    => $pidfile,
        stderr_file => $opt_outfile,
        stdout_file => $opt_outfile,
    }
);

$daemon->init_config( $ENV{PERLBREW_ROOT} . '/etc/bashrc' ) if ( $ENV{PERLBREW_ROOT} );
$daemon->user($user) if $user;
$daemon->group($group) if $group;

exit $daemon->run;

=head1 NAME

zonemaster_backend_testagent - Init script for Zonemaster Test Agent.

=head1 SYNOPSIS

    zonemaster_backend_testagent [OPTIONS] [COMMAND]

=head1 OPTIONS

=over 4

=item B<--help>

Print a brief help message and exits.

=item B<--user=USER>

When specified the daemon will drop to the user with this username when forked.

=item B<--group=GROUP>

When specified the daemon will drop to the group with this groupname when forked.

=item B<--pidfile=FILE>

The location of the PID file to use.

=item B<--logfile=FILE>

The location of the log file to use.

When FILE is -, the log is written to standard output.

=item B<--loglevel=LEVEL>

The location of the log level to use.

The allowed values are specified at L<Log::Any/LOG-LEVELS>.

=item B<--logjson>

Enable JSON logging when specified.

=item B<COMMAND>

One of the following:

=over 4

=item start

=item foreground

=item stop

=item restart

=item reload

=item status

=item get_init_file

=back

=back

=cut
