#------------------------------------------------------------
# Copyright (C) 2009 RoyaltyShare, Inc.   All Rights Reserved
# $Id$
#------------------------------------------------------------

# Mechanical runs, no matter what country, have many elements in common.
# So we have this base class that serves to abstract away that stuff.
#
package RPS::Mechanical::Process::RunController;
use strict;
use Data::Dumper;
use File::Path;

use lib '/app/tools/common/lib';
use lib '/app/tools/rps/lib';
use lib '/app/tools/job/lib';
use Common::Assert;
use Common::RSApp;
use Common::Log;
use Common::DB::Item;

use RPS::Statement::Status;
use Job::Status;
use RPS::RoyaltyRun::Status;
use RPS::Mechanical::Process::Exception;

use base 'RPS::Mechanical::Process';

use constant kSleepTime => 6;


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

    # Logging is provided by the base class, among other things.
    #
    $self->SUPER::_init(%args);


    # Gotta have a run id, or we're wasting our time.
    #
    my $runID = $args{runID};
    assert($runID, "ERROR - runID is _required_");

    $self->{_runID} = $runID;


    # Get the run item, so we can grab the payorID for future reference.
    #
    my $run = $self->_getRunDBItem();
    $self->{_payorID} = $run->payor_id;

    return $self;
}


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

    # !!! Each subclass will want to override this method.
    # !!! I would suggest we do _NOT_ want to 'cache' the
    # !!! db item record - Let's just fetch it from the database
    # !!! every time.

    die "ERROR - You MUST override _getRunDBItem";
}


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


    # !!! Let's keep the pre-flight sanity checking in place.
    #
    $self->_preCommitSanityCheck();


    # When we commit a run, we will always want to:
    # - Handle retentions
    # - Clean up any retentions.
    # - Loop through the statements, and 'commit' each of those.
    # - Flag this run as processed.
    # - Re-build the downloadable statements.


    # ? For UK mechanicals, retentions and carryover can be dealt with at a 'license' level.
    #   We have a mechanism to fetch all the licenses associated with the run.
    #   Do we have anything analogous for US mechanicals?
    # !!! Let's do this in a slightly different way.
    #
    $self->_commitCarryover();
    $self->_commitReserves();

    my $allStatementItems = $self->_getAllStatements();
    while (my $statement = $allStatementItems->next())
    {
        $self->_commitStatement($statement);
    }

    # mark run as committed
    #
    $self->_setStateToCommitted();


    # Mark previously committed run as closed.
    #
    my $otherCommittedRuns = $self->_getAllOtherCommittedRuns();
    if ($otherCommittedRuns)
    {
        while (my $otherRun = $otherCommittedRuns->next())
        {
            $otherRun->status(RPS::RoyaltyRun::Status::kClosed);
            $otherRun->save();
        }
    }

    # Now re-create the pdfs and text statement files.
    # We want to do this after we set the run's state to committed, because
    # that will ensure these files have the 'final' appearance (i.e. no watermark).
    #
    $self->_createStatementFiles();

    return 0;
}


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

    $self->_createPDFJobs();
    $self->_createTextJobs();
    $self->_createRetentionReport();
}

########



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

    # Get all of the statements created by this run,
    # and delete them.
    #
    my $statements = $self->_getAllStatements();

    while ($statements->hasNext())
    {
        my $statement = $statements->next();
        $self->_deleteStatement($statement);
    }


    # Delete from the SaleRunMap table
    #
    $self->_report("delete sale_run_map");
    $self->_deleteFromSaleRunMap();




    # Remove the log files
    #
    my $logBasePath = $self->_getLogFilePath();
    if (-d $logBasePath)
    {
        rmtree($logBasePath);
    }

    # Delete this run from the appropriate table.
    #
    $self->_report("deleting from run table");
    my $run = $self->_getRunDBItem();
    $run->delete();


    return 0;
}




sub _deleteStatement
{
    my ($self, $statement) = @_;
}




#########


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

    return unless( $self->SUPER::run() );

    $self->_setStateToRunning();


    # Create Statement database records, and
    # queue up CreateStatement jobs.
    #
    $self->_createStatementsAndJobs();


    # Now we wait.
    # Our 'child' processes won't necessarily be running on this host,
    # so we can't use any fancy waitpid sort of interface.
    # We're forced to poll, and wait until everybody is finished.
    #
    # If there is an error in one of our child processes, we can attempt
    # to retry it.  Getting those semantics correct will be interesting.
    #
    my @statementIDs;
    my $badCount = 0;
    my $numJobsRunning = scalar (keys %{$self->{_jobs}});
    while ($numJobsRunning)
    {
        sleep(kSleepTime);

        my $newCount = 0;

        # Iterate through the jobs, and see if they are still running.
        #
        foreach my $jobKey (keys %{$self->{_jobs}})
        {
            my $jobRecord = $self->{_jobs}{$jobKey};
            if (! $jobRecord->{complete})
            {
                my $job = $jobRecord->{job};
                my $jobStatus = $job->getStatus();

                if (Job::Status::kComplete eq $jobStatus
                 || Job::Status::kDead eq $jobStatus)
                {
                    Common::Log::Print( sprintf( "Job state: %s, Statement ID: %s, PID: %s, Job ID: %s", $jobStatus, $job->{CommandLineArguments}->{statementID}, $job->{PID}, $job->{JobID} ) );

                    # So what _really_ happened?
                    # !!! Starting to wonder if perhaps we should 'share' status codes?
                    #
                    my $statementID = $job->getCommandLineArg('statementID');
                    my $statement = $self->_getStatement($statementID);

                    if (RPS::Statement::Status::kComplete == $statement->status)
                    {
                        $jobRecord->{complete} = 1;

                        # Store the statement id so that we can queue up the pdf job later.
                        # For now, we'll assume all statements are MCPS.
                        push(@statementIDs, $statementID);
                    }
                    else
                    {
                        # !!! Do we want to try and re-queue?  Perhaps someday, but for now we just fail.
                        #
                        Common::Log::Print("job for statement $statementID did not complete successfully");
                        $jobRecord->{complete} = 1;
                        $jobRecord->{error} = $statement->status;
                        $badCount++;
                    }
                }
                else
                {
                    # It's still waiting/running/etc.
                    #
                    $newCount++;
                }
            }
        }

        $numJobsRunning = $newCount;
    }


    $self->_report("All jobs reported - bad count = $badCount");

    # Update the run table with our final status, and clean up.
    #
    if ($badCount > 0)
    {
        $self->_setStateToError();
    }
    else
    {
        $self->_setStateToComplete();

        # Create the PDF and Text statement jobs.
        #
        $self->_createStatementFiles();
    }

    # Return '0' to signify a normal exit
    #
    return 0;
}


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

    # Check the run's state - It should be in 'waitingToRun'.
    # Update the record to indicate we're taking over.
    #
    my $dbItem = $self->_getRunDBItem();

    die RPS::Mechanical::Process::Exception->new("runID " . $self->{_runID} . " is not in the 'waiting' state")
     unless RPS::RoyaltyRun::Status::kWaitingToRun == $dbItem->status();

    $dbItem->start_time(Common::DB::Item::kDateTimeNow);
    $dbItem->pid($$);
    $dbItem->hostname(Common::RSApp::GetHostname());
    $dbItem->status(RPS::RoyaltyRun::Status::kRunning);

    $dbItem->save();
}


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

    $self->_report("setting state to error");

    my $dbItem = $self->_getRunDBItem();

    $dbItem->end_time(Common::DB::Item::kDateTimeNow);
    $dbItem->status(RPS::RoyaltyRun::Status::kError);

    $dbItem->save();
}


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

    $self->_report("setting state to complete");
    my $dbItem = $self->_getRunDBItem();

    $dbItem->end_time(Common::DB::Item::kDateTimeNow);
    $dbItem->status(RPS::RoyaltyRun::Status::kComplete);

    $dbItem->save();
}


sub _setStateToCommitted
{
    my ($self) = @_;
    $self->_report("setting state to committed");

    my $dbItem = $self->_getRunDBItem();

    $dbItem->status(RPS::RoyaltyRun::Status::kCommitted);
    $dbItem->save();
}


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

    # The default behavior will be to create a job for each active publisher.
    # !!! So, for UK Mechanicals (currently), we just won't do anything here.
    #
    my @activePublishers = $self->_getAllActivePublisherIDs();

    $self->_report("_createStatementsAndJobs: Number of publisher = " . scalar @activePublishers, 4);

    foreach my $publisherID (@activePublishers)
    {
        $self->_addJobToJobList($publisherID, $self->_createDirectPublisherStatementJob($publisherID));
    }
}


sub _createDirectPublisherStatementJob
{
    my ($self, $publisherID) = @_;

    die "ERROR - you must override _createDirectPublisherStatementJob";
}

sub _addJobToJobList
{
    my ($self, $key, $job) = @_;
    assert($job);

    my $jobID = $job->id;
    die RPS::Mechanical::Process::Exception->new(" - ERROR - no job id!") unless $jobID;
    die RPS::Mechanical::Process::Exception->new(" - ERROR - We already have an entry for job $key") if $self->{_jobs}{$key};

    # So, we're going to use a hash to keep track of our Job objects.
    # Each record in this hash will be a hash as well, so we have some room to
    # jot down any additional state we may want to track.
    # !!! At some point, perhaps, we might use the database for this.
    # !!! I don't really know what we might want to track yet...
    #
    my %jobRecord;
    $jobRecord{job} = $job;

    $self->{_jobs}{$key} = \%jobRecord;
}


# Override this to return all other committed royalty runs.
# (committed, _NOT_ closed)
#
sub _getAllOtherCommittedRuns
{
    my ($self) = @_;

    assert(0, 'override this');
}


sub _onFailure {
    my $self = shift;
    my %args = @_;
    
    my $status = $args{interupt} ? RPS::RoyaltyRun::Status::kAborted : 
                                   RPS::RoyaltyRun::Status::kError;
    
    my $dbItem = $self->_getRunDBItem();

    if ($args{exception})
    {
        my $e = $args{exception};
        my $errorString;
        if (ref $e && $e->isa('Common::Exception'))
        {
            $errorString = $e->errorMessage();
        }
        else
        {
            $errorString = $e;
        }
        $dbItem->error($errorString);
    }
    
    $dbItem->status($status);
    $dbItem->end_time(Common::DB::Item::kDateTimeNow);
    $dbItem->save();
    
    $self->SUPER::_onFailure( %args );
}

# Check to ensure our status is "waiting"
sub canExecute {
    my $self = shift;
    my $dbItem = $self->_getRunDBItem();
     
    if( RPS::RoyaltyRun::Status::kWaitingToRun == $dbItem->status() ) {
	return 1;
    } else {
        print STDERR "ERROR - runID " . $self->{_runID} . " is not in the 'waiting' state\n";
	return;
    }
}

1;
