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

package RPS::Mechanical::Process::CreateStatement;

# This class will serve as the base class for the processes that create
# Mechanical statements.  There should be a fair amount of common code, so that
# will go here.
#
# The basic idea is the overall 'flow' will be defined here.  Meaning, the
# essential steps will be encapsulated as abstract methods which subclasses
# will override.  And there will be a 'createStatement' method that is implemented
# here in terms of these abstract methods.
#
use strict;

use lib '/app/tools/common/lib';
use lib '/app/tools/rps/lib';
use lib '/app/tools/raptor/lib';
use Data::Dumper;
use Common::Log;
use Common::Assert;
use Common::Util;
use RPS::Mechanical::Process;
use RPS::Statement::Status;
use RPS::Mechanical::Process::SaleException;
use RPS::DB::Item::ClientOptions;
use RPS::DB::Item::Product;
use RPS::DB::Item::SaleRunMap;
use RPS::DB::Item::ProductType;

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

use constant kDefaultLogLevel => 2;

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

    # Invoke the inherited method to set up logging.
    #
    $self->SUPER::_init(%args);

    # The statementID is required.
    # AND the statement should exist in the database already.
    #
    assert( $args{statementID} );

    $self->{_statementID} = $args{statementID};

    # Grab the 'run' data, so we can fetch the payor_id.
    #
    my $run = $self->_getRunDBItem();
    $self->{_payorID} = $run->payor_id;

    $self->_report( "CreateStatement process for statement " . $self->{_statementID} . " initialized", 3 );
    return $self;
}

sub _statementID {
    my ($self) = @_;
    return $self->{_statementID};
}

sub _payorID {
    my ($self) = @_;
    return $self->{_payorID};
}

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

    my $exception = $args{exception};

    my $errorString;
    if ( ref $exception && $exception->isa('Common::Exception') ) {
        $errorString = $exception->errorMessage();
    } else {
        $errorString = $exception;
    }

    my $statement = $self->_getStatementDBItem();
    $statement->error($errorString);
    $statement->status(RPS::Statement::Status::kError);
    $statement->save();
}

sub _readSaleLicenseMap {
    my ( $self, $saleIDs ) = @_;
    assert( 0, 'override this' );
}

sub _readSalePublisherLicenseMap {
    my ( $self, $statement ) = @_;
    assert( 0, 'override this' );
}

sub _isASubPublisher {
    my ( $self, $statement ) = @_;
    assert( 0, 'override this' );
}

sub run {
    my ($self) = @_;
    $self->_report("Entering run");

    my $statement = $self->_getStatementDBItem();

    $statement->status(RPS::Statement::Status::kRunning);
    $statement->save();

    # We'll iterate over all the possible licenses first.
    # This will allow us to pull in reserves, carryover,etc.
    #
    my $allLicenses = $self->_getAllPossibleLicenses();
    $self->_report("Processing reserves and carryover");
    while ( my $license = $allLicenses->next() ) {
        $self->_processLicense($license);
    }

    # Get the array of sale IDs we plan on processing.
    #
    my $saleIDs = $self->_getSaleIDs();
    $self->_report( "Fetched sales. Count = " . scalar(@$saleIDs) );

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

    if ( $self->{_RSD7859} ) {
        $self->_report( "RSD7859 Feature flag enabled - Initializing SaleLicenseMap and SalePublisherLicenseMap");
        $self->_readSaleLicenseMap( $saleIDs );            # map of paid sales
        $self->_readSalePublisherLicenseMap( $statement ); # map of publishers, licenses and applicable sales

        # Check if the stateent publisher has an admin/agent (used by _findMatchingLicensesFast)
        $self->{_isSubPublisher} = 1 if ( $self->_isASubPublisher( $statement )  );
    } else {
        $self->_report( "RSD7859 Feature flag NOT enabled");
    }

    $self->_processSales($saleIDs);

    # I would imagine that we'd want to finalize the statement in some fashion - Calculate
    # sums and so forth
    #
    $self->_report("Finalizing");
    $self->_finalize();
    $self->_report("Done, marking statement as complete");

    $statement->status(RPS::Statement::Status::kComplete);
    $statement->save();

    return 0;
}

sub _processSales {
    my ( $self, $saleIDs ) = @_;

    $self->_report("Processing sales");
    foreach my $saleID (@$saleIDs) {
        my $sale = Raptor::DB::Item::Sale->Lookup( sale_id => $saleID );

        $self->_report( "--------------------------------------------------------", 3 );
        $self->_report( "  sale_id " . $sale->sale_id,                              3 );
        $self->_report( "--------------------------------------------------------", 3 );

        # Skip inactive products.
        #
        my $product = RPS::DB::Item::Product->Lookup( product_id => $sale->product_id );
        if ( RPS::DB::Item::Product::kProductStatusActive != $product->product_status_id() ) {
            $self->_report( "Product inactive, skipping", 3 );
            $self->_markSaleError( $sale, RPS::DB::Item::SaleRunMap::kStatusInactiveProduct );
            next;
        }

        # I want to catch any exceptions, so I can display the naughty sale id.
        #
        eval { $self->_processSale($sale); };

        if ($@) {
            if ( ref $@ && $@->isa('RPS::Mechanical::Process::SaleException') ) {

                # We can log these.
                #
                $self->_logSaleException($@);
            } else {

                # Something else (bad) happened.
                # Re-throw the original exception
                #
                die($@);
            }
        }
    }
}

sub _processLicense {
    my ( $self, $license ) = @_;

    $self->_processLicenseReserves($license);
    $self->_processLicenseCarryover($license);
}

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

    # !!! We'll probably have a different accessor in the Sale class for each
    # !!! type of statement.  So I don't think there will be any inherited behavior.
    #
    die "ERROR - You must overload _getSales";
}

# This can probably be implemented generically.
# !!! For a given sale, though, there can be _multiple_ licenses!
# !!! For every album sale, for example, it may will map to several
# !!! licenses for this publisher.  That's just the way it is, I need to re-do this
#
sub _processSale {
    my ( $self, $sale ) = @_;

    $self->_report( "processSale", 4 );
    $self->_report( $sale,         4 );

    # Quick sanity-check for bad sales.
    #
    die RPS::Mechanical::Process::SaleException::NoDateEnd->new( $sale, 'Sale has invalid date_end, unable to determine stat rate' )
      unless ( $sale->date_end() && $sale->date_end ne '0000-00-00' );

    # Match this sale to the correct license.
    #
    # So will there be more than 1?   I don't think so, not in the single-statement
    # context we have here.
    #
    my $licenses = ( $self->{_RSD7859} ) ? $self->_findMatchingLicensesFast($sale) : $self->_findMatchingLicenses($sale);

    if ( !$licenses || 0 == scalar @$licenses ) {
        $self->_report( "   no license found", 3 );

        # So, if we didn't find a license, we want to _report_ that, so we can
        # display it as '100% unlicensed' in the retention report.
        #
        $self->_logSaleNoLicense($sale);

        # It would be swell if we could combine these logs somehow...
        #
        $self->_markSaleError( $sale, RPS::DB::Item::SaleRunMap::kStatusNoLicense );

        die RPS::Mechanical::Process::SaleException::NoLicense->new( $sale, 'No license could be found to match this sale' );
    }

    # There will be _vastly_ different strategies for this step.
    #
    $self->_processSaleWithLicenses( $sale, $licenses );

}

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

    $self->_report( 'processSaleWithLicenses : sale_id ' . $sale->sale_id, 3 );
    $self->_report( '   number of licenses: ' . scalar @$licenses,         3 );

    # !!! Need to add back in the previous paid statement logic!

    foreach my $trackLicense (@$licenses) {
        # Adding a stub in here so we have a place to log for reporting purposes.
        #
        $self->_logProcessedSaleWithLicense( $sale, $trackLicense );

        # !!! Or can I expect the licenses to have been scrubbed already?
        #
        if ( !$self->_licenseIsValid($trackLicense) ) {
            next;
        }

        if ( $self->_testAlreadyPaidSaleWithLicense( $sale, $trackLicense ) ) {
            $self->_report('already paid', 3);
            $self->_processAlreadyPaidSaleWithLicense( $sale, $trackLicense );
        } else {
            $self->_processSaleWithLicense( $sale, $trackLicense );
        }
    }
}

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

    # Just a stub - We'll override this in the 'Complex' class.
    #
}

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

    # Just a stub - We'll override this in the 'Complex' class.
    #
}

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

    assert( 0, 'override this' );
}

sub _processAlreadyPaidSaleWithLicense {
    my ( $self, $sale, $trackLicense ) = @_;
    assert( 0, 'override this' );
}

sub _processSaleWithLicense {
    my ( $self, $sale, $licenses ) = @_;
    assert( 0, 'override this' );
}

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

    # Create an entry in the sale_run_map
    #
    my $mapEntry = RPS::DB::Item::SaleRunMap->Create(
        sale_id  => $sale->sale_id(),
        run_id   => $self->_runID(),
        run_type => $self->_runType(),
        status   => $status,
    );

    $mapEntry->save();
}

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

    die "ERROR - You must override _runType()";
}

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

    assert($statementItem);

    $self->_markSalePaidWithIDs( $sale->sale_id(), $self->_idFromStatementItem($statementItem) );
}

sub _markSalePaidWithIDs {
    my ( $self, $saleID, $statementItemID ) = @_;

    assert($saleID);
    assert($statementItemID);

    # Create an entry in the sale_run_map
    #
    my $mapEntry = RPS::DB::Item::SaleRunMap->Create(
        sale_id           => $saleID,
        run_id            => $self->_runID(),
        run_type          => $self->_runType(),
        statement_item_id => $statementItemID,
        status            => RPS::DB::Item::SaleRunMap::kStatusPaid,
    );

    $mapEntry->save();
}

# Override this to return the id associated with the statement item.
# Different types have different column names.
# !!! A better design might have been to have a 'StatementItem' class with
#     well-known public accessor methods.
#
sub _idFromStatementItem {
    my ( $self, $statementItem ) = @_;

    die "ERROR - You must override _statementItemToID()";
}

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

    die "ERROR - You must overload _findMatchingLicenses";
}

sub _findMatchingLicensesFast { # RSD7859 - this method will (eventually) replace _findMatchingLicenses
    my ( $self, $sale ) = @_;

    die "ERROR - You must overload _findMatchingLicensesFast";
}

# Override this
#
sub _getAllPossibleLicenses {
    my ($self) = @_;

    die "ERROR - you must override _getAllPossibleLicenses\n";
}

sub _logSaleException {
    my ( $self, $exception ) = @_;

    # !!! By default, we will do nothing.
    # !!! This is only relevant for UK runs at this point.

    # (... so the UK run code will override this method).
}

sub _licenseTypeMatches {
    my ( $self, $license ) = @_;

    return 1;
}

sub _productTypeMatchesLicenseType {
    my ( $self, $productTypeID, $trackLicenseProductTypeID ) = @_;

    $self->_report(
        "_productTypeMatchesLicenseType:  Comparing productTypeID of $productTypeID to license productTypeID of $trackLicenseProductTypeID",
        4
    );

    # NULL == all products.  Matches any product.
    #
    return 1 if ( !$trackLicenseProductTypeID );
    return 1 if ( $productTypeID == $trackLicenseProductTypeID );

    if ( 3 == $productTypeID || 4 == $productTypeID ) {
        return 1 if ( $trackLicenseProductTypeID == RPS::DB::Item::ProductType::kAllDigitalProducts );
        return 0;
    } else {
        return 1 if ( $trackLicenseProductTypeID == RPS::DB::Item::ProductType::kAllPhysicalProducts );
        return 0;
    }
}

1;
