#---------------------------------------------------------------
# ____                   _ _         ____  _
#|  _ \ ___  _   _  __ _| | |_ _   _/ ___|| |__   __ _ _ __ ___
#| |_) / _ \| | | |/ _` | | __| | | \___ \| '_ \ / _` | '__/ _ \
#|  _ < (_) | |_| | (_| | | |_| |_| |___) | | | | (_| | | |  __/
#|_| \_\___/ \__, |\__,_|_|\__|\__, |____/|_| |_|\__,_|_|  \___|
#            |___/             |___/
#
# Copyright (C) 2012 RoyaltyShare, Inc.   All Rights Reserved
#---------------------------------------------------------------

package RPS::LabelRoyalty::Fast::Script::MapSales;

use strict;
use Data::Dumper;
use File::Path;
use POSIX ":sys_wait_h";

use lib '/app/tools/common/lib';
use lib '/app/tools/rps/lib';
use lib '/app/tools/raptor/lib';

use DB_File;

use Common::Util;
use Common::Log;
use Common::Parser;
use Common::WriteXML;
use Common::RSMath;

use RPS::LabelRoyalty::Fast::Static::Sales;
use RPS::LabelRoyalty::Fast::Static::Products;
use RPS::LabelRoyalty::Fast::Static::LabelTerms;
use RPS::LabelRoyalty::Fast::Static::Exceptions;

use RPS::LabelRoyalty::Fast::Mapped::Data;

use base 'Common::Script';

sub _options {
    {
        client_id => {
            short       => 'c',
            required    => 1,
            description => 'Limit to this client id',
            parameter   => 'i'
        },
        data_path => {
            short       => 'd',
            required    => 1,
            description => 'path to directory containing static data',
            parameter   => 's'
        },
        output_path => {
            short       => 'o',
            required    => 1,
            description => 'path to directory to write output files',
            parameter   => 's'
        },
        process_count => {
            short       => 'p',
            required    => 0,
            description => 'number of processes to use. defaults to 1',
            parameter   => 'i',
        },
        index_range => {
            short       => 'i',
            required    => 0,
            description => 'First,Last data indexes to process.  Ex: -s0,10000',
            parameter   => 's',
        },
    };
}

my $gChildCount;

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

    Log->warn("BEGIN");

    my ( $firstSale, $lastSale ) = $self->_parseFirstLastSaleIndex( $self->param('index_range') );

    my $dataPath = $self->param('data_path');

    my $outputDirectory = $self->param('output_path');

    # JPK - Should it be the responsibility of this process to create the directory if
    # it doesn't already exist?   It's a bit of a stretch, but then again it's convenient...
    #
    if ( !-d $outputDirectory ) {

        # !!! Might want to make this behavior optional...
        #
        mkpath($outputDirectory);
    }

    Log->info("Getting Sales");
    my $sales = RPS::LabelRoyalty::Fast::Static::Sales->GetSales( $dataPath, $firstSale, $lastSale );

    # The term data is stored as a hash of serialized array refs.
    # So we'll need to eval each record.
    # JPK - Not particularly abstract, true, but I don't want to waste time instantiating
    # some sort of wrapper object for each product.
    #
    Log->info("Getting LabelTerms");
    my $labelTermData = RPS::LabelRoyalty::Fast::Static::LabelTerms->GetLabelTermData($dataPath);

    Log->info("Getting Exceptions");
    my $exceptionData = RPS::LabelRoyalty::Fast::Static::Exceptions->GetExceptionData($dataPath);

    Log->info("Getting Products");
    my $productData = RPS::LabelRoyalty::Fast::Static::Products->GetProducts($dataPath);

    my $numSales = scalar(@$sales);
    Log->warn("Number of sales: $numSales");

    my $childCount = $self->param('process_count');
    $childCount = 1 unless $childCount;

    my $salesPerProcess = int( $numSales / $childCount );
    Log->warn("sales per process: $salesPerProcess");

    my $firstSale = 0;
    my $lastSale  = $firstSale + $salesPerProcess;

    my @childPIDs;
    $gChildCount = 0;

    for ( my $x = 0 ; $x < $childCount ; $x++ ) {
        my $pid = fork();
        if ( !$pid ) {

            # !!! There may be a bit of a race condition here.  I want to spawn all the children, then set up the parent's signal handlers.
            # !!! We might need to wait a bit before proceeding in the child to allow all that to get set up.
            #
            return $self->_doTheMapping( $firstSale, $lastSale, $outputDirectory, $sales, $productData, $labelTermData, $exceptionData );
        } else {
            $gChildCount++;

            $firstSale = $lastSale;

            # Include all remaining sales if the next child is the last one.
            #
            if ( $x == ( $childCount - 2 ) ) {
                $lastSale = $numSales;
            } else {
                $lastSale = $firstSale + $salesPerProcess;
            }
        }
    }

    $SIG{CHLD} = \&REAPER;
    $SIG{INT}  = 'IGNORE';

    while ($gChildCount) {
        sleep(2);
    }

    Log->warn("All children finished, exiting");
}

sub CTRL_C {
    Log->warn("caught a ctrl-c, exiting");
    exit(1);
}

sub REAPER {
    my $deadPid;
    while ( ( $deadPid = waitpid( -1, &WNOHANG ) ) > 0 ) {
        $gChildCount--;
    }
    $SIG{CHLD} = \&REAPER;
}

sub _doTheMapping {
    my ( $self, $firstSale, $lastSale, $outputDirectory, $sales, $productData, $labelTermData, $exceptionData ) = @_;

    # This is in the child process.
    # Install a signal handler to catch ctrl-c
    $SIG{INT} = \&CTRL_C;

    my $mappedDataFD = $self->_openMappedDataFileDescriptor($outputDirectory);

    # !!! JPK - If we ensure that the sales are sorted by product_id, then we can
    # !!! cache the last fetched product data (which would be more efficient).
    #
    my $lastProductID;
    my $lastProduct;

    Log->warn("mapping sales $firstSale up to $lastSale");
    for ( my $i = $firstSale ; $i < $lastSale ; $i++ ) {
        my $saleLine = $sales->[$i];

        Log->info( "\n\nSALE LINE:", $saleLine );

        my @saleArray = split( "\t", $saleLine );
        my $sale = \@saleArray;

        my $saleID    = $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kSaleID];
        my $productID = $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kProductID];

        # Map product id to all matching terms, then output the specifics.
        # !!! Any 'math' that needs to happen?   Exactly what will the MappedData contain?
        # !!! Probably delay any calculations until after the sort (i.e. wait until reduce step).

        my $product;
        if ( $productID != $lastProductID ) {
            my $productLine = $productData->{$productID};
            if ($productLine) {
                my @productArray = split( "\t", $productLine );
                $product = \@productArray;
            } else {
                $product = undef;
            }
            $lastProductID = $productID;
            $lastProduct   = $product;
        } else {
            $product = $lastProduct;
        }

        if ( !$product ) {
            Log->warn("Unable to find a product to match sale $saleID");
            next;
        }

        my $labelID = $product->[RPS::LabelRoyalty::Fast::Static::Products::kLabelID];

        my @labelTerms = $labelTermData->get_dup($labelID);
        foreach my $labelTermLine (@labelTerms) {
            my @labelTerm = split( ',', $labelTermLine );

            # JPK - I think at this point I can just spew out the stuff I'll need for mapping.
            # We'll need:
            # - sale_id
            # - distribution_fee - I believe this is going to be an aggregation factor. From the Term data.
            # - service_id
            # - distributor_id
            # - region_id
            # - start/end date  - These won't be used to aggregate, but we will probably want to keep min/max as we go.
            # - product_type
            # - format_type
            # - media_type
            # - track_id
            # - album_id  - So, note that product_id is NOT included...
            # - units
            # - price
            # - disc_no/track_no.   This could get... weird?   Perhaps we _should_ include product_id, and aggregate on it?
            #   ... going with the product_id for now
            #   But perhaps we should just aggregate on these two values after all.
            # - exception_id (when applicable)

            # JPK - I don't think I actually need the printf approach.

            # I don't have payee_id yet.  Seems like I ought to extract that along with the label term data.

            # Note that we are using the distributor ID (basically, the service ID from the file).
            # This may or may not be the same as the sale's service ID.
            my $serviceID   = $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kDistributorID];
            my $countryCode = $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kCountryCode];
            my $distFee     = $labelTerm[RPS::LabelRoyalty::Fast::Static::LabelTerms::kDistributionFee];
            my $groupTermID = $labelTerm[RPS::LabelRoyalty::Fast::Static::LabelTerms::kLabelContractTermID];

            # We may need to use the dist fee from one of the exceptions.
            # Loop through them for this group term and look for specific matches
            # on service AND country.
            # Next in priority would be an exception with a specific service and ALL for country.
            # Last would be ALL for service and a specific country.
            # If we don't find any of those, we'll leave the dist fee as-is.
            my @exceptions = $exceptionData->get_dup($groupTermID);

            # We need a tie breaker for when we match an "all services - specific country" exception and
            # also a "specific service - all countries" exception.  Gonna give priority to the one with
            # a specific service.
            my $matchedService   = 0;
            my $exceptionID      = 0;
            my $exceptionService = 0;
            my $exceptionCountry = 0;
            foreach my $exceptionLine (@exceptions) {
                my @exception            = split( ',', $exceptionLine );
                my $thisExceptionService = $exception[RPS::LabelRoyalty::Fast::Static::Exceptions::kServiceID];
                my $thisExceptionCountry = $exception[RPS::LabelRoyalty::Fast::Static::Exceptions::kCountryCode];
                my $thisExceptionID      = $exception[RPS::LabelRoyalty::Fast::Static::Exceptions::kExceptionID];
                my $thisExceptionDistFee = $exception[RPS::LabelRoyalty::Fast::Static::Exceptions::kDistributionFee];

                # Exact service and country matches.
                if ( $serviceID == $thisExceptionService && $countryCode eq $thisExceptionCountry ) {
                    $distFee          = $thisExceptionDistFee;
                    $exceptionID      = $thisExceptionID;
                    $exceptionService = $thisExceptionService;
                    $exceptionCountry = $thisExceptionCountry;
                    last;
                }

                if ( $serviceID == $thisExceptionService && $thisExceptionCountry eq '0' ) {
                    $distFee          = $thisExceptionDistFee;
                    $exceptionID      = $thisExceptionID;
                    $exceptionService = $thisExceptionService;
                    $matchedService   = 1;
                }

                if ( $countryCode eq $thisExceptionCountry && $thisExceptionService == 0 && $matchedService == 0 ) {
                    $distFee          = $thisExceptionDistFee;
                    $exceptionID      = $thisExceptionID;
                    $exceptionCountry = $thisExceptionCountry;
                }
            }

            print $mappedDataFD $labelTerm[RPS::LabelRoyalty::Fast::Static::LabelTerms::kLabelPayeeID] . "\t"
              . RPS::LabelRoyalty::Fast::Mapped::Data::kDataSale . "\t"
              . $labelID . "\t"
              . $labelTerm[RPS::LabelRoyalty::Fast::Static::LabelTerms::kLabelContractID] . "\t"
              . $product->[RPS::LabelRoyalty::Fast::Static::Products::kAlbumID] . "\t"
              . $product->[RPS::LabelRoyalty::Fast::Static::Products::kTrackID] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kServiceID] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kDistributorID] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kProductType] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kFormatType] . "\t"
              . (    $product->[RPS::LabelRoyalty::Fast::Static::Products::kMediaType]
                  || $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kMediaType] )
              . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kCountryCode] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kConversionRate] . "\t"
              . $distFee . "\t"
              . $productID . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kPrice] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kFree] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kUnits] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kDateBegin] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kDateEnd] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kCurrencyCode] . "\t"
              . $sale->[RPS::LabelRoyalty::Fast::Static::Sales::kSaleID] . "\t"
              . lc( substr( $product->[RPS::LabelRoyalty::Fast::Static::Products::kAlbumName], 0, 20 ) ) . "\t"
              . $product->[RPS::LabelRoyalty::Fast::Static::Products::kDiscNo] . "\t"
              . $product->[RPS::LabelRoyalty::Fast::Static::Products::kTrackNo] . "\t" . " " . "\t" . " " . "\t" . " " . "\t"
              . $exceptionID . "\t"
              . $exceptionService . "\t"
              . $exceptionCountry . "\n";
        }
    }
    Log->warn("END");
}

sub _openMappedDataFileDescriptor {
    my ( $self, $outputDirectory ) = @_;

    open OUTPUT, "> $outputDirectory/" . RPS::LabelRoyalty::Fast::Mapped::Data->FileName() . "_$$.unsorted" or die "ERROR: $!";

    binmode( *OUTPUT, ':utf8' );

    return *OUTPUT;
}

sub _openMissedSalesFileDescriptor {
    my ( $self, $outputDirectory ) = @_;

    open MISSED, "> $outputDirectory/" . RPS::LabelRoyalty::Fast::Mapped::MissedSales->FileName() . "_$$.unsorted" or die "ERROR: $!";

    return *MISSED;
}

sub _parseFirstLastSaleIndex {
    my ( $self, $rangeString ) = @_;
    return unless $rangeString;

    my @bits = split( ',', $rangeString );
    if ( 2 != scalar @bits || ( $bits[1] <= $bits[0] ) ) {
        $self->usage('ERROR: sale_range is invalid.');
    }
    return ( $bits[0], $bits[1] );
}

1;
