#---------------------------------------------------------------
# ____                   _ _         ____  _                    
#|  _ \ ___  _   _  __ _| | |_ _   _/ ___|| |__   __ _ _ __ ___ 
#| |_) / _ \| | | |/ _` | | __| | | \___ \| '_ \ / _` | '__/ _ \
#|  _ < (_) | |_| | (_| | | |_| |_| |___) | | | | (_| | | |  __/
#|_| \_\___/ \__, |\__,_|_|\__|\__, |____/|_| |_|\__,_|_|  \___|
#            |___/             |___/                            
#
# 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;
