#!/usr/bin/perl
#------------------------------------------------------------
# Copyright (C) 2006 RoyaltyShare, Inc.   All Rights Reserved
#------------------------------------------------------------
use strict;

use File::Path;
use Data::Dumper;
use Getopt::Std;
use POSIX qw(ceil);
use POSIX qw(:sys_wait_h :signal_h :errno_h);
use Sys::Hostname;
use Carp;

use IO::Handle;

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

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

use lib '/app/tools/rps/lib';
use RPS::DB::Item::LabelRoyaltyStatementSale;
use RPS::LabelRoyalty::Job::CreatePDFStatements;
use RPS::LabelRoyalty::LabelRoyaltyRun;
use RPS::RoyaltyRun::Status;
use RPS::Statement::Label::Report;
use RPS::Statement::Label::PDF;
use RPS::DB::Item::Track;
use RPS::DB::Item::Master;
use RPS::DB::Item::ProductTrack;
use RPS::DB::Item::Album;
use RPS::DB::Item::Artist;
use RPS::DB::Item::Label;
use RPS::DB::Item::Service;
use RPS::DB::Item::Product;
use RPS::DB::Item::LabelRoyaltyRun;
use RPS::DB::Item::SaleRunMap;

#use RPS::DB::Item::SaleLabelRoyaltyRunMap;
use RPS::DB::Item::LabelContract;
use RPS::DB::Item::LabelContractTerm;
use RPS::DB::Item::LabelPayee;
use RPS::DB::Item::LabelPayeeAccount;
use RPS::DB::Item::FinanceAccount;
use RPS::DB::Item::PendingTransaction;
use RPS::DB::Item::LabelRoyaltyLabelItem;
use RPS::DB::Item::LabelRoyaltyTransactionItem;
use RPS::DB::Item::LabelRoyaltyStatement;
use RPS::DB::Item::DistributionFee;

use constant kCR => "\r\n";

# Allow tuning of the verbosity of our output.
#
my $gVerbosityLevel = 1;

# Parse the command-line options.
#
my %options;
_parseCommandLine( \%options );

# JPK - This is not my usual style, but...
# Set up some global data structures that we'll fill in as we process sales.
#
#my %gProcessedSales;

# !!! Need to combine these two hashes
#
#my %gLabelSummary;
my %gData;

# Get the hostname we are running on.
#
my $hostname = hostname();

my $gRunID = $options{runID};

# Now, we're going to fork off a child process.
# This way I can tell (and therefore log) whether we exited cleanly or because
# of some exception
#

local $SIG{CHLD} = \&REAPER;
my $gChildIsRunning = 1;
my $gChildExitCode;
my $gChildPID = fork();
if ($gChildPID) {
    _parent();
} else {
    _child();
}

sub _child {

    # Install a signal handler to catch ctrl-c.
    #
    $SIG{INT} = \&CTRL_C;

    # Instantiate the application singleton object.
    #
    my $appSingleton = Common::RSApp->new( clientID => $options{clientID} );

    # There should already be an entry in the run table, with a status of kWaitingToRun.
    #
    my $obj = RPS::DB::Item::LabelRoyaltyRun->Lookup( label_royalty_run_id => $gRunID, );
    if ( !$obj ) {
        die "ERROR: run_id $gRunID is not valid\n";
    }

    # Sanity check - make sure this run is in the correct state.
    #
    if ( RPS::RoyaltyRun::Status::kWaitingToRun != $obj->status() ) {
        die "ERROR: run_id $gRunID is not in the 'waiting to run' state\n";
    }

    # Set the start time and pid fields
    #
    $obj->start_time(Common::DB::Item::kDateTimeNow);
    $obj->pid($$);
    $obj->status(RPS::RoyaltyRun::Status::kRunning);

    $obj->save();

    my $payors = [ $obj->payor_id ];

    # Now... do the run!
    #
    _report( "*** Label Royalty Run $gRunID for client id " . $options{clientID} . " for period " . $options{label} );
    _run_royalties($payors);

    _report("*** done - Child exiting");

}

sub _parent {

    # Ctrl-c catching is rather weird with forked processes.
    # I want to ignore this signal in the parent, so that the
    # child catches it.
    # Using 'local' so that only the parent ignores the signal.
    #
    local $SIG{INT} = 'IGNORE';

    my $runID = $options{runID};

    # Wait for the child process to complete
    #
    while ($gChildIsRunning) {
        sleep(1);
    }

    _report("PARENT: child exited - child exit code = $gChildExitCode");
    my $status;
    if ( 0 == $gChildExitCode ) {
        $status = RPS::RoyaltyRun::Status::kComplete;
    } elsif ( 1 == $gChildExitCode ) {
        $status = RPS::RoyaltyRun::Status::kAborted;
    } else {
        $status = RPS::RoyaltyRun::Status::kError;
    }

    # Mark the run as complete
    #
    my $appSingleton = Common::RSApp->new( clientID => $options{clientID} );
    my $obj = RPS::DB::Item::LabelRoyaltyRun->Lookup( label_royalty_run_id => $runID );
    $obj->status($status);
    $obj->end_time(Common::DB::Item::kDateTimeNow);
    $obj->save();

    _report("PARENT: exiting");
}

sub REAPER {
    my $deadChildPID = waitpid( -1, &WNOHANG );

    if ( -1 == $deadChildPID ) {

        # no child - ignore this
    } elsif ( WIFEXITED($?) ) {

        # The child really exited.
        # Let's find out why.
        #
        $gChildExitCode  = $? >> 8;
        $gChildIsRunning = 0;
    }
    $SIG{CHLD} = \&REAPER;
}

sub CTRL_C {
    _report("caught a ctrl-c, aborting");
    exit(1);
}

# ----------------------------------------------------------------------------------

sub _run_royalties {
    my ($payors) = @_;

    #
    # ---  Process Sales
    #
    _report("calling _processSales");
    _processSales($payors);

    #
    # --- Generate the statements.
    #
    _report("calling _generateStatements");
    _generateStatements($payors);

    # Go forth and create pdf files, I Command You!
    # !!! This is using a lot of CPU...  Let's nice the process.
    #
    _report("going to create job for PDFs");
    my $statementsPath = RPS::Statement::Label::PDF::StatementPathFromRunID( $gRunID, $options{clientID} );
    if ( !-d $statementsPath ) {
        mkpath($statementsPath) or die "ERROR: Unable to create path $statementsPath: $!\n";
    }

    my $jobArgs = RPS::LabelRoyalty::Job::CreatePDFStatements->new(
        runID    => $gRunID,
        clientID => $options{clientID},
        filePath => $statementsPath,
    );
    my $job = $jobArgs->enqueue();

    # Clean up - We can get rid of the entries in the LabelRoyaltyStatementSale table
    #
    RPS::DB::Item::LabelRoyaltyStatementSale->DeleteAllByLabelRoyaltyRunID($gRunID);

}

sub _processSales {
    my ($payors) = @_;

    my %payorIDMap;
    foreach my $payorID (@$payors) {
        $payorIDMap{$payorID} = 1;
    }

    my $run = new RPS::LabelRoyalty::LabelRoyaltyRun( labelRoyaltyRunID => $gRunID );

    _report("--- processing sales");
    my $sales = Raptor::DB::Item::Sale->GetUnprocessedLabelSales( ending_sale_date => $run->EndingSaleDate() );

    _report( "unprocess sales count: " . $sales->size, 2 );

    my %badProducts;

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

        _report( "SALE: " . Dumper($sale), 4 );

        # !!! So, how does this work?
        # !!! Seems like I have to map this sale to the label_id of a label contract term.
        # !!!  I go from product->album->label->term
        #
        # !!! I will then need to calculate for each sale the 'gross sales' amount.
        # !!! This will probably use the same basic logic we use for artist royalties:
        # !!!  - there will be different logic for unit-based sales vs net revenue sales.
        #

        # --> Basically, I will be outputting a line for each sale.
        # --> In addition, I need to accumulate gross sales, dist fee, and net sales
        #     by label id.
        #     I think I'll use a table for the summary, but just a simple file for the
        #     output file.
        # ... No, I will use some tables for both. I need to be able to re-generate the
        #     output file if necessary.
        #my %gLabelSummary;
        #my %gProcessedSales;

        my $productID = $sale->product_id;

        # We need the album_id, and the track_id if this is a track sale.
        # --> This is mostly just to make sure everything is _active_.
        #
        my $albumID;
        my $trackID;

        my $missingProductPriceFlag = 0;
        my $product;
        if ($productID) {
            $product = RPS::DB::Item::Product->Lookup( product_id => $productID );
            if ( !$product ) {
                _report( "skipping sale_id " . $sale->sale_id . " : product $productID cannot be found" );
                my $mi = RPS::DB::Item::SaleRunMap->Create(
                    sale_id  => $sale->sale_id,
                    run_id   => $gRunID,
                    run_type => RPS::DB::Item::SaleRunMap::kRunTypeLabelRoyalty,
                    status   => RPS::DB::Item::SaleRunMap::kStatusBadProductID,
                );
                $mi->save();
                next;
            }

            # Skip inactive products.
            #
            if ( RPS::DB::Item::Product::kProductStatusActive != $product->product_status_id ) {
                _report( "skipping sale_id " . $sale->sale_id . " : product $productID is not active" );
                my $mi = RPS::DB::Item::SaleRunMap->Create(
                    sale_id  => $sale->sale_id,
                    run_id   => $gRunID,
                    run_type => RPS::DB::Item::SaleRunMap::kRunTypeLabelRoyalty,
                    status   => RPS::DB::Item::SaleRunMap::kStatusInactiveProduct,
                );
                $mi->save();
                next;
            }

            ( $albumID, $trackID ) = _getAlbumTrackFromProduct($product);
        } else {
            _report( "skipping sale_id " . $sale->sale_id . " : no product id " );
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id  => $sale->sale_id,
                run_id   => $gRunID,
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeLabelRoyalty,
                status   => RPS::DB::Item::SaleRunMap::kStatusBadProductID,
            );
            $mi->save();
            next;
        }

        # Check the album's 'inactive' flag.
        #
        my $albumData = RPS::DB::Item::Album->Lookup( album_id => $albumID );
        if ( $albumData->inactive() || $albumData->status() == 0 ) {
            _report( "skipping sale_id " . $sale->sale_id . " : album $albumID is inactive" );
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id  => $sale->sale_id,
                run_id   => $gRunID,
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeLabelRoyalty,
                status   => RPS::DB::Item::SaleRunMap::kStatusInactiveAlbum,
            );
            $mi->save();
            next;
        }

        # We'll need to check for the distributor name (if there is one)...
        #
        # This gets a little complicated, so I'll try to explain it.
        #
        # If there is a service_id in the sale, this file came from a distributor.
        # That means the service_id in the sale is for the service and the service_id in the file is for the distributor.
        #
        # If there is _not_ a service_id in the sale, that means the file came from the service.
        # In that case, there is no distributor and the service_id in the file is for the service.

        # First let's see if the sale has a service_id.
        my $saleServiceID = $sale->service_id;
        my $fileData = Raptor::DB::Item::File->Lookup( file_id => $sale->file_id );
        my $distributor;
        my $distributorName;
        my $service;

        if ($saleServiceID) {

            # If the sale has a service_id, then we'll use that to find the service
            # and we'll use the file's service_id to find the distributor.

            $service = RPS::DB::Item::Service->Lookup( service_id => $saleServiceID );
            my $distributorID = $fileData->service_id;

            die "ERROR - could not determine service id from file " . $sale->file_id unless $distributorID;

            $distributor = RPS::DB::Item::Service->Lookup( service_id => $distributorID );
            $distributorName = $distributor->service_name;
        } else {

            # If there is not a service_id in sale, there is no distributor.
            # So, we just need to figure out the service.

            my $serviceID = $fileData->service_id;

            die "ERROR - could not determine service id from file " . $sale->file_id unless $serviceID;

            $service = RPS::DB::Item::Service->Lookup( service_id => $serviceID );
        }

        # We've got the product ID, now determine the label id
        #
        my $labelID = $albumData->label_id;
        my $label = RPS::DB::Item::Label->Lookup( label_id => $labelID );

        # The details on how we handle physical vs. digital sales will differ.
        # !!! For now, we're going to skip physical sales...
        #
        my $units;
        my $price;
        my $grossSales;

        my $productTypeID = $product->product_type_id;
        if (   RPS::DB::Item::Product::kProductTypeDigital == $productTypeID
            || RPS::DB::Item::Product::kProductTypeDigitalTrack == $productTypeID ) {

            # Digital
            #
            $units = $sale->units;
            $price = $sale->price;

            # !!! If the _file_ has a dist fee, that reduces the $price...
            #
            my $dist_fee = RPS::DB::Item::DistributionFee->GetDistFeeBySale( file_id => $sale->file_id, sale_id => $sale->sale_id );
            if ( $dist_fee > 1 ) {
                $price = Common::RSMath::round( $price * ( ( 100 - $dist_fee ) / 100 ), 8 );
            }
            $grossSales = Common::RSMath::round( $units * $price, 8 );
        } else {
            _report( "skipping sale_id " . $sale->sale_id . " : not a digital product" );
        }

        # Some last formatting stuff.
        # Our spec only specifies yyyy-mm, for some reason.
        #
        my $dateStart = _formatOutputDate( $sale->date_begin );
        my $dateEnd   = _formatOutputDate( $sale->date_end );
        my $artist    = RPS::DB::Item::Artist->Lookup( artist_id => $albumData->artist_id );

        my $isrc;
        my $clientTrackID;
        my $discNo;
        my $trackNo;
        my $trackName;
        my $trackArtist;
        my $trackCustom1;
        my $trackCustom2;
        my $trackCustom3;

        my $upc         = $product->upc_ean;
        my $releaseDate = $product->release_date;

        if ($trackID) {
            my $track = RPS::DB::Item::Track->Lookup( track_id => $trackID );
            $clientTrackID = $track->client_track_id;
            $trackName     = $track->title;
            $trackCustom1  = $track->custom_1;
            $trackCustom2  = $track->custom_2;
            $trackCustom3  = $track->custom_3;

            my $master = RPS::DB::Item::Master->Lookup( master_id => $track->master_id );
            $isrc = $master->isrc;

            my $tArtist = RPS::DB::Item::Artist->Lookup( artist_id => $track->artist_id );
            $trackArtist = $tArtist->name;

            # So, there won't _be_ a product_track entry for a digital track product.
            # In order to fill in the somewhat pointless and misleading disc-no and track-no fields,
            # we'll have to look up the digital album product that matches the album this digital
            # track product appears on.
            # wee
            #
            my $productTrack;
            if ( RPS::DB::Item::Product::kProductTypeDigitalTrack == $productTypeID ) {
                my $albumProduct = RPS::DB::Item::Product->Lookup(
                    asset_id        => $albumData->album_id,
                    product_type_id => RPS::DB::Item::Product::kProductTypeDigital
                );

                if ($albumProduct) {
                    $productTrack = RPS::DB::Item::ProductTrack->Lookup( product_id => $albumProduct->product_id, track_id => $trackID );

                    # Get the album product's upc and release date if necessary
                    #
                    $upc         = $albumProduct->upc_ean      unless $upc;
                    $releaseDate = $albumProduct->release_date unless $releaseDate;
                }
            } else {
                $productTrack = RPS::DB::Item::ProductTrack->Lookup( product_id => $productID, track_id => $trackID );
            }

            if ($productTrack) {
                $discNo  = $productTrack->disc_number;
                $trackNo = $productTrack->disc_track;
            } else {
                _report( "WARNING: no product_track entry for product_id $productID", 3 );
            }
        }

        # Fetch the contract terms that match this label.
        #
        my $termsMatched = 0;
        my $terms        = RPS::DB::Item::LabelContractTerm->GetByLabelID($labelID);
        while ( my $term = $terms->next() ) {

            # Fetch the contract, and from that the payee.
            #
            my $contract = RPS::DB::Item::LabelContract->Lookup( label_contract_id => $term->label_contract_id );
            my $payeeID  = $contract->label_payee_id;
            my $payee    = RPS::DB::Item::LabelPayee->Lookup( label_payee_id => $payeeID );

            my $payorID = $contract->payor_id;

            # Skip this term if the payor doesn't match...
            #
            if ( !$payorIDMap{ $contract->payor_id } ) {
                next;
            }

            # So... have more or less all we need now to populate both the sale hash and the payee summary hash.
            #
            my $feePercent = $term->distribution_fee;
            my $fee        = Common::RSMath::round( $grossSales * ( $feePercent / 100 ), 8 );
            my $netSales   = $grossSales - $fee;

            my $basePrice      = Common::RSMath::round( $price * $sale->conversion_rate,      8 );
            my $baseGrossSales = Common::RSMath::round( $grossSales * $sale->conversion_rate, 8 );
            my $baseNetSales   = Common::RSMath::round( $netSales * $sale->conversion_rate,   8 );
            my $baseFee        = Common::RSMath::round( $fee * $sale->conversion_rate,        8 );

            _report(
" Creating gData record: payorID $payorID, payeeID $payeeID, labelID $labelID, grossSales $baseGrossSales, fee $baseFee, feePct $feePercent, netSales $baseNetSales",
                3
            );

            # !!! No reason to store this stuff in a hash.
            # Lookup or create a label_royalty_label_item here instead.
            #
            my $labelRoyaltyLabelItem = _getLabelRoyaltyLabelItem( $payorID, $payeeID, $labelID );

            $labelRoyaltyLabelItem->gross_sales( $labelRoyaltyLabelItem->gross_sales() + $baseGrossSales );
            $labelRoyaltyLabelItem->net_sales( $labelRoyaltyLabelItem->net_sales() + $baseNetSales );
            $labelRoyaltyLabelItem->distribution_fee($feePercent);
            $labelRoyaltyLabelItem->fee( $labelRoyaltyLabelItem->fee() + $baseFee );

            if (   $labelRoyaltyLabelItem->label_contract_id()
                && $labelRoyaltyLabelItem->label_contract_id() != $term->label_contract_id ) {
                die "ERROR - label $labelID - contract id "
                  . $term->label_contract_id . " != "
                  . $gData{$payorID}{$payeeID}{summary}{$labelID}{contractID};
            }
            $labelRoyaltyLabelItem->label_contract_id( $term->label_contract_id );
            $labelRoyaltyLabelItem->save();

            # Make the SaleRunMap entry here, since we have a label item id and a sale id...
            #
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id           => $sale->sale_id,
                run_id            => $gRunID,
                statement_item_id => $labelRoyaltyLabelItem->label_royalty_label_item_id,
                run_type          => RPS::DB::Item::SaleRunMap::kRunTypeLabelRoyalty,
                status            => RPS::DB::Item::SaleRunMap::kStatusPaid,
            );
            $mi->save();

            # !!! No reason to track these elsewhere - Just create a row in the label_royalty_run_sale table.
            #            $gData{$payorID}{$payeeID}{summary}{$labelID}{saleIDs}{$sale->sale_id} = 1;
            my $labelRoyaltyStatement = _getLabelRoyaltyStatement( $payorID, $payeeID );

            my $statementSale = RPS::DB::Item::LabelRoyaltyStatementSale->Create(
                label_royalty_statement_id => $labelRoyaltyStatement->label_royalty_statement_id,    # superceeds payor and payee ids.
                sale_id                    => $sale->sale_id,
                label_name                 => $label->label_name,
                service_id                 => $service->service_id,
                service_name               => $service->service_name,
                distributor                => $distributorName,
                territory                  => $sale->country_code,
                period_begin               => $dateStart,
                period_end                 => $dateEnd,
                product_type               => $sale->product_type,
                product_format             => $sale->format_type,
                catalog_id                 => $albumData->catalog_number,
                upc                        => $upc,
                release_date               => $releaseDate,
                album_id                   => $albumData->client_album_id,
                album_name                 => $albumData->title,
                album_artist               => $artist->name,
                upc_alt                    => $product->upc_alt,
                album_custom_1             => $albumData->custom_1,
                album_custom_2             => $albumData->custom_2,
                album_custom_3             => $albumData->custom_3,
                isrc                       => $isrc,
                track_id                   => $clientTrackID,
                disc_no                    => $discNo,
                track_no                   => $trackNo,
                track_name                 => $trackName,
                track_artist               => $trackArtist,
                track_custom_1             => $trackCustom1,
                track_custom_2             => $trackCustom2,
                track_custom_3             => $trackCustom3,
                units                      => $units,
                unit_price                 => $price,
                ext_price                  => $grossSales,
                currency_code              => $sale->currency_code,
                currency_conv              => $sale->conversion_rate,
                base_unit_price            => $basePrice,
                base_ext_price             => $baseGrossSales,
                free                       => $sale->free,
                dist_fee                   => $feePercent / 100,
                base_net_price             => $baseNetSales,
                media_type                 => $sale->media_type,
            );
            $statementSale->save();

            #            push @{$gData{$payorID}{$payeeID}{sales}}, \%row;
        }

        # ... on to the next sale...
        #

        if ( 0 == $termsMatched ) {
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id  => $sale->sale_id,
                run_id   => $gRunID,
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeLabelRoyalty,
                status   => RPS::DB::Item::SaleRunMap::kStatusNoContractTerm,
            );
            $mi->save();
        }
    }
}

sub _generateStatements {
    my ($payorList) = @_;

    my %payorIDMap;
    foreach my $payorID (@$payorList) {
        $payorIDMap{$payorID} = 1;
    }

    _report("--- generating statements");

    #push @{$gReportLines{$payorID}{$payeeID}}, \%row;

    # Need to fetch balances, apply transactions, etc...
    #

    # Fetch the current balances for _all_ label payees, then add blank entries to the main
    # data structure for those artists that aren't currently in there.
    # This will result in our producing statements for payees that have balances but no sales.
    # (which is what we want).
    #
    # !!! So, we've already _created_ statements for everybody else.
    # !!! Therefore we should just create blank statements for these guys, too.
    #
    _report( "--- fetching label payee balances", 2 );
    my $payeePreviousBalanceHash = _getLabelPayeeBalances();
    foreach my $payorID ( keys %$payeePreviousBalanceHash ) {
        next unless ( $payorIDMap{$payorID} );

        my $payeeIDHash = $payeePreviousBalanceHash->{$payorID};
        foreach my $payeeID ( keys %$payeeIDHash ) {

            # Statement has already been saved.  This call will create a new one if necessary.
            # But there is no reason to call 'save' here.
            #
            my $statement = _getLabelRoyaltyStatement( $payorID, $payeeID );

            #            if (! exists $gData{$payorID}{$payeeID})
            #            {
            #                _report("!! creating an 'empty' spot for payorID $payorID, payeeID $payeeID, due to a previous balance", 3);
            #                $gData{$payorID}{$payeeID}{summary} = {};
            #            }
        }
    }

    # Now, we can create both the summary data _and_ the sales report
    #
    _report( "--- creating summary data", 2 );

    # So rather than looping through payor and payee, we'll iterate over all the Statements.
    #
    my $allStatements = RPS::DB::Item::LabelRoyaltyStatement->GetByLabelRoyaltyRunID($gRunID);
    while ( my $statement = $allStatements->next() ) {
        my $statementID = $statement->label_royalty_statement_id();
        my $payorID     = $statement->payor_id();
        my $payeeID     = $statement->payee_id();

        my $payeeData = RPS::DB::Item::LabelPayee->Lookup( label_payee_id => $payeeID );

        # If the payee doesn't exist anymore, or is inactive, we'll delete their statement.
        # !!! Don't need to delete the 'sales' lines - We'll blow all of those away at once at the end.
        #
        if ( !$payeeData || RPS::DB::Item::LabelPayee::kStatusInactive == $payeeData->status ) {
            _report( "label payee $payeeID is inactive or deleted - skipping", 3 );
            $statement->delete();
            next;
        }

        my $payeePreviousBalance     = $payeePreviousBalanceHash->{$payorID}{$payeeID}{balance};
        my $payeePendingTransactions = $payeePreviousBalanceHash->{$payorID}{$payeeID}{pending};
        my $minPayment               = $payeePreviousBalanceHash->{$payorID}{$payeeID}{minPayment};

        # Sum up all the seperate Label sections.
        #
        my $statementSubtotal = 0;

        my $labelItems = RPS::DB::Item::LabelRoyaltyLabelItem->GetByLabelRoyaltyStatementID($statementID);
        while ( my $labelItem = $labelItems->next() ) {
            $statementSubtotal += $labelItem->net_sales();
        }

        # Add up the transactions.
        #
        my $transactionSubtotal = 0;
        if ($payeePendingTransactions) {
            while ( my $transaction = $payeePendingTransactions->next() ) {
                $transactionSubtotal += $transaction->amount;
                my $transactionItem = RPS::DB::Item::LabelRoyaltyTransactionItem->Create(
                    label_royalty_statement_id => $statementID,
                    amount                     => $transaction->amount,
                    memo                       => $transaction->memo,
                    check_number               => $transaction->check_number,
                    pending_transaction_id     => $transaction->pending_transaction_id,
                    transaction_date           => $transaction->transaction_date,
                    type_code                  => $transaction->type_code,
                );
                $transactionItem->save();
            }
        }

        # !!! Here, at the last moment, we'll round to the nearest penny.
        #
        $minPayment           = Common::RSMath::round( $minPayment,           2 );
        $payeePreviousBalance = Common::RSMath::round( $payeePreviousBalance, 2 );
        $statementSubtotal    = Common::RSMath::round( $statementSubtotal,    2 );
        $transactionSubtotal  = Common::RSMath::round( $transactionSubtotal,  2 );

        # Finalize the statement
        #
        my $payeeBalance = $payeePreviousBalance + $statementSubtotal + $transactionSubtotal;

        # see if the minimum payment threshold has been breached.
        #
        my $amountDue = 0;
        if ( $payeeBalance >= $minPayment ) {
            $amountDue = $payeeBalance;
        }

        # If this payee is 'on_hold', then we tag the statement accordingly, and
        # set amount_due to 0.
        #
        my $onHold = 0;
        if ( RPS::DB::Item::LabelPayee::kStatusOnHold == $payeeData->status ) {
            $onHold    = 1;
            $amountDue = 0;
        }

        # Save the grand total
        #
        $statement->previous_balance($payeePreviousBalance);
        $statement->min_payment($minPayment);
        $statement->balance($payeeBalance);
        $statement->total($statementSubtotal);
        $statement->amount_due($amountDue);
        $statement->on_hold($onHold);
        $statement->transaction_subtotal($transactionSubtotal);

        $statement->save();

        my $locale         = Common::Client::Current()->Locale();
        my $currencyCodeLC = lc( $locale->currencyFormat()->currencyCode() );

        _report( "  creating the sales report file", 2 );

        my $salesLines = RPS::DB::Item::LabelRoyaltyStatementSale->GetAllByLabelRoyaltyStatementID($statementID);

        #            my $salesLines = $payeeIDHash->{$payeeID}{sales};

        my $outFilePath = RPS::Statement::Label::Report::ReportPathFromRunID($gRunID);
        if ( !-d $outFilePath ) {
            mkpath($outFilePath) or die "ERROR: Unable to create path $outFilePath: $!\n";
        }

        my $fileName = RPS::Statement::Label::Report::ReportFileNameFromID($statementID);
        my $filePath = $outFilePath . $fileName;

        open( REPORT_FILE, "> $filePath" ) or die "ERROR - unable to open report file $filePath: $!";
        binmode( REPORT_FILE, ':utf8' );

        # Output a header line
        #
        print REPORT_FILE 'label-name' . "\t"
          . 'service-id' . "\t"
          . 'service-name' . "\t"
          . 'distributor' . "\t"
          . 'territory' . "\t"
          . 'period-begin' . "\t"
          . 'period-end' . "\t"
          . 'product-type' . "\t"
          . 'product-format' . "\t"
          . 'catalog-id' . "\t" . 'upc' . "\t"
          . 'album-id' . "\t"
          . 'album-name' . "\t"
          . 'album-artist' . "\t"
          . 'release-date' . "\t"
          . 'upc-alt' . "\t"
          . 'album-custom-1' . "\t"
          . 'album-custom-2' . "\t"
          . 'album-custom-3' . "\t" . 'isrc' . "\t"
          . 'track-id' . "\t"
          . 'disc-no' . "\t"
          . 'track-no' . "\t"
          . 'track-name' . "\t"
          . 'track-artist' . "\t"
          . 'track-custom-1' . "\t"
          . 'track-custom-2' . "\t"
          . 'track-custom-3' . "\t" . 'units' . "\t"
          . 'unit-price' . "\t"
          . 'ext-price' . "\t"
          . 'currency-code' . "\t"
          . 'currency-conv' . "\t"
          . $currencyCodeLC
          . '-unit-price' . "\t"
          . $currencyCodeLC
          . '-ext-price' . "\t" . 'free' . "\t"
          . 'dist-fee' . "\t"
          . $currencyCodeLC
          . '-net-price' . "\t"
          . 'media_type'
          . kCR;

        if ( $salesLines && $salesLines->size() > 0 ) {
            while ( my $salesLine = $salesLines->next() ) {

                # Need to display 'free' as Y or N
                #
                my $free;
                if ( $salesLine->{'free'} == 0 ) {
                    $free = 'N';
                } else {
                    $free = 'Y';
                }

                print REPORT_FILE $salesLine->label_name . "\t"
                  . $salesLine->service_id . "\t"
                  . $salesLine->service_name . "\t"
                  . $salesLine->distributor . "\t"
                  . $salesLine->territory . "\t"
                  . $salesLine->period_begin . "\t"
                  . $salesLine->period_end . "\t"
                  . $salesLine->product_type . "\t"
                  . $salesLine->product_format . "\t"
                  . $salesLine->catalog_id . "\t"
                  . $salesLine->upc . "\t"
                  . $salesLine->album_id . "\t"
                  . $salesLine->album_name . "\t"
                  . $salesLine->album_artist . "\t"
                  . $salesLine->release_date . "\t"
                  . $salesLine->upc_alt . "\t"
                  . $salesLine->album_custom_1 . "\t"
                  . $salesLine->album_custom_2 . "\t"
                  . $salesLine->album_custom_3 . "\t"
                  . $salesLine->isrc . "\t"
                  . $salesLine->track_id . "\t"
                  . $salesLine->disc_no . "\t"
                  . $salesLine->track_no . "\t"
                  . $salesLine->track_name . "\t"
                  . $salesLine->track_artist . "\t"
                  . $salesLine->track_custom_1 . "\t"
                  . $salesLine->track_custom_2 . "\t"
                  . $salesLine->track_custom_3 . "\t"
                  . $salesLine->units . "\t"
                  . $salesLine->unit_price . "\t"
                  . $salesLine->ext_price . "\t"
                  . $salesLine->currency_code . "\t"
                  . $salesLine->currency_conv . "\t"
                  . $salesLine->base_unit_price . "\t"
                  . $salesLine->base_ext_price . "\t"
                  . $free . "\t"
                  . $salesLine->dist_fee . "\t"
                  . $salesLine->base_net_price . "\t"
                  . $salesLine->media_type
                  . kCR;
            }
        } else {
            print REPORT_FILE "No Sales this period" . kCR;
        }
        close REPORT_FILE;
    }
    _report( "--- done generating statements", 2 );
}

# Pass dates in 'yyyy-mm-dd' format.
#
sub _parseCommandLine {
    my ($settings) = @_;

    my %opt;
    getopts( 'r:c:V:', \%opt );

    if ( !$opt{r} || !$opt{c} ) {
        _usage();
        exit(1);
    }
    $settings->{runID}    = $opt{r};
    $settings->{clientID} = $opt{c};

    if ( defined $opt{V} ) {
        $gVerbosityLevel = $opt{V};
    }
}

sub _usage {
    print "\nusage: $0 -r run_id [-V n]\n";
    print "\n";
    print "Arguments:\n";
    print "\t-r <run_id>\t\tThe royalty_run id to run.\n";
    print "\t-c <client_id>\t\tThe client_id this run belongs to.\n";
    print "\t-V <N>\t\t\tVerbosity level - 0 means no output\n";
}

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

    if ( $gVerbosityLevel >= $verbosity ) {
        Common::Log::Print($string);
    }
}

sub _getAlbumTrackFromProduct {
    my ($product) = @_;

    my ( $albumID, $trackID );

    if ( RPS::DB::Item::Product::kProductTypeDigitalTrack == $product->product_type_id ) {
        $trackID = $product->asset_id;
        my $trackData = RPS::DB::Item::Track->Lookup( track_id => $trackID );
        $albumID = $trackData->album_id;
    } else {
        $albumID = $product->asset_id;
    }

    return ( $albumID, $trackID );
}

sub _formatOutputDate {
    my ($inDate) = @_;

    my @bits = split( '-', $inDate );
    return $bits[0] . '-' . $bits[1];
}

sub _formatNumber {
    my ($value) = @_;

    $value = Common::Client::Current()->Locale()->formatNumber($value);

    return $value;
}

sub _getLabelPayeeBalances {
    my %resultHash;

    my $allAccountMappings = RPS::DB::Item::LabelPayeeAccount->GetAll();
    while ( my $accountMap = $allAccountMappings->next() ) {

        # Get the current balance for this account.
        # !!! I'd like to make an entry in this hash if this guy has _pending transactions_, too.
        #
        my $currentBalance;
        my $hasPendingTransactions;
        my $pendingTransactions;
        if ( $accountMap->finance_account_id ) {
            $currentBalance = RPS::DB::Item::FinanceAccount->CurrentBalance( $accountMap->finance_account_id );
            $pendingTransactions =
              RPS::DB::Item::PendingTransaction->GetAccountTransactions( finance_account_id => $accountMap->finance_account_id );
        }
        if ( $currentBalance != 0 || ( $pendingTransactions && $pendingTransactions->size() > 0 ) ) {
            $resultHash{ $accountMap->payor_id }{ $accountMap->label_payee_id }{balance} = $currentBalance;
            if ( $pendingTransactions && $pendingTransactions->size() > 0 ) {
                $resultHash{ $accountMap->payor_id }{ $accountMap->label_payee_id }{pending} = $pendingTransactions;
            }
        }
        $resultHash{ $accountMap->payor_id }{ $accountMap->label_payee_id }{minPayment} = $accountMap->min_payment;
    }

    #    _report("_getLabelPayeeBalances: " . Dumper(\%resultHash), 4);
    return \%resultHash;
}

sub _uberSort {
    my $result = uc( $a->{'label-name'} ) cmp uc( $b->{'label-name'} );
    if ( !$result ) {
        $result = $a->{'service-id'} <=> $b->{'service-id'};
    }
    if ( !$result ) {
        $result = uc( $a->{'territory'} ) cmp uc( $b->{'territory'} );
    }
    if ( !$result ) {
        $result = uc( $a->{'product-format'} ) cmp uc( $b->{'product-format'} );
    }
    if ( !$result ) {
        $result = uc( $a->{'album-name'} ) cmp uc( $b->{'album-name'} );
    }
    if ( !$result ) {
        $result = uc( $a->{'disc-no'} ) cmp uc( $b->{'disc-no'} );
    }
    if ( !$result ) {
        $result = uc( $a->{'track-no'} ) cmp uc( $b->{'track-no'} );
    }

    return $result;
}

sub _getLabelRoyaltyStatement {
    my ( $payorID, $payeeID ) = @_;

    my $statement = RPS::DB::Item::LabelRoyaltyStatement->Lookup(
        payee_id             => $payeeID,
        payor_id             => $payorID,
        label_royalty_run_id => $gRunID
    );
    if ( !$statement ) {
        $statement = RPS::DB::Item::LabelRoyaltyStatement->Create(
            payee_id             => $payeeID,
            payor_id             => $payorID,
            label_royalty_run_id => $gRunID,
        );
        $statement->save();
    }
    return $statement;
}

sub _getLabelRoyaltyLabelItem {
    my ( $payorID, $payeeID, $labelID ) = @_;

    # Fetch the statement associated with this stuff.
    #
    my $statement = _getLabelRoyaltyStatement( $payorID, $payeeID );
    my $statementID = $statement->label_royalty_statement_id;

    my $labelItem = RPS::DB::Item::LabelRoyaltyLabelItem->Lookup(
        label_royalty_statement_id => $statementID,
        label_id                   => $labelID,
    );
    if ( !$labelItem ) {
        $labelItem = RPS::DB::Item::LabelRoyaltyLabelItem->Create(
            label_royalty_statement_id => $statementID,
            label_id                   => $labelID,
        );
        $labelItem->save();
    }
    return $labelItem;
}
