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

use strict;
use warnings;

use lib '/app/tools/common/lib';
use Common::RSApp;

use lib '/app/tools/rps/lib';
use RPS::DB::Item::TrackLicense;


sub new
{
    my ($class, %args) = @_;

    my $self = bless {}, $class;

    return $self->_init(%args);
}


sub _init
{
    my ($self, %args) = @_;
    
    $self->{clientID} = $args{clientID};
    $self->{startDate} = $args{startDate};
    $self->{endDate} = $args{endDate};
    $self->{dryRun} = $args{dryRun};
    $self->{verbosity} = $args{verbosity};
    $self->{showProgress} = $args{showProgress};
    $self->{outputFileHandle} = $args{outputFileHandle};

    return $self;
}


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

    $self->_findLicensesToAdjust();

    $self->_processSales();
}


sub commitToDB
{
    my ($self) = @_;
}


sub writeCache
{
    my ($self) = @_;
}



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

    my $sales = Raptor::DB::Item::Sale::GetUnprocessedMechanicalSales
    (
        endDate => $self->{endDate},
        period => 1, # <-- This is far from being generic...
        countryCode => 'US',
        formatType => 'D',
        skipFree => 1,
    );


	while (my $sale = $sales->next())
	{

        # We'll need to refer to both the stat rate that applied at the time the product
        # was released, and the stat rate that applied at the time of the sale.
        #
        # Alas, we don't actually have a firm date for these sales - All we have is
        # a date range, which corresponds to the reporting period from which these
        # sales were aggregated.   This means that there is the possibility that
        # the units in this sales record fall on opposite sides of a stat rate bump!
        # At this point, there is nothing we can do to get the 'real' date of sale, so
        # it would seem the safest thing to do is err on the side of the later date.
        #
        my $saleStatRateID = _getStatRateID($sale->date_end);

        my $productID = $sale->product_id;
        if (! $productID)
        {
            _report(" !!! skipping sale - missing product id: " . Dumper($sale), 2 );
            next;
        }


        # Fetch the product's data, then dereference some of the fields for later.
        #
        my $product = _getProduct($productID);

        my $productTypeID = $product->product_type_id;

        # !!! This is a bit of a hack - if we don't have a release date, just
        # use the date_end.
        #
        my $releaseDate = $product->release_date;
        $releaseDate = $sale->date_end unless $releaseDate;


        # Get the stat rate that corresponds to the release date.
        #
        my $issueStatRateID  = _getStatRateID($releaseDate);


        # Build a data structure that allows us to determine whether
        # we've already paid mechanicals for a given track_license,
        # and what the %share was.
        # This will enable us to detect when a new license has been
        # added, and we therefore need to re-visit a sale.
        # -- !!! How about controlled comp?  If we discover a new
        # -- license has been added, we will need to re-calculate what
        #    is owed to the controlled comp...
        #
        # -- Primary key will be track_license_id
        #
        my $previousSaleStatementItems = _getPreviousSaleStatementItems($sale, $issueStatRateID, $saleStatRateID);
        _report("\n\n-----------------------------------------------\nsale: " . Dumper($sale), 3);
        _report("previous sale statement items: " . Dumper($previousSaleStatementItems), 3);

        # Dereference the number of units in this sale.
        #
        my $units = $sale->units;


        # Fetch all the tracks associated with this sale.
        #
		my $productTracks = _getTracksFromProduct($product);


        _report("  track count: " . scalar @$productTracks, 3);


        # Process each track
        #
		foreach my $productTrack (@$productTracks)
		{
            $self->_processProductTrack($productTrack, $sale, $previousSaleStatementItems, $productTypeID, $issueStatRateID, $saleStatRateID);
        }
    }
}


sub _processProductTrack
{
    my ($self, $productTrack, $sale, $previousSaleStatementItems, $productTypeID, $issueStatRateID, $saleStatRateID) = @_;

    my $trackID = $productTrack->track_id;


    # Get the track data.
    #
    my $track = _getTrack($productTrack->track_id);


    # get the appropriate track-licenses for this track.
	#
    my $trackLicenseList = _getTrackLicenses($track->track_id(), $self->{startDate}, $self->{endDate}, $productTypeID);


    # We'll want to know whether this track's ownership is 100% accounted for.
    #
    my $shareSum = 0;


    # We'll need the track duration for stat rate calulcations.
    #
    my $masterID = $track->master_id;
    die "!!! Track has no master, cannot determine duration" unless $masterID;

    my $masterData = _getMaster($masterID);

    my $duration = $masterData->duration;


    foreach my $trackLicense (@$trackLicenseList)
    {
        my $share = $self->_processTrackLicense($track, $trackLicense, $sale, $duration, $previousSaleStatementItems, $issueStatRateID, $saleStatRateID);

        $shareSum += $share;
    }

    _report("   track share% attributed to licenses: $shareSum", 2);


    # Figure out how much remains unallocated
    #
    if (100 > $shareSum)
    {
        my $unallocatedShare = (100 - $shareSum);

        my $units = $sale->units;

        # Calculate the _maximum_ applicable rate.
        #
        my $effectiveRate = _calculateEffectiveRate($issueStatRateID, $saleStatRateID,
         RPS::DB::Item::TrackLicense::kRateBasisSale, RPS::DB::Item::TrackLicense::kRateTypeFull,
         0, $duration, $unallocatedShare);

        my $totalOwed = $units * $effectiveRate;
        _report("   unallocated shares: $unallocatedShare  rate: $effectiveRate  totalOwed: $totalOwed", 2);

        
        $self->{unallocatedBalance} += $totalOwed;
    }
}


sub _processTrackLicense
{
    my ($self, $track, $trackLicense, $sale, $duration, $previousSaleStatementItems, $issueStatRateID, $saleStatRateID) = @_;

    my $productID = $sale->productID;
    my $units = $sale->units;
    my $share = 0;
    my $trackLicenseID = $trackLicense->track_license_id;
    my $publisherID = $trackLicense->publisher_id;

    _report("   trackLicenseID: $trackLicenseID", 3);
                

    # Have we already paid out this track license in a previous lifetime?
    # If so, we may need to make adjustments to the controlled comp account.
    #
    # In other words, if we've added a new license since the last time we
    # processed this sale, and there was some $ given to the unknown publisher,
    # we are probably going to owe the ccomp publisher some money...
    #
    if ($previousSaleStatementItems->{$trackLicenseID})
    {
        my $previousStatementItem = $previousSaleStatementItems->{$trackLicenseID};

        _report("   -- detected a previous sale - looking for adjusted license with id $trackLicenseID", 3);

        if ($self->{licensesToAdjust}{$trackLicenseID})
        {
            # This statement item will need adjusting.
            #
            _report("   ++ found adjusted license, adding to adjustment state", 3);
            push @{$self->{adjustmentState}{$trackLicenseID}}, $previousStatementItem;
        }

        # Skip this license 
        # After all, we've already _paid_ this license holder for this sale.
        # The only point to this exercise is to gather some state to allow us
        # to calculate adjustments later...
        #
        return $trackLicense->share;
    }


    # Skip this license if it's the controlled composition clause.
    # (But first, we'll stash the controlled comp away for later perusal)
    #
    # !!! - There can be MORE THAN ONE controlled comp track-license per track.
    #       The _license_ should be the same, but the publisher (and share) may vary.
    #
    # jpk - Well, not quite - the _clause_ is the same, but the ccomp_license will vary.
    #       So, all the terms will be shared (and therefore, the effective rate).
    #       |-> This might help simplifying things, espescially calculating adjustments.
    #
    # So I need to store all the track-licenses.
    #
    if (RPS::DB::Item::TrackLicense::kControlledComposition eq $trackLicense->type)
    {
        my $albumID = $track->album_id;
        if (! $albumID)
        {
            croak("??? We have a controlled composition on a track with no album? - " . Dumper($trackLicense));
        }

        my $ccID = $trackLicense->controlled_composition_id;
        if (! $ccID)
        {
            _report("!!! ERROR !!! trackLicense is type controlled composition, but controlled_composition_id is invalid!  SKIPPING");
            next;
        }

        $share = $trackLicense->share;
        _report("    license is controlled comp - share = $share", 3);


        # (XXX) It may be helpful to wrap some more abstraction around this gnarly data structure..

        my %newRecord;
        $newRecord{units} = $units;
        $newRecord{saleID} = $sale->sale_id;
        $newRecord{share} = $trackLicense->share;

        my $trackID = $track->track_id;
        push @{$self->{ccompState}{$albumID}{$ccID}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}{$trackID}}, \%newRecord;

        return $share;
    }


    # If this is a public domain share, make a note of the share, then skip.
    #
    if (RPS::DB::Item::TrackLicense::kPublicDomain eq $trackLicense->type)
    {
        $share = $trackLicense->share;
        _report("    license is public domain - share = $share", 3);

        return $share;
    }


    # Check to see whether this track_license is tagged as 'publisher_direct'.
    # If it _is_, then we proceed as usual.
    # If _not_, then activity on this license is not reported on the publisher's
    # statement - instead, it gets reported on the Agent's statement.
    #
    if (! $trackLicense->publisher_direct)
    {
        my $publisherData = _getPublisher($publisherID);

        my $agentID = $publisherData->agent_id;
        if ( $agentID)
        {
            $publisherID = $agentID;
            _report("    reporting on agent $publisherID statement", 2);
        }

        # JPK - Note that if the agentID was not set, then we essentially
        # ignore the publisherDirect flag (and treat it automatically as publisherDirect)
        #
    }


    # Calculate the royalties!
    # 
    $share = $trackLicense->share;
    _report("    share % = $share", 2);


    # Calculate the effective units to pay on.
    #
    my $effectiveUnits = $units;
    _report("    gross units = $units", 2);

    $effectiveUnits = ceil($effectiveUnits * ($trackLicense->percentage_of_sales / 100 ));
    _report("    units after applying PercentageOfSales of " . $trackLicense->percentage_of_sales 
     . " = $effectiveUnits", 2);

    $effectiveUnits = _applyDeduction($effectiveUnits, $trackLicense->packaging_deduction);
    _report("    units after applying PackagingDeduction of " . $trackLicense->packaging_deduction 
     . " = $effectiveUnits", 2);

    $effectiveUnits = _applyDeduction($effectiveUnits, $trackLicense->free_goods);
    _report("    units after applying FreeGoods of " . $trackLicense->free_goods . " = $effectiveUnits", 2);

    $effectiveUnits = _applyDeduction($effectiveUnits, $trackLicense->misc_deduction);
    _report("    units after applying MiscDeduction of " . $trackLicense->misc_deduction 
     . " = $effectiveUnits", 2);


    # Calculate the effective rate
    #
    my $effectiveRate = _calculateEffectiveRate($issueStatRateID, $saleStatRateID, 
     $trackLicense->rate_basis, $trackLicense->rate_type, $trackLicense->penny_rate, $duration, $share);
    _report("    effectiveRate = $effectiveRate", 2);
                    

    # If there is a reserve specified, take it now (after other deductions)
    #
    my $amountReserved = 0;
    my $unitsAfterReserve = _applyDeduction($effectiveUnits, $trackLicense->reserve_percentage);
    if ($unitsAfterReserve < $effectiveUnits)
    {
        $amountReserved = $effectiveUnits - $unitsAfterReserve;
        $effectiveUnits = $unitsAfterReserve;
    }
    _report("    effectiveUnits after reserve % of " . $trackLicense->reserve_percentage
     . " : $effectiveUnits", 2);


    my $totalOwed = $effectiveUnits * $effectiveRate;
    _report("    totalOwed: $totalOwed", 2);


    # Fetch the Statement.
    #
    my $statement = $self->_getStatement($publisherID, $self->{startDate}, $self->{endDate});


    # Create a statement item if one doesn't exist yet.
    #
    my $statementID = $statement->mechanical_statement_id;
    my $statementItem = $self->_getStatementItem($statementID, $productID, $issueStatRateID,
     $saleStatRateID, $trackLicense->track_license_id, $sale->sale_id);



    # Update the statement item
    #
    $statementItem->gross_units( $statementItem->gross_units() + $units);
    $statementItem->net_units( $statementItem->net_units() + $effectiveUnits );
    $statementItem->net_rate( $effectiveRate ); # !!! Might be good to sanity-check this.
    $statementItem->amount( $statementItem->amount() + $totalOwed );
    $statementItem->reserved( $statementItem->reserved() + $amountReserved);

    $statementItem->save();

    _report("    created statement item\n", 2);


    # Save the reserved units someplace.
    # (Need to stash these away for post-processing)
    #
    if ($amountReserved)
    {
        my $statementItemID = $statementItem->mechanical_statement_item_id;
        $self->{reserves}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}{$statementItemID}{units} += $amountReserved;
        $self->{reserves}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}{$statementItemID}{rate} = $effectiveRate;
    }

                    

    return $share;
}



#
# _findLicensesToAdjust
#
# Find all 'new' track_license entries.
# Determine whether these new track_licenses have a controlled composition
# association.  If they do, we will make a list of affected track_licenses,
# so we can flag sales associated with those as possibly needing adjusting.
#
sub _findLicensesToAdjust
{
    my ($self) = @_;
    my %licenseMap;

    my $newTrackLicenses = RPS::DB::Item::TrackLicense::GetNewTrackLicenses();
    while ($newTrackLicenses->hasNext())
    {
        my $trackLicense = $newTrackLicenses->next();

        # I think I can skip controlled comp licenses.
        # (XXX) - Can I?  Can additional controlled comps be added after a run?
        #         What does that really mean, if anything?
        #
        next if (RPS::DB::Item::TrackLicense::kControlledComposition == $trackLicense->type);


        # (XXX) ccomp can be associated with an individual track, too, not just
        # a whole album.


        # Get the album associated with this track.
        #
        my $trackID = $trackLicense->track_id;

        my $track = RPS::DB::Item::Track->Lookup(track_id => $trackID);
        my $albumID = $track->album_id;
        next unless $albumID;


        # We want to get all the controlled comp trackLicense entries for this album.
        # (if any)
        #
        my $affectedLicenses = RPS::DB::Item::TrackLicense::GetCCompTrackLicensesForAlbum($albumID);
        while ($affectedLicenses->hasNext())
        {
            my $affectedTrackLicense = $affectedLicenses->next();
            $licenseMap{$affectedTrackLicense->track_license_id} = $affectedTrackLicense;
        }
    }       

    $self->{licensesToAdjust} = \%licenseMap;
}


sub _getStatement
{
    my ($self, $publisherID, $startDate, $endDate) = @_;
    my $timer = Common::Timer->new();

    my $statement = $self->{statements}{$publisherID};
    if (! $statement)
    {
        $statement = RPS::DB::Item::MechanicalStatement->Lookup
        (
            publisher_id => $publisherID,
            start_date => $startDate,
            end_date => $endDate
        );

        if (! $statement)
        {
            $statement = RPS::DB::Item::MechanicalStatement->Create
            (
                publisher_id => $publisherID,
                mechanical_run_id => $gRunID,
                status => RPS::DB::Item::MechanicalStatement::kStatusCreating,
                start_date => $startDate,
                end_date => $endDate
            );
            $statement->save();
        }
        else
        {
            # !!! Not sure what status are actually acceptable.
            # It occurs to me that even if we set the state to kStatusCreating,
            # we might still end up duplicating line items.
            # However, let's go ahead and allow kStatusCreating to be re-run, and assume
            # that we will have some way of catching dups at the Sale level.
            #
            if (RPS::DB::Item::MechanicalStatement::kStatusCreating != $statement->status )
            {
                # Maybe _dying_ is a bit drastic...
                #
                die "Error - You have already created a statement for publisher $publisherID for this period";
            }
        }

        $self->{statements}{$publisherID} = $statement;
    }
    return $statement;
}


sub _getStatementItem
{
    my ($mechanicalStatementID, $productID, $issueStatRateID, $saleStatRateID, $trackLicenseID, $saleID) = @_;
    my $timer = Common::Timer->new();

    my $statementItem = RPS::DB::Item::MechanicalStatementItem->Lookup
    (
        mechanical_statement_id => $mechanicalStatementID,
        track_license_id => $trackLicenseID,
        product_id => $productID,
        issue_stat_rate_id => $issueStatRateID,
        sale_stat_rate_id => $saleStatRateID
    );
                    
    if (! $statementItem)
    {
        # jpk - we need to report upc.
        # This is stored in the product table.
        # I really don't want to have to query the product table when 
        # we 'render' this statement item.
        #
        # So, I'm going to make upc a field in MechanicalStatementItem, and look up
        # the value here.
        #
        my $upc = _getUPCFromProductID($productID);

        my $service;
        if ($saleID)
        {
            my $sale = Raptor::DB::Item::Sale->Lookup(sale_id => $saleID);
            my $serviceID = $sale->service_id;
            if (! $serviceID)
            {
                $serviceID = _fileIDToService($sale->file_id);
            }
            $service = _serviceIDToName($serviceID);
        }

        $statementItem = RPS::DB::Item::MechanicalStatementItem->Create
        (
            mechanical_statement_id => $mechanicalStatementID,
            track_license_id => $trackLicenseID,
            product_id => $productID,
            issue_stat_rate_id => $issueStatRateID,
            upc => $upc,
            service => $service,
            sale_stat_rate_id => $saleStatRateID
        );
        

        # Have to save it now, so we can get the id
        #
        $statementItem->save();


        # create entry in stmt map table
        #
        if ($saleID)
        {
            my $mechSaleMap = RPS::DB::Item::SaleMechanicalStatementItemMap->Create
            (
                sale_id => $saleID,
                mechanical_statement_item_id => $statementItem->mechanical_statement_item_id(),
            );
            $mechSaleMap->save();
        }
    }

    return $statementItem;
}




sub _report
{
    my ($self, $string, $verbosity) = @_;
    $verbosity = 1 unless defined $verbosity;

    if ($self->{verbosity} >= $verbosity)
    {
        my $fh = $self->{outputFileHandle};
        print $fh $string . "\n";
    }
}



###
1;#
###
