#------------------------------------------------------------
# Copyright (C) 2009 RoyaltyShare, Inc.   All Rights Reserved
#------------------------------------------------------------
package RPS::Mechanical::US::Process::RunController;
use strict;

use Data::Dumper;
use File::Path;

use lib '/app/tools/common/lib';
use Common::Assert;
use Common::RSApp;
use Common::Log;
use Common::Timer;

use lib '/app/tools/rps/lib';
use lib '/app/tools/job/lib';
use lib '/app/tools/raptor/lib';

use Raptor::DB::Item::Sale;


use RPS::DB::Item::LicenseReserve;
use RPS::DB::Item::LicenseReserveRun;
use RPS::DB::Item::MechanicalCarryover;
use RPS::DB::Item::MechanicalRun;
use RPS::DB::Item::MechanicalRunCarryover;
use RPS::DB::Item::MechanicalRunTrackLicense;
use RPS::DB::Item::MechanicalStatement;
use RPS::DB::Item::MechanicalStatementItem;
use RPS::DB::Item::MechanicalStatementAdjustmentItem;
use RPS::DB::Item::MechanicalStatementTransaction;
use RPS::DB::Item::MechanicalStatementLicenseTransaction;
use RPS::DB::Item::MechanicalRunTrackLicense;
use RPS::DB::Item::Product;
use RPS::DB::Item::PendingTransaction;
use RPS::DB::Item::ProductTrack;
use RPS::DB::Item::Publisher;
use RPS::DB::Item::SaleLicenseMap;
use RPS::DB::Item::SalePublisherLicenseMap;
use RPS::DB::Item::SalePublisherMap;
use RPS::DB::Item::SaleRunMap;
use RPS::DB::Item::StatRate;
use RPS::DB::Item::TrackLicense;
use RPS::DB::Item::PublisherAccount;

use RPS::Statement::Status;
use RPS::Statement::Mechanical::US::PDF;
use RPS::Statement::Mechanical::US::Text;
use Job::Status;
use RPS::Mechanical::US::Process::CreateStatement;
use RPS::Mechanical::US::Job::RunMechanicalRoyalties;
use RPS::Mechanical::US::Job::CreateStatement;
use RPS::Mechanical::US::Job::CreateTextStatements;
use RPS::Mechanical::US::Job::CreatePDFStatements;
use RPS::Mechanical::Process::RunController::Complex;
use RPS::Mechanical::US::Job::CreateReservePipelineReport;
use RPS::DB::Item::ReportQueries::MechanicalReservePipeline;
use RPS::Mechanical::US::Job::CreateRoyaltyExportReport;
use RPS::Mechanical::US::Job::CreateRoyaltyExportReportExcel;

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

use constant kStatementVersion => 4;

use constant kBufferSize       => 10000; # used for bulk SQL inserts

sub _runType {
    return RPS::DB::Item::SaleRunMap::kRunTypeMechanical;
}

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

    # Call the inherited method
    #
    my $rval = $self->SUPER::run();

    # We don't want the data in the SalePublisherMap table to persist.
    # It takes up a lot of space...
    #
    RPS::DB::Item::SalePublisherMap->DeleteAllForRun( $self->{_runID} );

    # Create the unlicensed accrual report
    #
    $self->_createUnlicensedAccrualReport();

    # Create the reserve pipeline report
    #
    $self->_createReservePipelineReport();

    # Create the royalty data export report
    #
    $self->_createRoyaltyDataExportReport();

    return $rval;
}

sub _trackIsMechanicalExempt {
    my ( $self, $track ) = @_;
    return $track->mechanical_exempt();
}

sub _publisherIDFromTrackLicense {
    my ( $self, $tl ) = @_;
    return $tl->publisher_id;
}

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

    my $allSales = Raptor::DB::Item::Sale->GetUnprocessedMechanicalSales( ending_sale_date => $endDate );
    return $allSales;
}

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

    my $allPublishers = RPS::DB::Item::Publisher->GetAll();
    return $allPublishers;
}

sub _publisherIDFromPublisher {
    my ( $self, $publisher ) = @_;

    return $publisher->publisher_id();
}

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

    my $allItems = RPS::DB::Item::MechanicalRunTrackLicense->GetByRunIDAndTypeAndCountryCode( $self->{_runID}, $self->_runType, 'US' );
    return $allItems;
}

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

    my $allItems = RPS::DB::Item::MechanicalRunTrackLicense->DeleteByRunIDAndType( $self->{_runID}, $self->_runType );
    return $allItems;
}

sub _getReserveErrors {
    my $self = shift;

    return RPS::DB::Item::TrackLicense->GetAllWithoutReserveLiquidation();
}

sub _getPennyRateErrors {
    my $self = shift;

    return RPS::DB::Item::TrackLicense->GetAllWithInvalidPennyRate();
}

sub _getLicenseShareErrors {
    my $self = shift;

    return RPS::DB::Item::TrackLicense->GetAllWithZeroShare();
}

sub _getAllTrackLicenses {
    my $self = shift;

    return RPS::DB::Item::TrackLicense->GetAll();
}

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

    my %idHash;
    my $payorID    = $self->{_payorID};
    # with a license
    my $collection = RPS::DB::Item::Publisher->GetAllActiveOrOnHoldWithLicensesForPayor($payorID);
    while ( my $publisher = $collection->next() ) {

        # Add this publisher to the hash
        #
        $idHash{ $publisher->publisher_id } = 1;

        # Also add this guys admin or agent id..
        # agent trumps admin
        #
        if ( $publisher->agent_id ) {
            $idHash{ $publisher->agent_id } = 1;
        } elsif ( $publisher->admin_id ) {
            $idHash{ $publisher->admin_id } = 1;
        }

    }

    # publishers with pending transactions including publishers without licenses
    $collection = RPS::DB::Item::PublisherAccount->GetAllActiveOrOnHoldPublisherByPayorID($payorID);
    while ( my $publisher = $collection->next() ) {
        $idHash{ $publisher->publisher_id } = 1;
    }

    # publishers with positive balance
    $collection = RPS::DB::Item::Publisher->GetAllActiveOrOnHoldWithBalance($payorID);
    while ( my $publisher = $collection->next() ) {
        $idHash{ $publisher->publisher_id } = 1;
    }

    return keys %idHash;
}

sub _getRunDBItem {
    my ($self) = @_;
    assert( $self->{_runID} );

    my $dbItem = RPS::DB::Item::MechanicalRun->Lookup( mechanical_run_id => $self->{_runID} );
    return $dbItem;
}

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

    $self->_testPendingTransactionsAreValid();

    # See if all the reserves are present and accounted for...
    #
    if ( RPS::DB::Item::LicenseReserveRun->CountMissingRunReserves( $self->{_runID} ) > 0 ) {
        die RPS::Mechanical::Process::Exception->new("ERROR - Not all run reserves are accounted for");
    }

    return 1;
}

sub _logPublicDomainShare {
    my ( $self, $sale, $trackID, $trackLicense, $share ) = @_;

    return unless ( 'US' eq $sale->country_code );
    $self->SUPER::_logPublicDomainShare( $sale, $trackID, $trackLicense, $share );
}

sub _nativeCountryCode {
    my ($self) = @_;
    return 'US';
}

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

    my $runID = $self->{_runID};

    # Make sure nobody deleted the pending transactions that appear on this run!
    #
    my $statements = RPS::DB::Item::MechanicalStatement->GetByMechanicalRunID($runID);
    while ( my $statement = $statements->next() ) {
        my $statementID         = $statement->mechanical_statement_id;
        my $statementTransItems = RPS::DB::Item::MechanicalStatementTransaction->GetByMechanicalStatementID($statementID);
        my $transItem;
        while ( $transItem = $statementTransItems->next() ) {
            my $pendingTransactionID = $transItem->pending_transaction_id;
            my $pending = RPS::DB::Item::PendingTransaction->Lookup( pending_transaction_id => $pendingTransactionID );
            if ( !$pending ) {
                die RPS::Mechanical::Process::Exception->new( "original pending transaction was deleted! : ", $transItem );
            }
        }

        my $licenseTransItems = RPS::DB::Item::MechanicalStatementLicenseTransaction->GetByMechanicalStatementID($statementID);
        while ( $transItem = $licenseTransItems->next() ) {
            my $pendingTransactionID = $transItem->pending_transaction_id;
            my $pending = RPS::DB::Item::PendingTransaction->Lookup( pending_transaction_id => $pendingTransactionID );
            if ( !$pending ) {
                die RPS::Mechanical::Process::Exception->new( "original pending transaction was deleted! : ", $transItem );
            }
        }
    }

    return 1;
}

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

    # What we're really doing here is clearing out the _old_ carryovers.
    # Each 'statement' will then create entries for the new carryover (later).
    #

    # Truncate the carryover table.
    #
    my $runCarryoverIDs = RPS::DB::Item::MechanicalRunCarryover->GetByMechanicalRunID( $self->{_runID} );
    while ( my $carryoverMapItem = $runCarryoverIDs->next() ) {
        my $carryoverID = $carryoverMapItem->mechanical_carryover_id;
        my $carryover = RPS::DB::Item::MechanicalCarryover->Lookup( mechanical_carryover_id => $carryoverID );
        $carryover->delete();
    }

    # Get rid of entries in the carryover run map
    #
    # !!! KEEP FOR DEBUGGING
    #    RPS::DB::Item::MechanicalRunCarryover->DeleteByMechanicalRunID($self->{_runID});
}

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

    my $runID = $self->{_runID};

    # 'turn the crank' on the reserves table.
    #
    my $runReserves = RPS::DB::Item::LicenseReserveRun->GetByMechanicalRunID($runID);
    while ( my $runReserveItem = $runReserves->next() ) {
        my $reserve = RPS::DB::Item::LicenseReserve->Lookup( license_reserve_id => $runReserveItem->license_reserve_id );

        if ( !$reserve ) {

            # This should never happen!
            #
            $self->_report( "ERROR!  reserve id " . $runReserveItem->license_reserve_id . " is MISSING" );
            next;
        }
        my $period = $reserve->periods_remaining();

        if ( 1 == $period ) {

            # This reserve has been liquidated;
            # So we'll make note of the run id.
            #
            $reserve->liquidated_run_id($runID);
        }

        # Just decrement the periods_remaining column.
        # Liquidated reserves will end up with '0' periods_remaining.
        #
        $reserve->periods_remaining( $period - 1 );
        $reserve->save();
    }

}

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

    # Instantiate the proper CreatePublisherStatement entity, and let that handle the details.
    #
    my $statementProcessor = RPS::Mechanical::US::Process::CreateStatement->new( statementID => $statement->mechanical_statement_id );
    $statementProcessor->commit();
}

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

    my $statements = RPS::DB::Item::MechanicalStatement->GetByMechanicalRunID( $self->{_runID} );
    return $statements;
}

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

    RPS::DB::Item::SaleRunMap->DeleteByMechanicalRunID( $self->{_runID} );
}

sub _createSalePublisherMapEntry {
    my ( $self, %args ) = @_;
    assert( $args{run_id} );
    assert( $args{sale_id} );
    assert( $args{publisher_id} );

    my $newMapItem = RPS::DB::Item::SalePublisherMap->Create(
        run_id       => $args{run_id},
        sale_id      => $args{sale_id},
        publisher_id => $args{publisher_id},
    );

    $newMapItem->save();

}

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

    my $logBasePath = RPS::Mechanical::US::Job::RunMechanicalRoyalties->LogFilePath( Common::RSApp::GetClientID(), $self->{_runID} );
    return $logBasePath;
}

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

    RPS::DB::Item::LicenseReserveRun->DeleteByMechanicalRunID( $self->{_runID} );
    RPS::DB::Item::MechanicalRunCarryover->DeleteByMechanicalRunID( $self->{_runID} );

    $self->SUPER::delete();

    # MAKE SURE this is emptied out.
    # We clear this table when a run exits normally, but if something went haywire we might need to try again.
    #
    RPS::DB::Item::SalePublisherMap->DeleteAllForRun( $self->{_runID} );

    # Clear out any associated pipeline report
    #
    RPS::DB::Item::ReportQueries::MechanicalReservePipeline->DeleteByMechanicalRunID( $self->{_runID} );

    return 0;
}

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

    my $statementID = $statement->mechanical_statement_id;

    $self->_report("deleting statement $statementID");
    $statement->delete();

    $self->_report( "  ... deleting reserve items", 2 );
    RPS::DB::Item::LicenseReserve->DeleteByStatementID($statementID);

    $self->_report( "  ... deleting track items", 2 );
    RPS::DB::Item::MechanicalStatementTrack->DeleteByMechanicalStatementID($statementID);

    $self->_report( "  ... deleting license items", 2 );
    RPS::DB::Item::MechanicalStatementLicense->DeleteByMechanicalStatementID($statementID);

    $self->_report( "  ... deleting adjustment items", 2 );
    RPS::DB::Item::MechanicalStatementAdjustmentItem->DeleteByMechanicalStatementID($statementID);

    $self->_report( "  ... deleting statement items", 2 );
    RPS::DB::Item::MechanicalStatementItem->DeleteByMechanicalStatementID($statementID);

    return 0;
}

sub _checkForFeatureFlags {
    my $self = shift;
    my $err  = 0;  # default to no error

    if ( RPS::DB::Item::ClientOptions->Get( 'RSD7859' ) ) {  # Feature flag (RSD-6851)
        my $dbo = Common::RSApp::GetClientDB();
        # Check if the required tables are present
        my @tables = (
            'sale_license_map',
            'sale_publisher_license_map',
        );

        foreach my $table (@tables) {
            print "-- checking if $table exists ...\n";
            my $sql = "SHOW TABLES LIKE ". $dbo->DBQuote($table);
            my $sth = $dbo->DoCmd($sql);
            if ( $sth->rows == 0 ) {
                $self->_report("ERROR: Table '$table' NOT FOUND", 2 );
                $err = 1;
            }
        }
        if ( $err ) {
            $err = "ERROR: Required tables not present; unable to enable RSD7859 feature";
            $self->_report( $err, 2 );
        } else {
            $self->_report("### RSD7859 feature enabled");
            $self->{_RSD7859} = 1;
        }
    }

    if ( RPS::DB::Item::ClientOptions->Get( 'RSD7859_VERIFY' ) ) {  # Feature flag (RSD-6851)
        if ( !$self->{_RSD7859} ) {
            $err = "ERROR: RSD7859_VERIFY feature requires RSD7859";
            $self->_report( $err, 2 );
        } else {
            $self->_report("### RSD7859_VERIFY feature enabled");
            $self->{_RSD7859_VERIFY} = 1;
        }
    }
    return $err;
}

sub _createSalePublisherLicenseEntry {
    my ( $self, $saleID, $publisherID, $trackLicenses ) = @_;

    my $licenseList = join(',', @$trackLicenses);

    my $newMapItem = RPS::DB::Item::SalePublisherLicenseMap->Create(
        run_id       => $self->{_runID},
        publisher_id => $publisherID,
        sale_id      => $saleID,
        licenses     => $licenseList,
    );
    $newMapItem->save();
}


sub _initializeSalePublisherLicenseMap {
    my ( $self ) = shift;

    $self->_report( " >> Building sale_publisher_license...", 2 );

    if ( !exists $self->{_publisherMatchingLicenses} || !keys %{$self->{_publisherMatchingLicenses}} ) {
        $self->_report( " >> No publishers found .. skipping", 2 );
        return;
    }

    my $tableName = 'sale_publisher_license_map';

    # Write the data in $self->{_publisherMatchingLicenses} to a tab-delimited file.
    # We'll then use mysql_import to load the table data.  The path naming is structured
    # similar to artist royalties.
    #
    my $clientID       = Common::RSApp::GetClientID();
    my $staticDataPath = "/app/data/us_mechanical_run/" . Common::RSApp::GetClientID() . "/" . $self->{_runID};
    my $outputDataPath = $staticDataPath . "/output";  # to be consistent with artist run path naming
    if ( !-d $outputDataPath ) {
        mkpath($outputDataPath) or die("Unable to create path $outputDataPath $!");
    }

    my $filename = $outputDataPath . '/' . $tableName;
    my $t3 = Time::HiRes::gettimeofday();
    open DATAFILE, "> $filename" or die "ERROR: Unable to open $filename for writing: $!";
    foreach my $publisherID ( keys %{$self->{_publisherMatchingLicenses}} ) {
        foreach my $saleID ( keys %{$self->{_publisherMatchingLicenses}{$publisherID}} ) {
            my $trackLicenses = $self->{_publisherMatchingLicenses}{$publisherID}{$saleID};
            my $licenseList   = join(',', @$trackLicenses);

            print DATAFILE join("\t", $self->{_runID}, $saleID, $publisherID, $licenseList ) . "\n";

        }
    }
    my $t4 = Time::HiRes::gettimeofday();
    $self->_report( " >> Flushed _publisherMatchingLicenses to $filename in ". ($t4 - $t3) . " seconds", 2 );

    my $fields ="run_id,sale_id,publisher_id,licenses";
    my @command = (
        'mysqlimport',                                               '--local',
        '--fields-optionally-enclosed-by=\"',                        "--columns=$fields",
        "--user=" . $Common::RSDB::CLIENT_DB{$clientID}->{username}, "--password=" . $Common::RSDB::CLIENT_DB{$clientID}->{password},
        "--host=" . $Common::RSDB::CLIENT_DB{$clientID}->{server},   $Common::RSDB::CLIENT_DB{$clientID}->{db_name},
        $filename,
    );
    $self->_report("COMMAND: " . join( ' ', @command ), 2 );

    # Catch the bloody error code!
    #
    my $status = system(@command);
    die "ERROR EXECUTING " . join( ' ', @command ) . " : ($?) $!" unless 0 == $status;

}

sub _clearLicenseCache {
    my ( $self ) = shift;
    RPS::DB::Item::SaleLicenseMap->DeleteAllForRun( $self->{_runID} );

    RPS::DB::Item::SalePublisherLicenseMap->DeleteAllForRun( $self->{_runID} );
}

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

    $self->_report( "_createSaleLicenseMapping", 2 );

    my $dbo = Common::RSApp::GetClientDB();

    my $t0 = Time::HiRes::gettimeofday();

    my %itemLicenseMap; # licenseID for statement items on closed/committed runs
    my $sql =
          "SELECT mechanical_statement_item_id, track_license_id "
        . "FROM mechanical_statement_item msi "
        . "WHERE mechanical_statement_id IN ( "
        . "  SELECT mechanical_statement_id FROM mechanical_statement WHERE mechanical_run_id IN "
        . "     ( "
        . "       SELECT mechanical_run_id FROM mechanical_run WHERE status IN (2,5) "
        . "     )"
        . "  ) "
        . "AND track_license_id IN ( SELECT track_license_id FROM track_license WHERE payor_id=". $self->{_payorID} . ")"
    ;
    my $sth = $dbo->DoCmd($sql);
    while( my($itemID, $licenseID) = $sth->fetchrow_array() ) {
        $itemLicenseMap{$itemID} = $licenseID;
    }

    my $t1 = Time::HiRes::gettimeofday();

    $self->_report(" >> Built itemLicenseMap in ". ($t1 - $t0) . " second(s)", 2 );

    # Grab all sales and their applicable statement items from all closed/committed runs.
    $sql =
          "SELECT sale_id, statement_item_id FROM sale_run_map "
        . "WHERE run_type='MECH' AND status='paid' "
        . "AND statement_item_id IN ( "
        . "  SELECT msi.mechanical_statement_item_id "
        . "  FROM mechanical_statement_item msi "
        . "  WHERE msi.mechanical_statement_id IN ( "
        . "    SELECT mechanical_statement_id FROM mechanical_statement WHERE mechanical_run_id IN ( "
        . "      SELECT mechanical_run_id FROM mechanical_run WHERE status IN (2,5) "
        . "    ) "
        . "  ) "
        . ") "
    ;

    $self->_report( " >> Building paidSaleMap...", 2 );
    my $t2 = Time::HiRes::gettimeofday();

    my %psMap;

    $sth = $dbo->DoCmd($sql);
    while( my($saleID, $itemID) = $sth->fetchrow_array() ) {
        if ( exists $itemLicenseMap{$itemID} ) {
            push @{$psMap{$saleID}}, $itemLicenseMap{$itemID};
        }
    }
    my $t3 = Time::HiRes::gettimeofday();

    my $numUnique  = keys %psMap;
    my $totalSales = $sth->rows;

    $self->_report( " >> Built paidSaleMap in ". ($t3 - $t2) . " second(s)", 2 );
    $self->_report( " >> Found $numUnique unique sales out of $totalSales", 2 );


    my $t4 = Time::HiRes::gettimeofday();
    my @obuf;
    my $baseSql = "INSERT INTO sale_license_map(run_id, sale_id, licenses) VALUES";

    foreach my $saleID ( keys %psMap ) {
        my $licenses = join(',', @{$psMap{$saleID}});

        push @obuf, '(' . $self->{_runID} . ',' . $saleID . ',' . $dbo->DBQuote($licenses) . ')';

        if ( scalar @obuf > kBufferSize ) {
            my $rows = join(',',@obuf);
            my $sql = $baseSql . $rows;
            my $sth = $dbo->DoCmd($sql);
            undef @obuf;
        }
    }
    if ( scalar @obuf > 0 ) {  # handle any left overs
        my $rows = join(',',@obuf);
        my $sql = $baseSql . $rows;
        my $sth = $dbo->DoCmd($sql);
        undef @obuf;
    }
    my $t5 = Time::HiRes::gettimeofday();
    $self->_report( " >> Flushed psMap to sale_license_map in ". ($t5 - $t4) . " seconds", 2 );
}

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

    # Before we kick off the individual publisher jobs, we
    # need to 'pre-scan' the sale table.
    # For huge customers, I don't want to have every job looking at every sale.
    # So we will do a quick (15 minutes or so for our biggest clients) sorting process.
    #
    $self->_createSalePublisherMapping();

    $self->_createSaleLicenseMapping() if ( $self->{_RSD7859} );

    $self->SUPER::_createStatementsAndJobs();
}

sub _getStatement {
    my ( $self, $statementID ) = @_;

    my $statement = RPS::DB::Item::MechanicalStatement->Lookup( mechanical_statement_id => $statementID );
    return $statement;
}

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

    # First, create an 'empty' statement database record.
    #
    my $statementObj = RPS::DB::Item::MechanicalStatement->Create(
        mechanical_run_id => $self->{_runID},
        payor_id          => $self->{_payorID},
        publisher_id      => $publisherID,
        status            => RPS::Statement::Status::kNotQueued(),
        version           => kStatementVersion,
    );
    $statementObj->save();

    my $statementID = $statementObj->mechanical_statement_id();

    # Create the Job object
    #
    my $job = RPS::Mechanical::US::Job::CreateStatement->new(
        runID       => $self->{_runID},
        statementID => $statementID,
        logLevel    => $self->{_logLevel}
    );
    $job->enqueue();

    # Update the statement record with the job id, and
    # update the status to 'in queue'
    #
    $statementObj->job_id( $job->id() );
    $statementObj->status( RPS::Statement::Status::kInQueue() );
    $statementObj->save();

    return $job;
}

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

    # We'll create a seperate job for each statement.

    # Make sure the path exists
    #
    my $filePath = RPS::Statement::Mechanical::US::PDF->StatementPathFromRunID( $self->{_runID} );
    $self->_report( "_createPDFJobs - filePath=$filePath", 2 );
    if ( !-d $filePath ) {
        mkpath($filePath) or die RPS::Mechanical::Process::Exception->new("Unable to create path $filePath: $!");
    }

    # Create a job for all statements, and put it in the queue.
    #
    my $clientID = Common::RSApp::GetClientID();

    my $jobArgs = RPS::Mechanical::US::Job::CreatePDFStatements->new(
        runID    => $self->{_runID},
        clientID => $clientID,
        filePath => $filePath,
        mode     => 'b',
    );
    my $job = $jobArgs->enqueue();

    #my $allStatements = RPS::DB::Item::MechanicalStatement->GetByMechanicalRunID($self->{_runID});
    #while (my $statement = $allStatements->next())
    #{
    #    my $id = $statement->mechanical_statement_id();
    #    my $fileName = RPS::Statement::Mechanical::US::PDF->StatementFileNameFromDBItem($statement);
    #
    #    my $jobArgs = RPS::Mechanical::US::Job::CreatePDFStatement->new
    #    (
    #        statementID => $id,
    #        clientID 	=> $clientID,
    #        filePath 	=> $filePath.$fileName,
    #    );
    #    my $job = $jobArgs->enqueue();
    #}
}

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

    # This report will be stored in the same directory as the mechanical statements.
    #
    my $filePath = RPS::Statement::Mechanical::US::Text->StatementPathFromRunID( $self->{_runID} );
    if ( !-d $filePath ) {
        mkpath($filePath) or die RPS::Mechanical::Process::Exception->new("ERROR: Unable to create path $filePath: $!");
    }

    my $clientID = Common::RSApp::GetClientID();

    # Queue the text report
    #
    my $jobArgs = RPS::Mechanical::US::Job::CreateRoyaltyExportReport->new(
        runID    => $self->{_runID},
        clientID => $clientID,
        filePath => $filePath,
    );
    my $job = $jobArgs->enqueue();

    # Queue the Excel report
    #
    $jobArgs = RPS::Mechanical::US::Job::CreateRoyaltyExportReportExcel->new(
        runID    => $self->{_runID},
        clientID => $clientID,
        filePath => $filePath,
    );
    $job = $jobArgs->enqueue();

    $self->_report( "_createRoyaltyDataExportReport - filePath=$filePath", 2 );
}

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

    my $filePath = RPS::Statement::Mechanical::US::Text->StatementPathFromRunID( $self->{_runID} );
    if ( !-d $filePath ) {
        mkpath($filePath) or die RPS::Mechanical::Process::Exception->new("ERROR: Unable to create path $filePath: $!");
    }

    # Create a job for all statements, and put it in the queue.
    #
    my $clientID = Common::RSApp::GetClientID();

    my $jobArgs = RPS::Mechanical::US::Job::CreateTextStatements->new(
        runID    => $self->{_runID},
        clientID => $clientID,
        filePath => $filePath,
    );
    my $job = $jobArgs->enqueue();

    #my $allStatements = RPS::DB::Item::MechanicalStatement->GetByMechanicalRunID($self->{_runID});
    #while (my $statement = $allStatements->next())
    #{
    #    my $id = $statement->mechanical_statement_id();
    #    my $fileName = RPS::Statement::Mechanical::US::Text->StatementFileNameFromDBItem($statement);
    #    my $jobArgs = RPS::Mechanical::US::Job::CreateTextStatement->new
    #    (
    #        statementID => $id,
    #        clientID 	=> $clientID,
    #        filePath 	=> $filePath.$fileName,
    #    );
    #    my $job = $jobArgs->enqueue();
    #}

}

sub _createUnlicensedTrackLogEntry {
    my (
        $self, $track, $product, $upc, $productType,
        $missingShare, $totalOwed, $units, $year, $rate
    ) = @_;

    # Skip tracks that are mechanical exempt
    #
    return if ( $track->mechanical_exempt );

    # Before we add this entry to the log, let's see if the track is covered
    # by any other licenses.
    my $regionsWithUS = $self->_findRegionsForCountryCode( 'US' );
    my $productTypeID = $product->product_type_id;

    # Need to make sure we are only checking for ringtone license for ringtone sales.
    if ($productType eq 'RING') {
        $productTypeID = RPS::DB::Item::Product::kProductTypeRingtone;
    }

    # I guess we'll just ignore the $missingShare that is passed in, since it
    # doesn't factor in expired licenses.
    my $allPayorsShare = RPS::DB::Item::TrackLicense->GetAllPayorsShare($track->track_id, $productTypeID, $regionsWithUS);

    # If the missing share is covered by other payors,
    # do not include this in the report.
    return if ($allPayorsShare >= 100);

    $missingShare = 100 - $allPayorsShare;

    # Apply the missing share percentage to the total owed.
    my $accrual  = Common::RSMath::round( $totalOwed * ( $missingShare / 100 ), 4 );

    my $album = $self->_getAlbum( $track->album_id );

    # Write the data to a tab-delimited file.
    # We'll then use mysql_import to load the table data at the end.
    my $staticDataPath = "/app/data/us_mechanical_run/" . Common::RSApp::GetClientID() . "/" . $self->{_runID};
    my $outputDataPath = $staticDataPath . "/output";
    if ( !-d $outputDataPath ) {
        mkpath($outputDataPath) or die("Unable to create path $outputDataPath $!");
    }

    my $filename = $outputDataPath . '/mechanical_run_unlicensed_track_log';
    open DATAFILE, ">> $filename" or die "ERROR: Unable to open $filename for writing: $!";
    print DATAFILE join("\t",
        $self->{_runID},
        $album->label_id,
        $track->track_id,
        $album->album_id,
        $product->product_id,
        $upc,
        $productType,
        $missingShare,
        $accrual,
        $year,
        $rate,
        $units
    ) . "\n";

    return;
}

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

    my $runID = $self->{_runID};
    return RPS::DB::Item::MechanicalRun->GetOtherCommittedRuns($runID);
}

sub _getStatRates {
    my ($self) = @_;
    return RPS::DB::Item::StatRate->GetRatesUS();
}

sub _ringtoneStatRateType {
    my ($self) = @_;
    return RPS::DB::Item::StatRate::kUSRingStatRateType;
}

sub _idFromTrackLicense {
    my ( $self, $trackLicense ) = @_;
    return $trackLicense->track_license_id();
}

sub _trackLicenseFromID {
    my ( $self, $trackLicenseID ) = @_;

    return RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );
}

sub _defaultIssueStatRateID {

    my ( $self, $licenseType ) = @_;

    if ( $licenseType == RPS::DB::Item::TrackLicense::kRingtone ) {
        return 28;
    } else {
        return 16;
    }
}

sub _findMatchingLicenses {
    my ( $self, $sale ) = @_;

    $self->_report( "_findMatchingLicenses: sale_id = " . $sale->sale_id, 4 );

    # !!! This is an important method! :)
    # The license has to be either direct to this publisher, or
    # reported through this publisher.

    # !!! Take region into account here.
    #
    my $matchingRegions = $self->_findRegionsForCountryCode( $sale->country_code );
    if ( !$matchingRegions || 0 == scalar @$matchingRegions ) {
        die RPS::Mechanical::Process::SaleException::NoRegion->new( $sale,
            "No matching region found for country code '" . $sale->country_code() . "'" );
    }

    my $productID = $sale->product_id;
    my $product   = $self->_getProduct($productID);

    if ( !$product ) {
        die RPS::Mechanical::Process::SaleException::NoProduct->new( $sale,
            ( $productID ? "Sale had invalid ProductID $productID" : 'Sale had no Product ID!' ) );
    }

    my $productTypeID = $product->product_type_id;
    my $digitalFlag   = $self->_productIsDigital($product);

    my @tracks;
    if ( RPS::DB::Item::Product::kProductTypeDigitalTrack ne $productTypeID ) {
        push @tracks, $self->_getTracksForSale($sale);
    } else {
        push @tracks, $product->asset_id();
    }

    my $publisherID = $self->_publisherID();
    my $startDate   = $sale->date_begin;
    my $endDate     = $sale->date_end;

    my $ringtoneFlag = 0;

    # JPK - Restore when we're ready for RINGBACK
    #    if ($sale->format_type eq 'R' || $sale->format_type eq '1')
    if ( $sale->format_type eq 'R' ) {
        $ringtoneFlag = 1;
    }

# Use a nice fat magical accessor - We might as well have the database do some work for us.
#
# !!! I don't see how I can filter out licenses that don't match the correct region?
# !!! Well, maybe I can - Might not be that hard if I can figure out what regions _may_ apply to this sale?
#
# I need this to only return licenses that match the product_type_id of the sale!
# - And they need to be the most 'specific' license : A 'DT' license trumps the 'All Product Type' license.
#
#
#    return $self->_getLicensesForPublisherStatementWithTracks(\@tracks, $productTypeID, $matchingRegions, $startDate, $endDate, $publisherID);

    my @trackLicenses;
    foreach my $trackID (@tracks) {

        # !!! Skip tracks that are mechanical_exempt
        next if $self->_trackIsExemptFromMechanicals($trackID);

        foreach my $regionID (@$matchingRegions) {

            # !!! Note that for a given track and region, there can be multiple matching licenses.
            # That's because this publisher may be an agent/admin : So we need to choose winners for each
            # potential publisher.
            #
            my %winningTrackLicenseHash;

            my $c = $self->_getLicensesForPublisherStatementWithTrackAndRegion( $trackID, $regionID, $startDate, $endDate, $publisherID );
            $self->_report(
                "   query for track $trackID region $regionID startDate $startDate endDate $endDate publisherID $publisherID returned "
                  . $c->size()
                  . " licenses",
                4
            );
            while ( my $tl = $c->next() ) {
                $self->_report( " looking at track license id " . $self->_idFromTrackLicense($tl), 3 );
                $self->_report( $tl,                                                               4 );

                # Only _1_ track license for a given track and region can go on.
                # But the query may return more than 1, because we have the 'all product' and 'all digital product' types.
                # So I will filter that out here (rather than try and do that in a single query)

                my $licensePublisherID = $self->_publisherIDFromTrackLicense($tl);

                if (
                       $tl->product_type_id == RPS::DB::Item::Product::kProductTypeRingtone && $ringtoneFlag == 1
                    || !$tl->product_type_id && ( $ringtoneFlag == 0 )
                    || 255 == $tl->product_type_id && ( $digitalFlag == 0 && $ringtoneFlag == 0 )  # All physical products
                    || 254 == $tl->product_type_id && ( $digitalFlag == 1 && $ringtoneFlag == 0 )  # All digital products (except ringtones)
                    || $tl->product_type_id == $productTypeID
                  ) {
                    if ( $winningTrackLicenseHash{$licensePublisherID} ) {

                        # more specific beats less specific.
                        #
                        next if ( !$tl->product_type_id );
                        next if ( 255 != $winningTrackLicenseHash{$licensePublisherID}->product_type_id );
                        next if ( 254 != $winningTrackLicenseHash{$licensePublisherID}->product_type_id );
                    }

                    $winningTrackLicenseHash{$licensePublisherID} = $tl;
                }
            }

            foreach my $licensePublisherID ( keys %winningTrackLicenseHash ) {
                my $winningTrackLicense = $winningTrackLicenseHash{$licensePublisherID};
                $self->_report(
                    " WINNING TRACK LICENSE ID FOR PUBLISHER $licensePublisherID : " . $self->_idFromTrackLicense($winningTrackLicense),
                    4 );
                push @trackLicenses, $winningTrackLicense;
            }
        }
    }

    return \@trackLicenses;
}

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

    $self->_report("*** adding reserve pipeline job to queue");
    my $clientID = Common::RSApp::GetClientID();

    my $jobArgs = RPS::Mechanical::US::Job::CreateReservePipelineReport->new(
        runID    => $self->{_runID},
        clientID => $clientID,
    );
    my $job = $jobArgs->enqueue();
    $self->_report("*** done adding reserve pipeline job to queue!");
}

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

    $self->_report("*** importing unlicensed accrual data");
    my $clientID = Common::RSApp::GetClientID();

    my $staticDataPath = "/app/data/us_mechanical_run/" . Common::RSApp::GetClientID() . "/" . $self->{_runID};
    my $outputDataPath = $staticDataPath . "/output";
    my $filename = $outputDataPath . '/mechanical_run_unlicensed_track_log';

    ## Let's make sure file exists before attempting to import it
    if ( -e $filename ) {
        my $fields = "mechanical_run_id,label_id,track_id,album_id,product_id,upc_ean,product_type,unlicensed_share,accrual,year,rate,units";
        my @command = (
            'mysqlimport',                                               '--local',
            '--fields-optionally-enclosed-by=\"',                        "--columns=$fields",
            "--user=" . $Common::RSDB::CLIENT_DB{$clientID}->{username}, "--password=" . $Common::RSDB::CLIENT_DB{$clientID}->{password},
            "--host=" . $Common::RSDB::CLIENT_DB{$clientID}->{server},   $Common::RSDB::CLIENT_DB{$clientID}->{db_name},
            $filename,
        );
        $self->_report("COMMAND: " . join( ' ', @command ), 2 );

        # Catch the bloody error code!
        #
        my $status = system(@command);
        die "ERROR EXECUTING " . join( ' ', @command ) . " : ($?) $!" unless 0 == $status;

        $self->_report("*** done importing unlicensed accrual data!");
    }
}

1;
