#------------------------------------------------------------
# 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::DB::Item::ClientOptions;

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 _checkForFeatureFlags {
    my $self = shift;
    # override to enable feature flags
    return 0;
}

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

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

    if ( my $err = $self->_checkForFeatureFlags() ) {
        my $dbItem = $self->_getRunDBItem();
        $dbItem->error($err);
        $dbItem->save();
        $self->_setStateToError();
        return 0;
    }

    $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();
    }

    if ( $self->{_RSD7859} ) {
        # Clear license caches that were used during the run
        $self->_clearLicenseCache();
    }

    # 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;
