#!/usr/bin/perl -w -T
#
# Zymonic Business Process and Information Management System
# Copyright Zednax Limited 2008 -
# For Authors and Changelog see the subversion history
use strict;

package main;

# Library path
BEGIN
{
    use vars qw($location $script_name $script_location $utils);
    use Zymonic::Utils qw(clean debug death_handler get_array);
    $main::SIG{__DIE__} = \&death_handler;
}

# Modules
use Zymonic::Config;
use Zymonic::DB;
use Zymonic::Table;
use Zymonic::Process;
use Zymonic::Filter;
use Zymonic;
$Zymonic::session = '';
$Zymonic::system  = '';

# until Utils::send_email is working
use MIME::Base64;
use Mail::Sendmail 0.75;
use XML::Simple;
use Storable qw(dclone);

# Clean path
$ENV{PATH} = "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/sbin:/bin:/usr/bin:/sbin:/usr/sbin";

# set binmode for STDOUT and STDERR to handle unicode
binmode STDOUT, ':utf8';
binmode STDERR, ':utf8';

# jobs
my @jobs      = ();
my @jobs_done = ();
$SIG{'INT'} = 'scheduler_death_handler';

# get args
my $system = $ARGV[0];
unless ($system)
{
    usage();
}

my $log_file = $ARGV[1];
if ($log_file)
{
    $log_file = clean( $log_file, '_.\\/' );
    my $fh;
    open $fh, ">$log_file" or die "Unable to open logfile: $!\n";
    $Zymonic::Utils::debugfile = $fh;
}
else
{

    # default to STDOUt
    my $fh;
    open $fh, ">&STDOUT";
    $Zymonic::Utils::debugfile = $fh;
}

# setup config for given system
$Zymonic::system = clean( $system, '_' );
scheduler_debug("Zymonic starting $Zymonic::system");
$Zymonic::ZCONFIG{$Zymonic::system} = Zymonic::Config->new(
    system_name => $Zymonic::system,
    config_dir  => "/etc/zymonic",
    ip_address  => 'SCHEDULER',
    protocol    => 'http'
);

# Load an auth object for securing records
my $auth = Zymonic::Auth->new(
    config     => $Zymonic::ZCONFIG{$Zymonic::system},
    DB         => $Zymonic::ZCONFIG{$Zymonic::system}->{DB},
    ip_address => $Zymonic::ZCONFIG{$Zymonic::system}->{ip_address}
);

# load processes table
my $processes_table = Zymonic::Table->new(
    zname  => 'zz_process',
    ident  => '',
    config => $Zymonic::ZCONFIG{$Zymonic::system},
    auth   => $auth,
    DB     => $Zymonic::ZCONFIG{$Zymonic::system}->{DB},
);

# load the schedule table
my $schedule_table = Zymonic::Table->new(
    zname  => 'zs_schedules',
    ident  => '',
    config => $Zymonic::ZCONFIG{$Zymonic::system},
    auth   => $auth,
    DB     => $Zymonic::ZCONFIG{$Zymonic::system}->{DB},
);

# load the schedule mapping table
my $schedule_mapping_table = Zymonic::Table->new(
    zname  => 'zs_schedule_mappings',
    ident  => '',
    config => $Zymonic::ZCONFIG{$Zymonic::system},
    auth   => $auth,
    DB     => $Zymonic::ZCONFIG{$Zymonic::system}->{DB},
);

# use scheduler filter to find schedules and lookup search fields
my $scheduler_filter = Zymonic::Filter->new(
    zname            => 'zs_filter',
    config           => $Zymonic::ZCONFIG{$Zymonic::system},
    auth             => $auth,
    DB               => $Zymonic::ZCONFIG{$Zymonic::system}->DB,
    ident            => '',
    block_parameters => {},

    # don't cache results as this is a daemon so changes need to be picked up
    skip_cache => 'true',
);
$scheduler_filter->{autorun} = 'Y';
my $minutes_search   = $scheduler_filter->get_search_field('zsf_minutes_sf');
my $hours_search     = $scheduler_filter->get_search_field('zsf_hours_sf');
my $days_search      = $scheduler_filter->get_search_field('zsf_days_sf');
my $week_days_search = $scheduler_filter->get_search_field('zsf_week_days_sf');
my $months_search    = $scheduler_filter->get_search_field('zsf_months_sf');

# only show active filters
$scheduler_filter->get_search_field('zsf_active_sf')->set_value_from_other('Y');

# will now loop forever (until killed)
get_jobs();

## REST IS FUNCTIONS ##

# one process will loop putting schedules into jobs list
sub get_jobs
{

    # vars needed
    my @times = (time);
    my $time;
    my @schedules;

    # loop indefinitely
    while (1)
    {

        # re-connect the DB
        $Zymonic::ZCONFIG{$Zymonic::system}->{DB}->connect('reconnect');

        # get schedule ids
        $time      = shift @times;
        @schedules = get_schedules($time);
        if ( @schedules == 0 )
        {
            scheduler_debug( 'No Schedules at ' . localtime($time) );
        }
        else
        {
            while (@schedules)
            {
                my $schedule = shift @schedules;
                $schedule->{run_time} = scalar localtime($time);

                # if process_id is set, then need to lookup the zname
                if ( $schedule->{scheduled_process_id} )
                {
                    $schedule->{scheduled_process} =
                      $Zymonic::ZCONFIG{$Zymonic::system}
                      ->zname_from_id( 'Process', $schedule->{scheduled_process_id} );
                }

                # load field mapping
                my $field_mappings = get_field_mappings( $schedule->{id} );

                # add job to queue
                push( @jobs, { schedule => dclone($schedule), field_mappings => dclone($field_mappings) } );
            }
        }

        # do the jobs
        do_jobs();

        # add explicit debug which should flush all previous debugs to file
        scheduler_debug("FLUSHING LOG\n");

        # if missed any minutes, add to list
        my $mins_missed = ( localtime(time) )[1] - ( localtime($time) )[1];
        if ( $mins_missed > 1 )
        {
            map { push( @times, $time + ( 60 * $_ ) ) } ( 1 .. $mins_missed );
        }
        else
        {

            # wait until next minute
            while ( ( localtime($time) )[1] == ( localtime(time) )[1] ) { sleep(1); }
        }

        # add current time
        push( @times, time );
    }
}

# pull jobs from jobs list ad perform them
sub do_jobs
{
    while (@jobs)
    {
        my $job = shift @jobs;
        run_schedule( $job->{schedule}, $job->{field_mappings} );
        push( @jobs_done, $job );
    }
}

# looks up all schedules for the current (or given) time,
# maps them into simple hashrefs
sub get_schedules
{
    my $time = shift || time;
    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime($time);
    ++$mon;    # to range 1-12;

    # set search fields and run filter to find schedules
    $minutes_search->set_value_from_other($min);
    $hours_search->set_value_from_other($hour);
    $days_search->set_value_from_other($mday);
    $week_days_search->set_value_from_other($wday);
    $months_search->set_value_from_other($mon);

    # get list of schedule ids from filter
    my $scheduler_filter_output = $scheduler_filter->output();
    my @results = get_array( $scheduler_filter_output->{ $scheduler_filter->{zname} }->{report}->{result} );
    my @schedule_ids = grep { $_ } map { $_->{zs_id}->{Value}->{content} } @results;

    # stop now if no schedules found
    return () unless @schedule_ids;

    # lookup each schedule and build list of hashrefs
    my @schedules = ();
    $schedule_table->get_all_records(
        {
            where_clause => 'schedules.id IN (' . join( ', ', map { '?' } @schedule_ids ) . ')',
            where_params => [@schedule_ids]
        }
    );
    foreach my $schedule ( get_array( $schedule_table->{records} ) )
    {
        push(
            @schedules,
            {
                map {
                    $_->{zname}   => $schedule_table->get_field_ref_value($_),
                      $_->{field} => $schedule_table->get_field_ref_value($_),
                } values( %{ $schedule_table->get_fields($schedule) } )
            }
        );
    }

    # return sorted list
    return sort { ( $a->{sequence} || 999 ) <=> ( $b->{sequence} || 999 ) } @schedules;
}

# given a schedule id will return a hashref of field mappings
sub get_field_mappings
{
    my $schedule_id = shift;

    # get all mapppings that match this scheduler and add them to hash
    my $field_mappings = {};
    $schedule_mapping_table->get_all_records( { where_clause => 'schedule_id = ?', where_params => [$schedule_id] } );
    foreach my $mapping ( get_array( $schedule_mapping_table->{records} ) )
    {
        my $field_value = $schedule_mapping_table->get_field_value( 'zs_mapping_value', $mapping );
        if ($field_value)
        {
            my $field_zname = $schedule_mapping_table->get_field_value( 'zs_mapping_field', $mapping );
            if ($field_zname)
            {
                $field_mappings->{$field_zname} = $field_value;
            }
        }
    }
    return $field_mappings;
}

# runs the given scheduler
sub run_schedule
{
    my $schedule       = shift || {};
    my $field_mappings = shift || {};
    scheduler_debug("Running Schedule: $schedule->{name} at $schedule->{run_time}");

    # Load new auth object and 'log in' as schedule user
    # TODO: check if this is the correct way to do things
    #       or do we need special scheduler permissions
    my $auth = Zymonic::Auth->new(
        config     => $Zymonic::ZCONFIG{$Zymonic::system},
        DB         => $Zymonic::ZCONFIG{$Zymonic::system}->{DB},
        ip_address => $Zymonic::ZCONFIG{$Zymonic::system}->{ip_address}
    );
    $auth->{user} = $schedule->{username};
    $auth->get_user_record();

    # load the desired process
    my $process = Zymonic::Process->new(
        zname            => $schedule->{scheduled_process},
        process_id       => $schedule->{scheduled_process_id} || '',
        config           => $Zymonic::ZCONFIG{$Zymonic::system},
        auth             => $auth,
        DB               => $Zymonic::ZCONFIG{$Zymonic::system}->{DB},
        ident            => '',
        block_parameters => {}
    );

    # load the mapping for this schedule, can only do this if process has a form
    if ( $process->{current_state} && $process->{current_state}->{form} )
    {
        my $form = $process->{current_state}->{form};

        # map to first record
        my $form_record = $form->{records}->[0];
        if ($form_record)
        {
            foreach my $form_field (
                map  { $form->get_field_object($_) }
                grep { $field_mappings->{ $_->{zname} } } values %{ $form->get_fields($form_record) }
              )
            {
                $form_field->set_value_from_other( $field_mappings->{ $form_field->{zname} } );
            }
        }
    }

    # set transition param for process and get it's output
    $process->{requested_transition} = $schedule->{scheduled_transition};
    my $process_output;
    eval {
        $process_output = $process->output();
        scheduler_debug("Success");
        send_email(
            $schedule->{success_email},
            $schedule->{success_email_cc},
            $schedule->{success_email_bcc},
            "Schedule Run: $schedule->{name} at $schedule->{run_time}",
            "Schedule $schedule->{name} ran sucessfully at $schedule->{run_time}. Attached is its output.",
            XMLout(
                $process_output,
                KeyAttr       => [],
                RootName      => 'Zymonic',
                NoEscape      => 1,
                NoIndent      => 1,
                SuppressEmpty => 1
            )
        );
        1;
    } or do
    {
        my $error_report = death_handler( $@, '', 'return_error' );
        scheduler_debug("An error occured, $error_report->{reference}");
        send_email(
            $schedule->{failure_email},
            $schedule->{failure_email_cc},
            $schedule->{failure_email_bcc},
            "Schedule Failure: $schedule->{name} at $schedule->{run_time}",
            "Running Schedule $schedule->{name} at $schedule->{run_time} caused an error.\n\n"
              . $error_report->{message}->{content}
        );
    };
}

# function to send a scheduler email
sub send_email
{
    my $email   = shift;
    my $cc      = shift || '';
    my $bcc     = shift || '';
    my $subject = shift;
    my $body    = shift;
    my $xml     = shift || '';

    return unless $email;
    scheduler_debug("Sending email to $email");

    my $smtp_server = $Zymonic::ZCONFIG{$Zymonic::system}->{email_smtp_server};
    unless ($smtp_server)
    {
        scheduler_debug("No email_smtp_server set, unable to send email");
        return;
    }

    # create mail object
    my %mail = (
        from    => 'zymonic-scheduler@zymonic.com',
        to      => $email,
        cc      => $cc,
        bcc     => $bcc,
        subject => $subject,
        smtp    => $smtp_server,
    );
    if ($xml)
    {

        # encode XML and add as attachments as shown here:
        # http://alma.ch/perl/Mail-Sendmail-FAQ.html#attachments
        my $encoded_xml = encode_base64($xml);
        my $boundary    = "====" . time() . "====";

        $mail{'content-type'} = "multipart/mixed; boundary=\"$boundary\"";
        $mail{body} .= <<END_OF_BODY;
--$boundary
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable

$body
--$boundary
Content-Type: application/octet-stream; name="scheduled_process_output.xml"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="scheduled_process_output.xml"

$encoded_xml
--$boundary--
END_OF_BODY
    }
    else
    {
        $mail{'content-type'} = 'text/plain; charset="iso-8859-1"';
        $mail{body} = $body;
    }

    sendmail(%mail) or scheduler_debug("Unable to send mail because: $Mail::Sendmail::error");
}

sub scheduler_debug
{
    my $msg = shift;
    debug("ZS: $msg");
}

sub scheduler_death_handler
{
    print "Stoppping...\n";
    scheduler_debug(
        "Stopping... " . ( scalar @jobs_done ) . " schedules run.\n" . ( scalar @jobs ) . " schedules in queue." );
    exit(0);
}

sub usage
{
    print STDERR "Usage: zymonic_scheduler.pl [system] [log file (optional)]\n";
    exit(0);
}
