#---------------------------------------------------------------
# ____                   _ _         ____  _
#|  _ \ ___  _   _  __ _| | |_ _   _/ ___|| |__   __ _ _ __ ___
#| |_) / _ \| | | |/ _` | | __| | | \___ \| '_ \ / _` | '__/ _ \
#|  _ < (_) | |_| | (_| | | |_| |_| |___) | | | | (_| | | |  __/
#|_| \_\___/ \__, |\__,_|_|\__|\__, |____/|_| |_|\__,_|_|  \___|
#            |___/             |___/
#
# Copyright (C) 2008 RoyaltyShare, Inc.      All Rights Reserved
#---------------------------------------------------------------
package Job::Scheduler;

use strict;
use warnings;

use Data::Dumper;
use Date::Calc;

use lib '/app/tools/common/lib';
use lib '/app/tools/job/lib/';
use Common::Log;
use Common::Util;
use Common::Assert;
use Common::DB::Item;
use Job::Job;
use Job::Config;
use Job::DB::Item::JobHost;
use Job::JobRuleSet;

use Job::RuleList;
use Job::SkipRuleList;
use Job::RunStatus;

sub new {
    my ( $class, %args ) = @_;
    my $self = bless {}, $class;

    return $self->_init(%args);
}

sub report {
    my ( $self, $msg, $level ) = @_;

    $level = 1 unless $level;

    if ( $level <= $self->{_logLevel} ) {
        Common::Log::Print($msg);
    }
}

sub run {
    my ($self) = @_;
    my $hostname = $self->{_hostname};

    $self->report( "ENTERING RUN", 4 );

    # Fetch the job_host record.
    # If there isn't one, we'll just create a new record.
    #
    my $jobHostDBItem = Job::DB::Item::JobHost->Lookup( hostname => $hostname );
    if ( !defined $jobHostDBItem ) {
        $self->report( "No job_host record found for hostname $hostname - We'll create one", 2 );
        $jobHostDBItem = Job::DB::Item::JobHost->Create( hostname => $hostname );
    }

    $self->report( " CHECKING LOAD", 4 );
    my ( $currentLoad, $fiveMinLoad, $fifteenMinLoad ) = $self->_fetchLoadAverage();

    # So, we're alive - let's tell people.
    #
    $jobHostDBItem->os($^O);
    $jobHostDBItem->heartbeat(Common::DB::Item::kDateTimeNow);
    $jobHostDBItem->pid($$);
    $jobHostDBItem->load_average_1($currentLoad);
    $jobHostDBItem->load_average_5($fiveMinLoad);
    $jobHostDBItem->load_average_15($fifteenMinLoad);

    $jobHostDBItem->save();

    # The queue might be on hold...
    #
    if ( $jobHostDBItem->on_hold ) {
        $self->report( "$hostname job queue is on hold", 2 );
        return;
    }

    $self->{_runstatus}->refresh();

    # Now that we've recorded the load, we can exit if necessary.
    #
    if ( $self->{_maxLoad} && $currentLoad && $currentLoad > $self->{_maxLoad} ) {
        $self->report( "current load of $currentLoad exceeds max load of " . $self->{_maxLoad} . " - exiting", 2 );
        return;
    }

    # Fetch the next job off the queue.
    # This, in fact, returns a DB::Item.
    #
    my $jobDBItem;
    while (
        $jobDBItem = Job::Job::GetNextJobFromQueue(
            ruleset   => $self->{_ruleset},
            skiprules => $self->{_skiprules},
            runstatus => $self->{_runstatus},
            hostname  => $hostname
        )
      ) {
        my $jobID = $jobDBItem->job_id();

        # !!! I am starting to feel that this needs to change.
        # !!! Fundamentally, we're keeping this lock around way too long.
        # !!! These locks are actually pretty fragile - My main concern is that if in the
        # !!! future another AutoLock gets instantiated, our lock goes away.
        # !!! This is not that remote a possibility - We're instantiating a lot of objects
        # !!! here, calling getStatus, etc.  These are all doing stuff through abstract interfaces.
        # !!! Any of which could change in the future to include some behavior which invalidates
        # !!! the lock...
        #
        # !!! So I am going to re-establish the original, tighter locking behavior.
        # !!! GetNextJobFromQueue will do the locking, _AND_ it will update the run semaphore (i.e. start_date).
        # !!! All without ever touching any of our classes.
        # !!! Not going to bother with re-checking the run state in here - We refresh that state outside this loop anyway,
        # !!! so it's not like we are never going to notice that the rules have been altered.  True, if we're running
        # !!! on a high-speed server and the run state changes while we're in this loop firing off jobs, we won't abort.
        # !!! I can live with that.
        #
        #        my $lock = Common::DB::AutoLock->new( Common::RSApp::GetCommonDB(), 'job', 'table_state' );
        #
        #	    # If the jobs system run state has changed since we picked a job then just
        #    	# short circut
        #    	if( $self->{_runstatus}->hasRunStateChanged() ) {
        #	        Common::Log::Print " +++ Job run status has changed and needs to be refreshed.  Aborting job run";
        #    	    last;
        #	    }
        #
        #        # Check the job one last time just to make sure it hasn't started running
        #		my $j = Job::Job->new(jobID => $jobDBItem->job_id );
        #    	if( $j->getStatus eq Job::Status::kQueued ) {
        #		    Common::Log::Print " +++ Job run status has changed.  Aborting job run";
        #    	    last;
        #		}
        #
        #        # Set the start date so this job can't be selected again.
        #        # And set the run_hostname, so that this job will be considered
        #        # when we apply our 'local' run rules.
        #        #
        #		$jobDBItem->start_date(Common::DB::Item::kDateTimeNow);
        #        $jobDBItem->run_hostname($hostname);
        #        $jobDBItem->save();
        #
        #		# Unlock the job table.
        #		$lock = undef;

        # Ready to go - launch the job
        # !!! This might fail if things are not set up correctly.
        # In that case, we'll want to mark the job as 'complete'.
        #
        eval {
            # Lock the job table

            $self->_launchJob($jobID);
        };
        if ($@) {
            $self->report("Error launching job: $@");
            $jobDBItem->end_date(Common::DB::Item::kDateTimeNow);
            $jobDBItem->exit_code(Job::Job::kExitCodeJobFailedToStart);
            $jobDBItem->save();
        }

        # Keep dequing jobs if we're running in 'high speed' mode
        #
        if ( $self->{_highSpeed} ) {
            $self->{_runstatus}->forceRefresh();
        } else {
            last;
        }
    }

    $self->_killAbortedJobs( hostname => $hostname );    ### Linux Only

    $self->report( 'sleeping', 3 );
}

sub _launchJob {
    my ( $self, $jobID ) = @_;

    assert("ERROR - You must overload this method");
}

sub _killAbortedJobs { }

sub _init {
    my ( $self, %args ) = @_;

    # Establish all of our running parameters.
    # Order of precedence:
    #  - Command-line arguments (i.e. anything in the %args hash wins)
    #  - Config file

    assert( $args{hostname}, "hostname required" );

    my $config = $self->_parseConfig();

    $self->{_logLevel}  = Common::Util::returnFirstDefined( $args{logLevel},  $config->get('log_level') );
    $self->{_maxLoad}   = Common::Util::returnFirstDefined( $args{maxLoad},   $config->get('max_load') );
    $self->{_highSpeed} = Common::Util::returnFirstDefined( $args{highSpeed}, $config->get('high_speed') );

    $self->{_hostname}  = $args{hostname};
    $self->{_ruleset}   = Job::RuleList->new( hostname => $args{hostname} );
    $self->{_skiprules} = Job::SkipRuleList->new();
    $self->{_runstatus} = Job::RunStatus->new();

    return $self;
}

# This implementation seems to work ok for all platforms
# - at least, it doesn't blow up, although it might not
# actually return anything on a Windows box.
#
sub _fetchLoadAverage {
    my ($self) = @_;

    my ( $currentLoad, $fiveMinLoad, $fifteenMinLoad );

    # If we don't _care_ about load, don't bother fetching it.
    if ( $self->{_maxLoad} ) {
        ( $currentLoad, $fiveMinLoad, $fifteenMinLoad ) = Sys::CpuLoad::load();
        $self->report( "   CURRENT LOAD: $currentLoad", 3 ) if ($currentLoad);
    }

    return ( $currentLoad, $fiveMinLoad, $fifteenMinLoad );
}

sub _generateNewLogPath {
    my ( $self, $jobID ) = @_;

    die "CONFIG ERROR - no base path specified!" unless $self->{_logBasePath};

    my $basePath = $self->{_logBasePath} . '/';

    # !!! We may end up with a lot of logs.
    # !!! So, we might need to implement some sort of directory hashing scheme
    # !!! so we don't 'fill up' a directory.
    #
    my $uniquePart = $jobID . time();
    return $basePath . $uniquePart;
}

sub _parseConfig {
    my ( $self, $configFilePath ) = @_;

    #    $configFilePath = $self->_defaultConfigFilePath() unless $configFilePath;

    my $configHash = {};

    my $configObj = Job::Config->new();

    return $configObj;
}

sub wrapperPath { die "must be overloaded"; }

1;
