#------------------------------------------------------------
# Copyright (C) 2006 RoyaltyShare, Inc.   All Rights Reserved
#------------------------------------------------------------
package Job::Job;

use strict;
use warnings;
use Data::Dumper;

use Date::Calc;
use Date::Calc qw(Delta_DHMS);
use JSON;
use XML::Simple;

use lib '/app/tools/common/lib';
use Common::Log;
use Common::Email;
use Common::Client;
use Common::Assert;
use Common::XMLObject;
use base 'Common::XMLObject';

use lib '/app/tools/job/lib/';
use Job::DB::Item::Job;
use Job::Log;

#use Job::Status;
use Job::Config;

use constant kExitCodeJobFailedToStart => -1;
use constant kExitCodeUnknownState     => -2;
use constant kExitCodeSuccess          => 0;

use constant kStatusDone    => 'Done';
use constant kStatusRunning => 'Running';
use constant kStatusPending => 'Pending';
use constant kStatusAbort   => 'Abort';
use constant kStatusAborted => 'Aborted';

use constant kPriorityHigh   => 100000;
use constant kPriorityNormal => 10000;
use constant kPriorityLow    => 10;

use constant kHeartbeatTimeout => 1800;    # 30 minutes

# This is the 'virtual constructor' to use in order to instantiate a Job
# object when you have the Job's ID.
#
sub CreateFromJobID {
    my ( $class, $id ) = @_;
    assert($class);
    assert($id);

    my $dbItem = Job::DB::Item::Job->Lookup( job_id => $id );

    return $class->CreateFromDBItem($dbItem);
}

sub CreateFromDBItem {
    my ( $class, $dbItem ) = @_;
    assert($class);
    assert($dbItem);

    # !!! Wrap the whole thing in an eval block, so we can update the database record's
    # !!! state if we fail for some reason.
    #
    my $obj;
    eval {
        # !!! If there is a 'command_line' column, then this is an 'old' style job.
        # !!! We're not going to bless it into a specific subclass, since we don't
        # !!! know what that is.
        if ( $dbItem->command_line ) {
            $obj = $class->new( _dbItem => $dbItem );
        } else {
            my ( $actualClass, %commandLineArguments ) = _ParseXML( $dbItem->command_xml );

            # So, for this to _work_, you're going to have to have the @INC array set up correctly.
            # If this is getting called in the web app, you should be good to go.
            # If you are calling this from a script, though, you will need to make sure
            # all necessary library paths have been included.
            #
            eval "require $actualClass";
            die "$@" if ($@);
            $obj = $actualClass->new( _dbItem => $dbItem, %commandLineArguments );
        }
    };

    if ($@) {
        Common::Log::Print("ERROR - Could not instantiate the Job object: $@");
        $dbItem->exit_code(kExitCodeJobFailedToStart);
        $dbItem->end_date(Common::DB::Item::kDateTimeNow);
        $dbItem->save();

    }
    return $obj;
}

# This will create a new Job object that is a copy of the original Job you pass in.
# It will be blessed into the correct subclass, and contain the same
# command-line arguments.
# It will _not_ inherit the queue-related state: It will be a fresh object waiting
# to be enqueued.
#
sub Copy {
    my ( $class, $objToCopy ) = @_;
    assert( $objToCopy->isa('Job::Job') );

    my $subclass = ref($objToCopy);
    my $argHash  = $objToCopy->getAllCommandLineArgs();
    my $newObj   = $subclass->new(%$argHash);

    # !!! One special hack - We want to preserve the original job's client id.
    #
    $newObj->{ClientID} = $objToCopy->clientID();

    # Hack for old style jobs
    if ( $objToCopy->{CommandLine} ) {
        $newObj->{CommandLine} = $objToCopy->{CommandLine};
        $newObj->{Priority}    = $objToCopy->{Priority};
        $newObj->{Subclass}    = $objToCopy->{Subclass};

        #print STDERR Dumper $objToCopy; die;
    }

    return $newObj;
}

sub complete {
    my $self     = shift;
    my %args     = @_;
    my $exitCode = defined $args{exitCode} ? $args{exitCode} : kExitCodeUnknownState;

    $self->setExitCode($exitCode);

    if ( $exitCode == kExitCodeSuccess ) {
        $self->_processSuccess();
    } else {
        $self->_processFailure();
    }
}

# We don't have an explicit 'new' method, because this class inherits from XMLObject.
# Which means that it inherits the basic constructor, and a writeXML method.
#
sub _init {
    my ( $self, %args ) = @_;

    $self->SUPER::_init(%args);

    # We won't refresh state more than once a second.
    # So we'll check this value in the refresh() method.
    #
    $self->{_lastRefresh} = time();

    my %properties;
    $self->_getProperties( \%properties, %args );

    $self->_initProperties( \%properties );
    return $self;
}

# This is called internally when we wish to reset our state.
# The main reason we break this out is so we don't lose our
# original command-line arguments.
#
sub _reInit {
    my ( $self, $dbItem ) = @_;

    my ( $actualClass, %commandLineArguments ) = _ParseXML( $dbItem->command_xml )
      if ( $dbItem->command_xml );

    return $self->_init( _dbItem => $dbItem, %commandLineArguments );
}

sub _getProperties {
    my ( $self, $properties, %args ) = @_;
    if ( $args{_dbItem} ) {

        # We probably don't have any args, yet.
        # We need to pull those out of the XML, if there is any.
        # But let any state in the %args hash win...
        #
        $self->_propertiesFromDBItem( $properties, $args{_dbItem} );
    }

    foreach my $key ( keys %args ) {
        next if '_' eq substr( $key, 0, 1 );
        my $value = $args{$key};
        $properties->{command_line_args}{$key} = $value;
    }
}

sub _propertiesFromDBItem {
    my ( $self, $properties, $dbItem ) = @_;
    assert($dbItem);

    $properties->{job_id}         = $dbItem->job_id;
    $properties->{hostname}       = $dbItem->hostname;
    $properties->{client_id}      = $dbItem->client_id;
    $properties->{class}          = $dbItem->class;
    $properties->{subclass}       = $dbItem->subclass;
    $properties->{command_line}   = $dbItem->command_line;
    $properties->{command_xml}    = $dbItem->command_xml;
    $properties->{pid}            = $dbItem->pid;
    $properties->{priority}       = $dbItem->priority;
    $properties->{min_start_date} = $dbItem->min_start_date;
    $properties->{queue_date}     = $dbItem->queue_date;
    $properties->{start_date}     = $dbItem->start_date;
    $properties->{end_date}       = $dbItem->end_date;
    $properties->{heartbeat}      = $dbItem->heartbeat;
    $properties->{exit_code}      = $dbItem->exit_code;
    $properties->{on_hold}        = $dbItem->on_hold;
    $properties->{run_hostname}   = $dbItem->run_hostname;
    $properties->{log_file}       = $dbItem->log_file;
    $properties->{log_path}       = $dbItem->log_path;
    $properties->{paused}         = $dbItem->paused;
    $properties->{aborted}        = $dbItem->aborted;
    $properties->{parent_job_id}  = $dbItem->parent_job_id;

}

sub _initProperties {
    my ( $self, $properties ) = @_;

    # This is not a Common::FormObject.
    # So we don't need to use the fancy-schmancy FormObject::Scalar objects for
    # these properties - We won't be assigning values to Job object via a call to
    # assignCGIParams().
    #
    $self->{JobID}        = $properties->{job_id};
    $self->{HostName}     = $properties->{hostname};
    $self->{Class}        = $properties->{class};
    $self->{Subclass}     = $properties->{subclass};
    $self->{ClientID}     = $properties->{client_id};
    $self->{CommandLine}  = $properties->{command_line};
    $self->{CommandXML}   = $properties->{command_xml};
    $self->{PID}          = $properties->{pid};
    $self->{MinStartDate} = $properties->{min_start_date};
    $self->{QueueDate}    = $properties->{queue_date};
    $self->{StartDate}    = $properties->{start_date};
    $self->{EndDate}      = $properties->{end_date};
    $self->{Heartbeat}    = $properties->{heartbeat};
    $self->{ExitCode}     = $properties->{exit_code};
    $self->{Priority}     = $properties->{priority};
    $self->{OnHold}       = $properties->{on_hold};
    $self->{Paused}       = $properties->{paused};
    $self->{Aborted}      = $properties->{aborted};
    $self->{ParentJobID}  = $properties->{parent_job_id};

    $self->{CommandLineArguments} = $properties->{command_line_args};

    $self->{RunHostName} = $properties->{run_hostname};
    $self->{LogFile}     = $properties->{log_file};
    $self->{LogPath}     = $properties->{log_path};

    my $startDate = $properties->{start_date};
    my $endDate = $properties->{end_date};
    my $heartbeat = $properties->{heartbeat};

    # To calculate the job duration, we need a start date
    # and either an end date or a heartbeat.
    if ($startDate && $startDate ne '0000-00-00 00:00:00'
        && (($endDate && $endDate ne '0000-00-00 00:00:00')
            || ($heartbeat && $heartbeat ne '0000-00-00 00:00:00'))
        ) {
        $startDate =~ s/[:-]/ /g;
        my @start = split ' ', $startDate;

        if ($endDate eq '0000-00-00 00:00:00') {
            # If the job is still running, use the heartbeat.
            $endDate = $heartbeat;
        }
        $endDate =~ s/[:-]/ /g;
        my @end = split ' ', $endDate;

        my @diff = Delta_DHMS(@start,@end);
        my $seconds = $diff[3];
        my $minutes = $diff[2];
        my $hours   = $diff[1];
        my $days    = $diff[0];
        $hours += $days * 24;

        my $duration = sprintf '%02d:%02d', $minutes, $seconds;
        if ($hours > 0) {
            $duration = $hours . ":" . $duration;
        }
        $self->{Duration} = $duration;
    }

    # We only care about child counts for running jobs
    if ( $self->getStatus() eq Job::Status::kRunning ) {
        $self->{ChildCount}        = $self->getChildCount();
        $self->{WaitingChildCount} = $self->getWaitingChildCount();
    }

    # Distill the job's status down to a simple property.
    #
    my $status;
    if ( defined $properties->{end_date} && $properties->{end_date} ne Common::DB::Item::kDateTimeNULL ) {
        $status = kStatusDone;
    } elsif ( defined $properties->{start_date} && $properties->{start_date} ne Common::DB::Item::kDateTimeNULL ) {
        $status = kStatusRunning;
    } elsif ( defined $properties->{queue_date} && $properties->{queue_date} ne Common::DB::Item::kDateTimeNULL ) {
        $status = kStatusPending;
        $self->{ParentPaused} = $self->getParentPaused();
    }

    # Let's see if there's a Run ID to pull out of the command XML
    #
    if ($self->{CommandXML} =~ /<Value>(\d+)<\/Value>\s*<Name>runID<\/Name>/) {
        $self->{RunID} = $1;
    }

    $self->{RunStatus} = $status;

    # If the time since the last heartbeat exceeds our timeout threshold, the job is _probably_ dead.
    # We won't change the state, but I do want to set a flag in the XML we can use to hilight that in the UI.
    #
    if ( $self->{Heartbeat} && $self->{Heartbeat} ne '0000-00-00 00:00:00' ) {
        my $heartBeatSeconds = Common::Util::dateTimeToSeconds( $self->{Heartbeat} );
        my $now = time();
        if ( $now - $heartBeatSeconds > kHeartbeatTimeout ) {
            $self->{HeartbeatStale} = 1;
        }
    }
}

sub _processSuccess {
    my $self = shift;

    $self->_notifySuccess();
}

sub _processFailure {
    my $self = shift;

    $self->_notifyFailure();
    $self->_notifyFailureSlack();
}

sub _notifySuccess {
    my $self = shift;
    my @to   = $self->_notifyFailureRecipients();

    if ( @to > 1 ) {
        die "Recipient list must be a single scalar or list reference";
    }

    if ( $self->_notifySuccessRecipients() ) {
        Common::Email->SendAWS(
            to      => $self->_notifySuccessRecipients(),
            from    => 'do-not-reply@royaltyshare.com',
            subject => $self->_notifySuccessSubject(),
            body    => $self->_notifySuccessBody(),
        );
    }
}

sub _notifyFailure {
    my $self = shift;

    my @to   = $self->_notifyFailureRecipients();

    if ( @to > 1 ) {
        die "Recipient list must be a single scalar or list reference";
    }

    if ( $self->_notifyFailureRecipients() ) {
        Common::Email->SendAWS(
            to      => $self->_notifyFailureRecipients(),
            from    => 'do-not-reply@royaltyshare.com',
            subject => $self->_notifyFailureSubject(),
            body    => $self->_notifyFailureBody(),
        );
    }
}

sub _notifyFailureSlack {
    my $self = shift;

    my %variables = $self->_notifyFailureVariables();

    if ( %variables ) {
        my $postURL;
        if( Common::RSApp::IsProductionServer ) {
            $postURL = $self->_notifyFailureURL();
        } else {
           $postURL = $self->_notifyFailureTestURL();
        }

        if ($postURL) {
            # First, let's add the ClientName and JobURL variables.
            my $clientID = $self->clientID();
            my $clientData = Common::DB::Item::Client->Lookup( client_id => $clientID );
            my $clientName = $clientData->client_name;
            $variables{"ClientName"} = $clientName;

            my $supportURL = $self->_getSupportURL();
            my $jobURL = "$supportURL/job_queue?c=show_job&JobID=" . $self->id();
            $variables{"JobURL"} = $jobURL;

            # Now let's convert it to json
            my $json = encode_json \%variables;

            print "Sending notification to slack\n";

            # Send it
            my $request = `curl --header "Content-Type: application/json" \\
            --request POST \\
            --data '$json' \\
            $postURL 2>&1`;

            # Log the output
            print $request . "\n";
        }
    }
}


sub _notifySuccessRecipients { }
sub _notifySuccessSender     { '"Job Notifier" <noreply@royaltyshare.com>'; }

sub _notifySuccessSubject {
    my $self = shift;
    return "Job Run Complete (" . $self->runHostname() . ")";
}
sub _notifySuccessBody { assert( 0, "Must be overloaded" ) }

sub _notifyFailureRecipients { }
sub _notifyFailureSender     { '"Job Notifier" <noreply@royaltyshare.com>'; }

sub _notifyFailureVariables { }

sub _notifyFailureURL { }

sub _notifyFailureTestURL { }

sub _notifyFailureSubject {
    my $self = shift;
    return "Job Run Failed (" . $self->runHostname() . ")";
}
sub _notifyFailureBody { assert( 0, "Must be overloaded" ) }

# I'd prefer we use an abstract interface to access
# these (rather than just de-referencing {CommandLineArguments}.
#
sub getCommandLineArg {
    my ( $self, $argName ) = @_;

    return undef unless $self->{CommandLineArguments};
    return $self->{CommandLineArguments}{$argName};
}

sub getAllCommandLineArgs {
    my ($self) = @_;
    return $self->{CommandLineArguments};
}

#
# This class is 'quasi-stateless'.
# What I mean by that is that for those properties that could conceivably
# change from moment-to-moment, I will always go to the database and look them
# up - I won't 'cache' any of that state.
#
# Hence, some of these accessor methods will _always_ call _refresh() to reload the
# Job's state.
#

sub id {
    my ($self) = @_;
    return $self->{JobID};
}

sub hostname {
    my ($self) = @_;
    return $self->{HostName};
}

sub clientID {
    my ($self) = @_;
    return $self->{ClientID};
}

sub class {
    my ($self) = @_;
    return $self->{Class};
}

sub subclass {
    my ($self) = @_;
    return $self->{Class};
}

# !!! Deprecating this method.
#sub commandLine
#{
#    my ($self) = @_;
#    return $self->{CommandLine};
#}

sub commandXML {
    my ($self) = @_;
    return $self->{CommandXML};
}

sub minStartDate {
    my ($self) = @_;
    return $self->{MinStartDate};
}

sub pid {
    my ($self) = @_;

    $self->_refresh();
    return $self->{PID};
}

sub aborted {
    my ($self) = @_;

    $self->_refresh();
    return $self->{Aborted};
}

sub parentJobID {
    my ($self) = @_;
    return $self->{ParentJobID};
}

sub paused {
    my ($self) = @_;

    $self->_refresh();
    return $self->{Paused};
}

sub onHold {
    my ($self) = @_;

    $self->_refresh();
    return $self->{OnHold};
}

sub startDate {
    my ($self) = @_;

    $self->_refresh();
    return $self->{StartDate};
}

sub queueDate {
    my ($self) = @_;

    $self->_refresh();
    return $self->{EndDate};
}

sub endDate {
    my ($self) = @_;

    $self->_refresh();
    return $self->{EndDate};
}

sub exitCode {
    my ($self) = @_;

    $self->_refresh();
    return $self->{ExitCode};
}

sub heartbeat {
    my ($self) = @_;

    $self->_refresh();
    return $self->{Heartbeat};
}

sub priority {
    my ($self) = @_;

    # priority value is variable.
    # Whenever a job is queued, the priority of all jobs already in the queue
    # is incremented.
    # This prevents old jobs from getting stuck forever in the queue.
    #
    $self->_refresh();
    return $self->{Priority};
}

sub runHostname {
    my ($self) = @_;
    return $self->{RunHostName};
}

sub logFile {
    my ($self) = @_;

    return $self->{LogFile};
}

sub logDir {
    my ($self) = @_;

    return $self->{LogDir};
}

sub setLogFile {
    my ( $self, $logFileName ) = @_;
    my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );
    $newDBItem->log_file($logFileName);
    $newDBItem->save();
    $self->_reInit($newDBItem);
}

sub setLogPath {
    my ( $self, $logPath ) = @_;
    my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );
    $newDBItem->log_path($logPath);
    $newDBItem->save();
    $self->_reInit($newDBItem);
}

sub getChildCount {
    my ($self) = @_;

    return Job::DB::Item::Job::ChildCount( $self->{JobID} );
}

sub getWaitingChildCount {
    my ($self) = @_;

    return Job::DB::Item::Job::WaitingChildCount( $self->{JobID} );
}

sub getParentPaused {
    my ($self) = @_;

    return unless ( $self->{ParentJobID} );

    my $DBItem = Job::DB::Item::Job->Lookup( job_id => $self->{ParentJobID} );

    return $DBItem ? $DBItem->paused : undef;
}

sub getStatus {
    my ($self) = @_;

    my $endDate = $self->endDate();
    return Job::Status::kComplete if ( $endDate && ( Common::DB::Item::kDateTimeNULL ne $endDate ) );

    my $startDate = $self->startDate();
    my $heartbeat = $self->heartbeat();
    my $paused    = $self->paused();

    if ( $startDate && Common::DB::Item::kDateTimeNULL ne $startDate ) {
        return Job::Status::kRunning if ($paused);

        # Is the heart still beating?
        # We'll give it 2 minutes...
        #
        if ( $heartbeat ne Common::DB::Item::kDateTimeNULL
            && kHeartbeatTimeout <= _dateDeltaSeconds( Common::Util::today_and_now(), $heartbeat ) ) {
            return Job::Status::kDead;
        }
        return Job::Status::kRunning;
    }

    if ( $self->queueDate && Common::DB::Item::kDateTimeNULL ne $self->queueDate() ) {
        return Job::Status::kQueued;
    }

    return Job::Status::kNew;
}

sub _dateDeltaSeconds {
    my ( $d1, $d2 ) = @_;
    my $d1Seconds = Common::Util::dateTimeToSeconds($d1);
    my $d2Seconds = Common::Util::dateTimeToSeconds($d2);
    return $d1Seconds - $d2Seconds;
}

sub _refresh {
    my ($self) = @_;

    if ( $self->{JobID} && ( !$self->{_lastRefresh} || 1 < ( time() - $self->{_lastRefresh} ) ) ) {

        # For now, we'll just query the database every time.  There isn't a lot of data.
        #
        my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );
        $self->_reInit($newDBItem);
    }
}

sub getCommandLine {
    my ($self) = @_;

    if ( !$self->{_actualCommandLine} ) {
        if ( $self->{CommandLine} ) {
            $self->{_actualCommandLine} = $self->{CommandLine};
        } else {
            $self->{_actualCommandLine} = $self->_generateCommandLine();
        }
    }

    return $self->{_actualCommandLine};
}

sub setAborted {
    my ($self) = @_;
    my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );
    $newDBItem->aborted(1);
    $newDBItem->end_date(Common::DB::Item::kDateTimeNow);
    $newDBItem->start_date(Common::DB::Item::kDateTimeNow);
    $newDBItem->save();
    $self->_reInit($newDBItem);
}

sub setOnHold {
    my ( $self, $onHoldFlag ) = @_;
    my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );
    $newDBItem->on_hold($onHoldFlag);
    $newDBItem->save();
    $self->_reInit($newDBItem);
}

sub setPaused {
    my ( $self, $pausedFlag ) = @_;
    my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );
    $newDBItem->paused($pausedFlag);
    $newDBItem->save();
    $self->_reInit($newDBItem);
}

sub setHostname {
    my ( $self, $newHostname ) = @_;

    my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );

    # Sanity check... the job should only be getting a 'new' hostname if it doesn't
    # already have one.
    # It's ok to attempt to set the hostname to the job's current hostname - but we don't
    # allow the hostname to ever _change_ once it is set.
    #
    assert( !$newDBItem->hostname() || $newDBItem->hostname() eq $newHostname, "cannot change a job's host once it is set" );

    $newDBItem->hostname($newHostname);
    $newDBItem->save();
    $self->_reInit($newDBItem);
}

sub setPID {
    my ( $self, $pid ) = @_;

    my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );

    # PID should only be set _once_ !!!
    #
    assert( !$newDBItem->pid(), 'Cannot set the PID of a job that already HAS a PID!!!' );

    $newDBItem->pid($pid);
    $newDBItem->save();
    $self->_reInit($newDBItem);
}

sub startJob {
    my ( $self, $pid ) = @_;
    my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );

    assert( !$newDBItem->pid(), 'Cannot set the PID of a job that already HAS a PID!!!' );

    $newDBItem->pid($pid);
    $newDBItem->start_date(Common::DB::Item::kDateTimeNow);
    $newDBItem->run_hostname( Common::RSApp::GetHostname() );

    $newDBItem->save();
    $self->_reInit($newDBItem);
}

sub setExitCode {
    my ( $self, $exitCode ) = @_;

    my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );

    $newDBItem->exit_code($exitCode);
    $newDBItem->end_date(Common::DB::Item::kDateTimeNow);
    $newDBItem->save();
    $self->_reInit($newDBItem);

}

# This will give us some sort of way to tell when a job has unexpectedly croaked.
sub sendHeartbeat {
    my ($self) = @_;

    if ( Job::Status::kRunning eq $self->getStatus() ) {
        Job::DB::Item::Job->updateHeartbeat( $self->{JobID} );
        my $newDBItem = Job::DB::Item::Job->Lookup( job_id => $self->{JobID} );
        $self->_reInit($newDBItem);
    }
}

sub enqueue {
    my ( $self, %queueArgs ) = @_;

    # Create the 'job_xml' string.
    #
    my %xmlData;
    $xmlData{Class} = ref($self);

    my $args = $self->{CommandLineArguments};
    if ($args) {
        my @argArray;
        foreach my $name ( keys %$args ) {
            if ( defined $args->{$name} ) {
                push @argArray,
                  {
                    Name  => $name,
                    Value => $args->{$name},
                  };
            }
        }
        $xmlData{Param} = \@argArray;
    }

    my $xmlString = Common::WriteXML::GetXMLString( \%xmlData );

    # Construct the rest of the queue arguments.
    # Each class should override the 'default$X' methods.
    # In particular, defaultClass, defaultSubclass and defaultPriority
    #
    $queueArgs{client_id} = Common::Util::returnFirstDefined( $queueArgs{client_id}, $self->clientID(), Common::RSApp::GetClientID() );

    # Some clients are more important than others...
    # We'll run their jobs at a higher priority.
    #
    my $priorityScaling = Common::Client::Current->JobPriorityScale();
    assert( $priorityScaling, "ERROR - client has a 0 job_priority_scale factor!" );
    my $basePriority = Common::Util::returnFirstDefined( $queueArgs{priority}, $self->_defaultPriority );

    $queueArgs{priority} = $basePriority * $priorityScaling;

    $queueArgs{class}    = Common::Util::returnFirstDefined( $queueArgs{class},    $self->_defaultClass );
    $queueArgs{subclass} = Common::Util::returnFirstDefined( $queueArgs{subclass}, $self->_defaultSubclass );
    $queueArgs{hostname} = Common::Util::returnFirstDefined( $queueArgs{hostname}, $self->_defaultHostname );
    $queueArgs{log_file} = Common::Util::returnFirstDefined( $queueArgs{log_file}, $self->_defaultLogFile );
    $queueArgs{delay}    = Common::Util::returnFirstDefined( $queueArgs{delay},    $self->_defaultDelay );
    $queueArgs{command_xml} = $xmlString;

    my $newDBItem = $self->SubmitJobToQueue(%queueArgs);
    $self->_reInit($newDBItem);
}

sub SubmitJobToQueue {
    my $self = shift;
    my (%args) = @_;

    # !!! command_line is being deprecated in favor of command_xml
    assert( $args{command_xml} || $args{command_line}, 'command_xml is required' );
    assert( $args{priority} );

    $args{command_xml} = undef if ( $args{command_line} );

    # Translate 'delay' (a relative number of seconds) into an absolute datetime
    #
    if ( $args{delay} ) {
        my $absoluteDelayTime = $args{delay} + time();
        $args{min_start_date} = Common::Util::secondsToDateTime($absoluteDelayTime);
    }
    delete $args{delay};

    $args{hostname} = '' unless $args{hostname};
    $args{hostname} = cleanHostname( $args{hostname} );
    $args{class}    = '' unless $args{class};
    $args{subclass} = '' unless $args{subclass};

    # We'll insist on client_id as an argument.
    #
    assert( $args{client_id} );

    # Host name isnt required for job is it???
    # assert($args{hostname} || $args{class}, "either hostname or class must be specified");

    $args{queue_date} = Common::DB::Item::kDateTimeNow;

    use Data::Dumper;
    Common::Log::Debug( "Submit with " . Dumper \%args );

    # Create the database record item, then return it.
    #
    my $dbItem = Job::DB::Item::Job->Create(%args);
    $dbItem->save();

    return $dbItem;
}

# Fetch the next job off of the Queue, if there is one.
# It will fetch the highest priority job that either :
# - matches this machine's hostname or
# - matches one of the classes listed in the @classList array (optional)
#
# !!! I am thinking that this will change.  Right now it returns a job object...
# !!! I think I'd rather have it return a simple JobID.
# !!! Or, more usefully, it will return a DB::Item.
#
sub GetNextJobFromQueue {
    my %args      = @_;
    my $ruleset   = $args{ruleset};
    my $skiprules = $args{skiprules};
    my $runstatus = $args{runstatus};
    my $hostname  = $args{hostname};
    my @validRules;

    assert( $ruleset,   "Ruleset required" );
    assert( $skiprules, "Skip rules required" );
    assert( $runstatus, "Run status required" );
    assert( $hostname,  "hostname required" );

    my $rules = $ruleset->Rules();

    foreach my $rule (@$rules) {
        push( @validRules, $rule ) if ( $runstatus->canRun( $rule, $hostname ) );
    }

    my $dbItem = Job::DB::Item::Job->GetNextQueuedJob( rules => \@validRules, skiprules => $skiprules->SkipRules, hostname => $hostname )
      if (@validRules);

    return $dbItem;
}

#  Kill off children jobs.  This will first mark all un-run and running jobs as 'Abort'.
#  The Abort status tells the scheduler to attempt to kill the job.  This status
#  change will also prevent any new jobs from kicking off.  Then we sleep for a
#  couple seconds and then mark any un-run jobs as 'Aborted'.
#
#  NOTE: Some of the child jobs could complete before the scheduler has a chance
#  to kill them and will then have a 'Completed' status.
sub KillChildJobs {
    my $class = shift;
    my $parentJobID = shift || die;

    Job::DB::Item::Job->MarkChildrenAborted($parentJobID);
    sleep(2);
    Job::DB::Item::Job->MarkPendingChildrenComplete($parentJobID);
}

# Kill a job.  By marking the job aborted the scheduler will attempt to kill
# it.
sub kill {
    my $self = shift;

    $self->KillChildJobs( $self->id );
    $self->setAborted;
}

# A clean hostname is a hostname that has been stripped of all domain qualifiers, and
# converted to lower case.
#
sub cleanHostname {
    my ($hostname) = @_;
    return '' unless $hostname;

    ($hostname) = split( /\./, lc($hostname) );

    return $hostname;
}

# ---- Override these 'default' methods

sub _generateCommandLine {
    my ($self) = @_;
    assert( 0, "ERROR - you _must_ override _generateCommandLine in your subclass" );
}

# This is the default path.
#
sub generateLogBasePath {
    my ($self) = @_;

    my $jobConfig = Job::Config->new();
    my $path      = $jobConfig->get('log_dir');
    return $path;
}

sub generateLogFileName {
    my ($self) = @_;
    my $filename = $self->logFile();

    unless ($filename) {
        my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime( time() );
        my $date = sprintf( "%04d%02d%02d%02d%02d%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec );

        my $jobID    = $self->id();
        my $hostname = Common::RSApp::GetHostname();

        $filename = sprintf( "%d_%s_%s.log", $jobID, $hostname, $date );
    }

    return $filename;
}

sub _defaultClass {
    return undef;
}

sub _defaultSubclass {
    return undef;
}

sub _defaultHostname {
    return undef;
}

sub _defaultLogFile {
    return undef;
}

sub _defaultPriority {
    return Job::Job::kPriorityNormal;
}

sub _defaultDelay {
    return undef;
}

sub _ParseXML {
    my ($xmlString) = @_;

    my ( $actualClass, %commandLineArguments );

    my $parser = XML::Simple->new( ForceArray => ['Param'] );
    my $parsedData = $parser->XMLin($xmlString);

    $actualClass = $parsedData->{Class} or die "ERROR - Missing 'Class' tag";
    my $paramArray = $parsedData->{Param};
    if ($paramArray) {
        foreach my $paramBlock (@$paramArray) {
            $commandLineArguments{ $paramBlock->{Name} } = $paramBlock->{Value};
        }
    }
    return ( $actualClass, %commandLineArguments );
}

sub _getSupportURL {
    my $self     = shift;
    my $clientID = $self->clientID;
    my $client   = Common::Client->new( clientID => $clientID );

    my $clientName = $client->ClientNameClean();

    my $url = "http://$clientName.royaltyshare.com/support";

    return $url;
}

###
1;    #
###
