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

# 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::Complex;
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::DB::Item::MechanicalRunTrackLicense;
use RPS::DB::Item::SalePublisherMap;

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

sub _runType
{
    my ($self) = @_;
    assert(0, 'override this to return the correct SaleRunMap constant');
}

sub _createUnlicensedTrackLogEntry
{
    my ($self, $album, $label, $track, $product, $isrc, $upc, $productType, $missingShare, $totalOwed, $units, $year, $rate) = @_;
    assert(0, 'override this');
}

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

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

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


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

    # call the inherited method first.
    #
    $self->SUPER::delete();

    
    # Make sure any 'temporary' data is deleted from the database.
    # Usually this happens automatically for a normal run, but if the run
    # errored out then this data might still be hanging around.
    #
    $self->_deleteMechanicalRunTrackLicenseItems();

    return 0;
}

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

    # Probably the easiest thing would be to read in the whole table, and
    # hash it appropriately.
    #
    my %missingShareMap;

    # !!! If we extend the table to include country code, then we can abstract this access method to just return
    # !!! the rows from the country or contries we care about.
    #
#    my $allItems = RPS::DB::Item::MechanicalRunTrackLicense->GetByRunIDAndType($self->{_runID}, $self->_runType);
    my $allItems = $self->_getMechanicalRunTrackLicenseItems();
    while (my $item = $allItems->next())
    {
        my $productID = $item->product_id;
        my $product = $self->_getProduct($productID);
        my $productTypeID = $product->product_type_id;
        my $productType = $self->_getProductType($productTypeID);
        $productType = $productType->description;

        # Look at the track license - If it's a RINGTONE license, then set productType to 'RING'
        #
        my $trackLicenseID = $item->track_license_id();
        if ($trackLicenseID)
        {
            my $trackLicense = $self->_trackLicenseFromID($trackLicenseID);
            if ($trackLicense && 5 == $trackLicense->type)
            {
                $productType = 'RING';
            }
        }

        push @{$missingShareMap{$item->track_id}{$item->product_id}{$productType}{$item->track_license_id}},
        {
            share => $item->share,
            units => $item->units,
            rate => $item->base_rate,
            year => $item->year,
        };
    }

    $self->_report("_createRetentionReport", 4);
    $self->_report(\%missingShareMap, 4);

    # Now build the report
    #
    foreach my $trackID (keys %missingShareMap)
    {
        my $track = $self->_getTrack($trackID);
        if (defined($track))
        {
		    my $album = $self->_getAlbum($track->album_id);
		    my $label = $self->_getLabel($album->label_id);
		    my $master = $self->_getMaster($track->master_id);
            my $duration = $master->duration;
            my $isrc = $master->isrc;
		
            #next if ($track->mechanical_exempt);

		    # !!! Can't assume that there _is_ a track artist, oddly.
		    #
		    my $trackArtist;
		    my $trackArtistName = '';
			if ($track->artist_id)
			{
				$trackArtist = $self->_getArtist($track->artist_id);
			}

		    if (defined($trackArtist))
		    {
		        $trackArtistName = $trackArtist->name;
		    }
		
		        
		        
            my $productIDHash = $missingShareMap{$trackID};
            foreach my $productID (keys %$productIDHash)
            {   
                my $product = $self->_getProduct($productID);
                my $upc = $product->upc_ean;

                my $productTypeHash = $productIDHash->{$productID};
                foreach my $productType (keys %$productTypeHash)
                {
                    my %rateUnitHash;
		            my $totalShare;

                    my $trackLicenseIDHash = $productTypeHash->{$productType};
                    foreach my $trackLicenseID (keys %$trackLicenseIDHash)
                    {
                        my $share;

                        my $dataArray = $trackLicenseIDHash->{$trackLicenseID};
                        foreach my $data (@$dataArray)
                        {
				            my $units = $data->{units};

                            # !!! Making an assumption here, that this is the same for all data items that
                            # !!! hashed to this location.
                            #
                            $rateUnitHash{$data->{rate}}{$data->{year}} += $data->{units};

                            $share = $data->{share};
                        }

                        $totalShare += $share;
                    }

                    if ($totalShare < 99)
                    {
                        my $missingShare = 100 - $totalShare;
                        foreach my $baseRate (keys %rateUnitHash)
                        {
                            my %totalOwedHash; # keep track of accrual by date range

                            my $yearUnitHash = $rateUnitHash{$baseRate};
                            foreach my $year (keys %$yearUnitHash)
                            {
                                my $units = $yearUnitHash->{$year};
                                my $rate = Common::RSMath::round($baseRate * ($missingShare / 100), 4);
                                $totalOwedHash{$year}{accrual} += Common::RSMath::round($units * $rate, 2);
                                $totalOwedHash{$year}{units} += $units;
                            }

                            foreach my $year (keys %$yearUnitHash)
                            {
                                my $log = $self->_createUnlicensedTrackLogEntry( $album, $label, $track, $product, $trackArtistName, $isrc, $upc, $productType, $missingShare, $totalOwedHash{$year}{accrual}, $totalOwedHash{$year}{units}, $year, $baseRate );
                        
                                $log->save() if ($log);
                            }
                        }
                    }
                }
            }
        }
    }

    # Now delete everything in the temporary table.
    #
    $self->_deleteMechanicalRunTrackLicenseItems();
#    my $allItems = $self->_getMechanicalRunTrackLicenseItems();
}# _createRetentionReport

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

    
    my $runID = $self->{_runID};
    my $runType = $self->_runType();
    my $productID = $sale->product_id;
    my $saleStatRateID = $self->_getStatRateID($sale->date_end);
    my $duration = $self->_getTrackDuration($trackID);
    my $trackLicenseID = $self->_idFromTrackLicense($trackLicense);

    my $countryCode = $sale->country_code;
    my $units = $sale->units;
    my $numSales = $sale->sales;
    my $numReturns = $sale->returns;

    my $year = $self->_getYear($sale);

    my $product = $self->_getProduct($productID);
    if ($self->_productIsDigital($product))
    {
        if ($units < 0)
        {
            $numReturns = (-1 * $units);
        }
        else
        {
            $numSales = $units;
        }
    }
    else
    {
        $units = $numSales - $numReturns;
    }


    # Here's the trick part: _calculateEffectiveRate !!!  
    # This is defined in CreateStatement::Complex, so not in our current scope.
    # !!! But, I believe we can move that to the base class.

    # Calculate what the rate would be, assuming the worst.
    #
    my ($rate)= $self->_calculateEffectiveRate
    (
        $self->_defaultIssueStatRateID(RPS::DB::Item::TrackLicense::kPublicDomain),
        $saleStatRateID,
        RPS::License::RateBasis::kSale,
        RPS::License::RateType::kFull,
        0,
        100,
        $duration,
        100,
        0,
        $trackLicense
    );

    RPS::DB::Item::MechanicalRunTrackLicense->updateUnits (
            run_id => $runID,
            run_type => $runType,
            track_license_id => $trackLicenseID,
            product_id => $productID,
            track_id => $trackID,
            base_rate => $rate,
            units => $units,
            share => $share,
            country_code => $countryCode,
            year => $year,
    );
}

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

    $self->_report("_createSalePublisherMapping", 3);


    # To speed this up, we're going to cache a few tables.
    #
    my %productCache;
    my $allProducts = RPS::DB::Item::Product->GetAll();
    while (my $product = $allProducts->next())
    {
        $productCache{$product->product_id()} = $product;
    }

    my %productTrackCache;
    my $allProductTracks = RPS::DB::Item::ProductTrack->GetAll();
    while (my $productTrack = $allProductTracks->next())
    {
        push @{$productTrackCache{$productTrack->product_id()}}, $productTrack->track_id;
    }


    # Limit this using our payor id
    my %trackLicenseCache;
    my %otherPayorTrackLicenseCache;
    my $allTrackLicenses = $self->_getAllTrackLicenses();
    while (my $tl = $allTrackLicenses->next())
    {
        # Skip inactive licenses.
        #
        next if $tl->inactive();


        # Skip licenses attached to tracks that are exempt from mechanicals...
        #
        my $track = $self->_getTrack($tl->track_id);

        next if ($self->_trackIsMechanicalExempt($track));


        # We want to keep track of tracks that are licesed by _other_ payors as well,
        # so we don't treat them as completely unlicensed (and freak people out).
        # !!! HOWEVER... public domain is public domain for every payor.
        #
        if ($tl->payor_id == $self->{_payorID} || RPS::DB::Item::TrackLicense::kPublicDomain == $tl->type())
        {
            push @{$trackLicenseCache{$tl->track_id()}}, $tl;
        }
        else
        {
            push @{$otherPayorTrackLicenseCache{$tl->track_id()}}, $tl;
        }
    }


    # Fetch all the publishers too.
    # We'll want to know their admin/agent ids.
    #
    my %publisherCache;
    my $allPublishers = $self->_getAllPublishers();
    while (my $pub = $allPublishers->next())
    {
        my $publisherID = $self->_publisherIDFromPublisher($pub);
        $publisherCache{$publisherID} = $pub;
    }



    # We'll use this hash to keep track of any track sales that are totally unlicensed.
    #
    my %unlicensedTracks;

    my $run = $self->_getRunDBItem();
    my $allSales = $self->_getAllUnprocessedSales($run->ending_sale_date());
    $self->_report("_createSalePublisherMapping - scanning sales: count= " . $allSales->size(), 3);
    while (my $sale = $allSales->next())
    {
        my %salePublisherHash;


        $self->_report("looking at sale id " . $sale->sale_id, 3);
        $self->_report($sale, 4);


        if (! $sale->product_id())
        {
            $self->_report("  skipping - no product id", 4);
            next;
        }
        my $product = $productCache{$sale->product_id()};
        if (! $product)
        {
            $self->_report("  skipping - product not in cache", 4);
            next;
        }

        # Going to need to know how many units for the unlicensed report.
        #
        my $units = $sale->units;
        if (! $units)
        {
            $units = $sale->sales - $sale->returns;
        }

        # FB14251 - save the year so that it can be surfaced on the accrual report
        #
        my $year = $self->_getYear($sale);

        # !!! A hack for now...
        #
        my $saleStatRateID;
# 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')
        {
            $saleStatRateID = $self->_getRingStatRateID($sale->date_end);
        }
        else
        {
            $saleStatRateID = $self->_getStatRateID($sale->date_end);
        }

        # We'll attempt to grab the upc from the parent product first.
        my $upc;
        if ($product->parent_product_id && $productCache{$product->parent_product_id}) 
        {
            my $parentProduct = $productCache{$product->parent_product_id};
            $upc = $parentProduct->upc_ean;
        }
        else
        {
            $upc = $product->upc_ean;
        }
        
		my $productTypeID = $product->product_type_id;
        my $productType = $self->_getProductType($productTypeID);

        # !!! This never made it to the CA mechanicals.
        #
		# One off to change product type label to RING for the report. CASE #10576
# JPK - Restore when we're ready for RINGBACK
		$productType = $sale->format_type eq 'R' ? 'RING' : $productType->description;


        my $tracks;
        if (RPS::DB::Item::Product::kProductTypeDigitalTrack == $product->product_type_id)
        {
            $tracks = [ $product->asset_id() ];
        }
        else
        {
            $tracks = $productTrackCache{$product->product_id()};
        }

        # We'll count the number of track licenses encountered for each track associated with this sale.
        # If there aren't _any_ found for any of the tracks on this sale, then we'll log this sale
        # as totally unlicensed.
        #
        my $saleLicenseCount = 0;

        foreach my $trackID (@$tracks)
        {

            # !!! At some point, we will be able to skip tracks that have a license for an other payor.
            # The theory being that we will associate payor at the ALBUM level, so we won't be able to
            # have multiple payors on a single product.
            #
            # However, as of right now (11/2009), we have at least one client (Madacy) where this is not true.
            #
            $self->_report("  product track $trackID", 4);

            # !!! Ok, so here's the trick.
            # !!! Let's just look at the licenses that WILL ACTUALLY apply.
            #
            my $licenses = $self->_getLicensesThatMatchSale($sale, $trackLicenseCache{$trackID});

            foreach my $trackLicense (@$licenses)
            {
                $self->_report("     track license " . $self->_idFromTrackLicense($trackLicense), 4);


                if (RPS::DB::Item::TrackLicense::kPublicDomain == $trackLicense->type)
				{
                    # JPK - We'll put the country test inside the override of _logPublicDomainShare, so we can stay abstract here.
                    #
                    $self->_logPublicDomainShare($sale, $trackID, $trackLicense, $trackLicense->share);
                    next;
                }


                # Which publisher ID to use?
                # If it is not a direct license, use the agent/admin if there is one.
                #
                my $publisherID = $self->_publisherIDFromTrackLicense($trackLicense);
                my $publisher = $publisherCache{$publisherID};
                my $indirectID = $publisher->agent_id;
                $indirectID = $publisher->admin_id unless $indirectID;

                if (! $trackLicense->publisher_direct() && $indirectID)
                {
                    $self->_report("   indirect id = $indirectID", 4);
                    $salePublisherHash{$indirectID} = 1;
                }
                else
                {
                    $self->_report("   direct id = $publisherID", 4);
                    $salePublisherHash{$publisherID} = 1;
                }
            }


            # If there weren't _any_ licenses for that track, in theory that's an accrual, right?
            # But... if this track is licensed by another payor, then we won't include it in this report.
            #
            if (0 != scalar @$licenses || defined $otherPayorTrackLicenseCache{$trackID})
            {
                $self->_report("  incrementing saleLicenseCount", 4);
                $saleLicenseCount++;
            }
            else
            {
                $self->_report("  NO LICENSES FOUND FOR SALE_ID " . $sale->sale_id . " : PRODUCT_ID : " . $product->product_id . " TRACK ID $trackID", 4);

				# Don't bother logging accrual for non US or CA digital sales.
                # JPK - No... don't bother logging accrual for any sale outside of the US, no matter what kind.
				# !!! We'll need a 'native country code' method to make this abstract.
                #
				if ($self->_nativeCountryCode() eq $sale->country_code)
				{
                	my $productID = $product->product_id;
                	$self->_report(" incrementing units by $units", 4);

                    # FB14251 - store the date range information with the stat rate ID
                    # The units will be aggregated for each statRateID by year.
                    #
                    # Depending on the format types of the matched sales, it's possible to have
                    # accruals for different product types even though they're matched to the same
                    # product.  For example, RING and DT sales matched to a DT product.
                    #
                	$unlicensedTracks{$trackID}{$productID}{$productType}{STATRATE}{$saleStatRateID}{$year}{units} += $units;

                	$unlicensedTracks{$trackID}{$productID}{$productType}{upc} = $upc;
				}
				else
				{
					$self->_report(" sale country is " . $sale->country_code . ", so we ignore the accrual. Not logging.", 4);
				}
            }
        }# foreach trackID

        foreach my $publisherID (keys %salePublisherHash)
        {
            $self->_report("  creating SalePublisherMap for publisher $publisherID", 4);
            $self->_createSalePublisherMapEntry(
                run_id => $self->{_runID},
                sale_id => $sale->sale_id(),
                publisher_id => $publisherID,
            );
#            my $newMapItem = RPS::DB::Item::SalePublisherMap->Create
#            (
#                run_id => $self->{_runID},
#                sale_id => $sale->sale_id(),
#                publisher_id => $publisherID,
#            );
#            $newMapItem->save();
        }
    }# while sale

    $self->_report("_createSalePublisherMapping - creating log entries for unlicensed tracks", 3);
    $self->_report(\%unlicensedTracks, 4);
    foreach my $trackID (keys %unlicensedTracks)
    {
        my $track = $self->_getTrack($trackID);
        if ($track)
        {
		    my $album = $self->_getAlbum($track->album_id);
		    my $label = $self->_getLabel($album->label_id);
		    my $master = $self->_getMaster($track->master_id);
            my $duration = $master->duration;
            my $isrc = $master->isrc;

            next if ($self->_trackIsMechanicalExempt($track));


		    # !!! Can't assume that there _is_ a track artist, oddly.
		    #
		    my $trackArtist;
		    my $trackArtistName = '';
			if ($track->artist_id)
			{
			    $trackArtist = $self->_getArtist($track->artist_id);
			}

		    if (defined($trackArtist))
		    {
		        $trackArtistName = $trackArtist->name;
		    }

            # !!! FLIP THIS AROUND:
            # !!! We want to accumulate across stat rates.
            # So, {trackID}{productID}{statRateID}, instead of {trackID}{statRateID}{productID}
            foreach my $productID (keys %{$unlicensedTracks{$trackID}})
            {
                my $product = $self->_getProduct($productID);

                foreach my $productType ( keys %{$unlicensedTracks{$trackID}{$productID}} )
                {

                    my $accrual=0;

                    # FB14251: For each statRateID, we want to calculate the accrual for each
                    # date range that was covered by the statRateID
                    #
                    my $upc = $unlicensedTracks{$trackID}{$productID}{$productType}{upc};

                    foreach my $statRateID (keys %{$unlicensedTracks{$trackID}{$productID}{$productType}{STATRATE}})
                    {

                        # Calculate the actual rate.
                        # The rate will be the same across all date ranges that were associated to the statRateID
                        #
                        my ($statRate, $minuteRate) = $self->_getStatRate($statRateID);
                        my $rate;

                        # If the duration is less than 5 minutes, or if this is a ringtone (no minute
                        # rate) just use the rate associated with the statRateID.  Otherwise use the
                        # minute rate.
                        #
                        if ($duration < 300 || 0 == $minuteRate )
                        {
                            $rate = $statRate;
                        }
                        else
                        {
                            my $numMinutes = int($duration / 60) + 1;
                            $rate = $numMinutes * $minuteRate;
                        }

                        foreach my $year (keys %{$unlicensedTracks{$trackID}{$productID}{$productType}{STATRATE}{$statRateID}})
                        {
                            my $units = $unlicensedTracks{$trackID}{$productID}{$productType}{STATRATE}{$statRateID}{$year}{units};
                            $accrual = ($units * $rate);

                            # FB14251 - save the units, date range and rate info along with the accrual
                            #
                            my $newLogEntry = $self->_createUnlicensedTrackLogEntry($album, $label, $track, $product, $trackArtistName, $isrc, $upc, $productType, 100, $accrual, $units, $year, $rate);
                            $newLogEntry->save();
                        }
                    }

                }# foreach productType

            }# foreach productID
        }
    }# foreach trackID
}# _createSalePublisherMapping

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

    $self->_report("_getLicensesThatMatchSale:  incoming licenses:", 4);
    $self->_report($trackLicenses, 4);

    my $matchingRegions = $self->_findRegionsForCountryCode($sale->country_code);
    if (! $matchingRegions || 0 == scalar @$matchingRegions)
    {
        $self->_report("No matching region found for country code '".$sale->country_code."', in sale " . $sale->sale_id . " - returning no licenses");
        return [];
    }

    # Create a hash of matching region_ids, to speed up comparisons later.
    #
    my %validRegions;
    foreach my $regionID (@$matchingRegions)
    {
        $validRegions{$regionID} = 1;
    }
    $self->_report("_getLicensesThatMatchSale:  valid regions:", 4);
    $self->_report(\%validRegions, 4);

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


    # Ok,let's get organized here.
    # We have an array of all the track licenses, from everybody imaginable, that were on this track.
    # Just for our payor, though, which is nice.
    # So, we want to return the best license for each publisher
    #
    my %winningTrackLicenseHash;
    foreach my $tl (@$trackLicenses)
    {
        $self->_report(" looking at track license id " . $self->_idFromTrackLicense($tl), 3);

        # Public domain licenses don't have regions...
        #
        next unless ($validRegions{ $tl->region_id } || RPS::DB::Item::TrackLicense::kPublicDomain == $tl->type);



        # 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 && $ringtoneFlag == 0 ) ) # Prevent ringtone sales from hitting DT licenses
        {
            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);
            }

            $self->_report("becoming the current winner...", 4);
            $winningTrackLicenseHash{$licensePublisherID} = $tl;
        }
    }

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

    $self->_report("______ returning this list of licenses:", 4);
    $self->_report(\@matchingLicenses, 4);

    return \@matchingLicenses;
}

1;
