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

package RPS::ArtistRoyalty::Process::RunController;
use strict;
use Data::Dumper;
use File::Path;
use Time::HiRes qw(gettimeofday);

use lib '/app/tools/job/lib';
use Job::Status;

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

use lib '/app/tools/raptor/lib';
use Raptor::DB::Item::Sale;

use lib '/app/tools/rps/lib';
use RPS::DB::Item::ArtistRoyaltyStatement;
use RPS::Statement::Status;
use RPS::RoyaltyRun::Status;

use RPS::DB::Item::ArtistPayee;
use RPS::DB::Item::ArtistPayeeAccount;
use RPS::DB::Item::ArtistRoyaltyAlbumBalanceAccount;
use RPS::DB::Item::ArtistRoyaltyRun;
use RPS::DB::Item::ArtistRoyaltyAlbum;
use RPS::DB::Item::ArtistRoyaltyExpenseItem;
use RPS::DB::Item::ArtistRoyaltyIncomeItem;
use RPS::DB::Item::ArtistRoyaltyLicenseIncomeItem;
use RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog;
use RPS::DB::Item::ConsolidatedArtistRoyaltyRunMissedSaleLog;
use RPS::DB::Item::ContractLevelLicenseIncomeBalanceAccount;
use RPS::DB::Item::ArtistRoyaltyTransaction;
use RPS::DB::Item::SaleRunMap;
use RPS::DB::Item::ProductTrack;
use RPS::DB::Item::ReportQueries;
use RPS::DB::Item::ReportQueries::ArtistReservePipeline;
use RPS::DB::Item::NewArtistContract;
use RPS::DB::Item::TrackContract;
use RPS::DB::Item::AlbumContract;
use RPS::DB::Item::PendingTransaction;
use RPS::DB::Item::FinanceTransaction;
use RPS::DB::Item::Expense;
use RPS::DB::Item::ArtistContractTermReserveRun;
use RPS::DB::Item::ArtistContractTermReserve;
use RPS::DB::Item::Payor;
use RPS::DB::Item::Track;
use RPS::DB::Item::Product;
use RPS::ArtistRoyalty::Job::CreatePDFStatements;
use RPS::ArtistRoyalty::Job::CreateExcelStatements;
use RPS::Statement::Artist::PDF;
use RPS::ArtistRoyalty::Process::Exception;

use base 'RPS::ArtistRoyalty::Process';

use constant kSleepTime => 6;

# maximum number of sales to update in single update (see _setSaleStatePending)
use constant kMaxSales => 2000;

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;
    $self->{_label}   = $run->label;

    return $self;
}

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

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

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

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

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

    # Tag all sales processed this run as 'pending'.
    #
    $self->_setSaleStatePending();

    # decrement the periods_remaining for the reserves we 'touched' this run.
    #
    $self->_adjustReservesPeriod();

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

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

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

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

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

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

    # Create a job for all Excel statements, and put it in the queue.
    #
    $jobArgs = RPS::ArtistRoyalty::Job::CreateExcelStatements->new(
        runID    => $self->{_runID},
        clientID => $clientID,
        filePath => $filePath,
    );
    $job = $jobArgs->enqueue();
}

########

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

    my $runID = $self->{_runID};
    assert($runID);

    # Get the run - we need to check it's status to make sure we can delete it.
    #
    my $run = RPS::DB::Item::ArtistRoyaltyRun->Lookup( artist_royalty_run_id => $runID );
    if ( !$run ) {
        die RPS::ArtistRoyalty::Process::Exception->new("ERROR - cannot find artist royalty run with id $runID");
    }

    if ( RPS::RoyaltyRun::Status::kCommitted == $run->status ) {
        die RPS::ArtistRoyalty::Process::Exception->new("ERROR - run $runID has already been committed, and cannot be deleted");
    }

    # Delete the reserves.
    #
    RPS::DB::Item::ArtistContractTermReserve->DeleteByArtistRoyaltyRunID($runID);
    RPS::DB::Item::ArtistContractTermReserveRun->DeleteByArtistRoyaltyRunID($runID);

    # Get all of the statements created by this run,
    # and delete them (and their children!).
    #
    my $statements = RPS::DB::Item::ArtistRoyaltyStatement->GetByArtistRoyaltyRunIDForDeletion($runID);

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

    # Delete the pdf statements
    #
    my $pdfPath = RPS::Statement::Artist::PDF::StatementPathFromRunID($runID);
    if ( -d $pdfPath ) {
        system("rm -rf $pdfPath");
    }

    # Delete this run from the run table.
    #
    $run->delete();

    # Delete sale map entries
    #
    RPS::DB::Item::SaleRunMap->DeleteByArtistRoyaltyRunID($runID);

    # Delete the reports
    #
    RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog->DeleteByRunID($runID);
    RPS::DB::Item::ConsolidatedArtistRoyaltyRunMissedSaleLog->DeleteByRunID($runID);
    RPS::DB::Item::ReportQueries::ArtistReservePipeline->DeleteByArtistRoyaltyRunID($runID);

    return 0;
}

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

    my $statementID = $statement->artist_royalty_statement_id;

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

    $self->_report( "  ... deleting transactions", 3 );
    RPS::DB::Item::ArtistRoyaltyTransaction->DeleteByArtistRoyaltyStatementID($statementID);

    $self->_report( "  ... deleting license income items", 3 );
    RPS::DB::Item::ArtistRoyaltyLicenseIncomeItem->DeleteByArtistRoyaltyStatementID($statementID);

    my $albums = RPS::DB::Item::ArtistRoyaltyAlbum->GetByArtistRoyaltyStatementID($statementID);
    while ( $albums->hasNext() ) {
        my $album   = $albums->next();
        my $albumID = $album->artist_royalty_album_id;

        $self->_report( "  ... deleting expense items", 3 );
        RPS::DB::Item::ArtistRoyaltyExpenseItem->DeleteByArtistRoyaltyAlbumID($albumID);

        $self->_report( "  ... deleting income items", 3 );
        RPS::DB::Item::ArtistRoyaltyIncomeItem->DeleteByArtistRoyaltyAlbumID($albumID);
    }

    $self->_report( "  ... deleting album items", 3 );
    RPS::DB::Item::ArtistRoyaltyAlbum->DeleteByArtistRoyaltyStatementID($statementID);

    return 0;
}

#########

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

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

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

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

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

sub _preCommitSanityCheck {
    my $self = shift;
    my ($runID) = $self->{_runID};

    # Make sure nobody deleted the pending transactions that appear on this run!
    #
    my $statements = RPS::DB::Item::ArtistRoyaltyStatement->GetByArtistRoyaltyRunID($runID);
    while ( my $statement = $statements->next() ) {
        my $statementID         = $statement->artist_royalty_statement_id;
        my $statementTransItems = RPS::DB::Item::ArtistRoyaltyTransaction->GetByArtistRoyaltyStatementID($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 ) {
                $self->_report( "ERROR - original pending transaction was deleted! : " . Dumper($transItem) );
                die RPS::ArtistRoyalty::Process::Exception->new( "original pending transaction was deleted", $transItem );
            }
        }

    }

    return 1;
}

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

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

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

    my $statementID = $statement->artist_royalty_statement_id;
    my $payorID     = $statement->payor_id;

    $self->_report( "\n------------------------------------\n", 2 );

    $self->_report( "committing statement id $statementID", 2 );

    # Go through each album on the statement.
    #
    my $albums = RPS::DB::Item::ArtistRoyaltyAlbum->GetByArtistRoyaltyStatementID($statementID);
    while ( my $album = $albums->next() ) {
        $self->_commitAlbum( $album, $statement );
    }

    # Handle the crossed license income account
    #
    if ( $statement->cross_collateralized_subtotal < 0 ) {

        # Carry the total balance forward, by applying the subtotal as a transaction.
        #
        if ( $statement->contract_level_license_income_subtotal != 0 ) {
            $self->_applyTransactionToContractLevelBalanceAccount( $statement->payee_id, $payorID,
                $statement->contract_level_license_income_subtotal );
        }
    } else {

        # We paid off any previous balance, so apply a transaction to zero the accuont.
        # !!! If there is an account...
        #
        if ( $statement->contract_level_license_income_previous_balance != 0 ) {
            $self->_applyTransactionToContractLevelBalanceAccount( $statement->payee_id, $payorID,
                ( -1 * $statement->contract_level_license_income_previous_balance ) );
        }
    }

    # Now handle the holdover account
    #
    my $total         = $statement->total;
    my $artistPayeeID = $statement->payee_id;

    my $payeeAccountMap = RPS::DB::Item::ArtistPayeeAccount->Lookup( artist_payee_id => $artistPayeeID, payor_id => $payorID );
    if ( !$payeeAccountMap ) {
        $payeeAccountMap = RPS::DB::Item::ArtistPayeeAccount->Create(
            artist_payee_id => $artistPayeeID,
            payor_id        => $payorID,

            # !!! Will need to account for currency_code
        );
        $payeeAccountMap->save();
    }

    my $payeeAccountID = $payeeAccountMap->finance_account_id;
    if ( !$payeeAccountID ) {
        my $account = RPS::DB::Item::FinanceAccount->Create(
            description   => "advance account for artist payee $artistPayeeID",
            type_code     => RPS::DB::Item::FinanceAccount::kAccountTypeAdvance,
            currency_code => Common::Client::Current()->Locale()->currencyFormat()->currencyCode(),
        );
        $account->save();
        $payeeAccountID = $account->finance_account_id;
        $self->_report( "created new account, id $payeeAccountID", 3 );

        # Save the account id in the mapping table.
        #
        $payeeAccountMap->finance_account_id($payeeAccountID);
        $payeeAccountMap->save();
    }

    # Apply the royalties total as an adjustment
    #
    my $statementSubtotal = $statement->total;
    if ( $statementSubtotal < 0 or $statementSubtotal > 0 ) {
        $self->_report( "adjusting balance by total amount of $statementSubtotal", 3 );
        my $transaction = RPS::DB::Item::FinanceTransaction->Create(
            finance_account_id => $payeeAccountID,
            amount             => $statementSubtotal,
            type_code          => RPS::DB::Item::FinanceTransaction::kTypeClosingBalance,
            memo               => "artist royalty run $self->{_label}",

            # !!! currency_code?
        );
        $transaction->save();
    }

    # Now apply all pending transactions, and delete them.
    #
    my $transactionItems = RPS::DB::Item::ArtistRoyaltyTransaction->GetByArtistRoyaltyStatementID($statementID);
    while ( my $item = $transactionItems->next() ) {

        # Fetch the 'real' pending transaction.
        #
        my $pending = RPS::DB::Item::PendingTransaction->Lookup( pending_transaction_id => $item->pending_transaction_id );

        # !!! Note that amount == amount. no sign flipping.  We have to get the sign right when we create the
        # !!! pending transaction in the first place.
        #
        $self->_report( "applying pending transaction id " . $pending->pending_transaction_id . " amount of " . $pending->amount(), 3 );
        my $transaction = RPS::DB::Item::FinanceTransaction->Create(
            finance_account_id => $pending->finance_account_id,
            amount             => $pending->amount,
            type_code          => $pending->type_code,
            memo               => $pending->memo,
            check_number       => $pending->check_number,
            currency_code      => $pending->currency_code,
            transaction_date   => $pending->transaction_date,
        );
        $transaction->save();

        $pending->delete();
    }
}

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

    my $payorID = $statement->payor_id;

    # Need to adjust this album's balance.
    #
    my $previousBalance  = $album->previous_balance;
    my $total            = $album->total;
    my $appliedToAccount = 0;

    # Whether we carry a _positive_ balance forward depends on whether
    # this album is crossed, and whether the crossed total is < 0
    #
    if ( $album->is_cross_collateralized() ) {
        if ( $statement->cross_collateralized_subtotal < 0 ) {

            # Carry forward everything
            #
            # !!! This is WRONG !!!
            # !!! Should be $total - $previousBalance, just like always.
            #            $appliedToAccount = $total;
            $appliedToAccount = $total - $previousBalance;
        } else {

            # Zero out album balances
            #
            $appliedToAccount = ( -1 * $previousBalance );
        }
    } else {
        if ($previousBalance) {
            if ( $total >= 0 ) {
                $appliedToAccount = ( -1 * $previousBalance );
            } else {
                $appliedToAccount = $total - $previousBalance;
            }
        } elsif ( $total < 0 ) {
            $appliedToAccount = $total;
        }
    }

    if ( 0 != $appliedToAccount ) {

        my $artistPayeeID    = $statement->payee_id;
        my $albumID          = $album->album_id;
        my $artistContractID = $album->artist_contract_id;

        $self->_report( "applying balance of $appliedToAccount to holdover for album $albumID, payee $artistPayeeID", 3 );

        # Get the account, post the transaction
        #
        my $accountID;
        my $accountMap = RPS::DB::Item::ArtistRoyaltyAlbumBalanceAccount->Lookup(
            album_id           => $albumID,
            artist_payee_id    => $artistPayeeID,
            payor_id           => $payorID,
            artist_contract_id => $artistContractID,
        );
        if ($accountMap) {
            $accountID = $accountMap->account_id;
        } else {

            # Create a new account
            #
            my $account = RPS::DB::Item::FinanceAccount->Create(
                description   => "holdover account for album $albumID artist payee $artistPayeeID",
                type_code     => RPS::DB::Item::FinanceAccount::kAccountTypeHoldover,
                currency_code => Common::Client::Current()->Locale()->currencyFormat()->currencyCode(),
            );
            $account->save();
            $accountID = $account->finance_account_id;
            $self->_report( "created new holdover account, id $accountID", 3 );

            # Create a new mapping
            #
            $accountMap = RPS::DB::Item::ArtistRoyaltyAlbumBalanceAccount->Create(
                album_id           => $albumID,
                artist_payee_id    => $artistPayeeID,
                payor_id           => $payorID,
                account_id         => $accountID,
                artist_contract_id => $artistContractID,

                # !!! Will need to account for currency_code
            );
            $accountMap->save();
        }

        # Post the transaction
        #
        my $statementID = $statement->artist_royalty_statement_id;
        my $transaction = RPS::DB::Item::FinanceTransaction->Create(
            finance_account_id => $accountID,
            amount             => $appliedToAccount,
            type_code          => RPS::DB::Item::FinanceTransaction::kTypeAdjustment,
            memo               => "artist statement $statementID",

            # !!! currency_code?
        );
        $transaction->save();
    }

    # Mark all processed expenses as 'processed'
    #

    my $expenseItems = RPS::DB::Item::ArtistRoyaltyExpenseItem->GetByArtistRoyaltyAlbumID( $album->artist_royalty_album_id );
    if ($expenseItems) {
        $self->_report( "marking " . $expenseItems->size() . " expenses as 'processed'", 3 );
        while ( my $expenseItem = $expenseItems->next() ) {
            my $expense = RPS::DB::Item::Expense->Lookup( expense_id => $expenseItem->expense_id );
            $expense->processed(1);
            $expense->save();
        }
    }
}

sub _applyTransactionToContractLevelBalanceAccount {
    my ( $self, $payeeID, $payorID, $amount ) = @_;

    my $balanceAccount =
      RPS::DB::Item::ContractLevelLicenseIncomeBalanceAccount->Lookup( artist_payee_id => $payeeID, payor_id => $payorID );
    if ( !$balanceAccount ) {
        $balanceAccount =
          RPS::DB::Item::ContractLevelLicenseIncomeBalanceAccount->Create( artist_payee_id => $payeeID, payor_id => $payorID );
        $balanceAccount->save();
    }

    my $accountID = $balanceAccount->finance_account_id;
    if ( !$accountID ) {
        my $account = RPS::DB::Item::FinanceAccount->Create(
            description   => "contract level balance account for artist payee $payeeID",
            type_code     => RPS::DB::Item::FinanceAccount::kAccountTypeAdvance,
            currency_code => Common::Client::Current()->Locale()->currencyFormat()->currencyCode(),
        );
        $account->save();
        $accountID = $account->finance_account_id;

        # Save the account id in the mapping table.
        #
        $balanceAccount->finance_account_id($accountID);
        $balanceAccount->save();
    }

    my $transaction = RPS::DB::Item::FinanceTransaction->Create(
        finance_account_id => $accountID,
        amount             => $amount,
        type_code          => RPS::DB::Item::FinanceTransaction::kTypeAdjustment,
        memo               => "artist royalty run $self->{_label}",
    );
    $transaction->save();
}

sub _setSaleStatePending {
    my $self  = shift;
    my $runID = $self->{_runID};
    assert($runID);

    my $saleMapItems = RPS::DB::Item::SaleRunMap->GetPaidByArtistRoyaltyRunID($runID);
    if ($saleMapItems) {
        $self->_report( "marking " . $saleMapItems->size() . " sales as processed", 2 );


        my $dbo        = Common::RSApp::GetClientDB();
        my $chunk      = 1;
        my $saleCount  = 0; # number of sales in buffer (reset after each UPDATE)
        my $totalSales = 0; # running total of sales processed
        my @sbuf;           # buffer of saleIDs to flag

        my $baseQuery  = "UPDATE sale SET artist_royalty_status=". Raptor::DB::Item::Sale::kPending
                       . " WHERE sale_id IN (";

        # Loop over all the sales in the run, but instead of flagging each one individually,
        # we'll load up a sale buffer (chunk) and flag sales en masse once the buffer gets to
        # certain size.
        while ( my $saleMapItem = $saleMapItems->next() ) {

            my $saleID = $saleMapItem->sale_id;
            $saleCount++;
            $totalSales++;

            push @sbuf, $saleID;

            if ( $saleCount > kMaxSales ) {
                my $sql = $baseQuery . join(",", @sbuf) . ")";

                # execute the update ...
                my $sth = $dbo->DoCmd($sql);
                $self->_report( "# Processed chunk # $chunk ($saleCount sale(s))", 3 );

                # reset the buffer
                $saleCount = 0;
                undef @sbuf;
                undef $sql;
                $chunk++;
            }
        } # saleMapItems loop

        # flag any leftover sales in buffer
        if ( @sbuf ) {
            my $sql = $baseQuery . join(",", @sbuf) . ")";

            # execute the update ...
            my $sth = $dbo->DoCmd($sql);
            $self->_report( "# Processed chunk # $chunk ($saleCount sale(s))", 3 );
        }
        $self->_report( "DONE: Processed $totalSales sale(s)", 2 );

    }
}

sub _adjustReservesPeriod {
    my $self  = shift;
    my $runID = $self->{_runID};
    assert($runID);

    my $reserveMapItems = RPS::DB::Item::ArtistContractTermReserveRun->GetByArtistRoyaltyRunID($runID);
    while ( my $reserveMap = $reserveMapItems->next() ) {
        my $reserve =
          RPS::DB::Item::ArtistContractTermReserve->Lookup( artist_contract_term_reserve_id => $reserveMap->artist_contract_term_reserve_id,
          );

        my $period = $reserve->periods_remaining();

        if ( 1 == $period ) {
            $reserve->liquidated_run_id($runID);
        }

        $reserve->periods_remaining( $period - 1 );
        $reserve->save();
    }
}

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

    # Only kill children if a users aborts the process.
    $self->_abortChildren(@_) if ( $args{interupt} );

    $self->SUPER::_onFailure(%args);
}

sub _abortChildren {
    my $self = shift;
    my %args = @_;

    $self->_report( "  - Marking aborted processes as aborted", 2 );
    my $dbItem = $self->_getRunDBItem();
    my $jobID  = $dbItem->job_id;

    assert($jobID);

    $self->_report( "  - Found parent job, ID: $jobID", 2 );
    Job::Job->KillChildJobs($jobID);
}

1;
