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

package RPS::Sale::Report::EMIPulse;
use strict;
use IO::File;
use Date::Calc;
use File::stat;

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

use RPS::DB::Item::ReportQueries::EMIPulse;
use Common::Client;
use Common::Assert;

use base 'Common::XMLObject';

# JPK - We don't really have a small, well-abstracted base class for any of these asynchronous reports.
# I wish we did, but we don't.
#
# I don't want to spend a ton of time dreaming up the perfect interface.
#
# I think we'll want to have an instance of a 'Sale::Report' object (which is not the current model at all).
# Needs a period_id parameter.
#
# !!! The current model has everything stuffed into a single file, RPS::Sale::Report.
# !!! There is a table that maps a report 'name' (i.e. 'royalty' or 'reconcilliation') to the report code, sort of.
# !!! It's pretty wacky, in my opinion.

# I can't help myself, this code is gross.   So I will start with what I think is a cleaner model for a 'sale report'.
# The current RPS::Report class is structured around web-displayed, paginated reports where the data lives in a table.
# That's not what we're doing here.
# The Report::Dynamic stuff is also totally different.... although perhaps I can structure that?

# !!! These public methods are named the same as the public method in the Report::Dynamic class.
# !!! That is deliberate...
#
# !!! I'd like to transform this into a base class, and refactor the old Report code to use this model.
#

# We may want/need to have this file zipped up.
# So let's keep that in mind...
#

use constant kGSSReportingEntity => 'RS';                        # <-- just a placeholder for now.
use constant REPORT_BASE_DIR     => '/app/shared/sale_report';
use constant kZIPPath            => '/usr/bin/zip';

sub _init {
    my ( $self, %args ) = @_;
    assert( defined $args{periodID} );
    $self->{periodID} = $args{periodID};

    # The fixed width format is rather challenging to audit.
    # I'll provide an option that allow us to use tab delimiting.
    #
    if ( $args{tabDelimited} ) {
        $self->{_useTabs} = 1;
    }

    # We might want to make the ZIP archiving optional
    #
    if ( $args{skipZip} ) {
        $self->{_skipZip} = 1;
    }

    return $self;
}

sub periodID {
    my ($self) = @_;
    return $self->{periodID};
}

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

    # filename ought to be the name of the file as sent to the user.
    # which may or may not be what we call it on our filesystem.
    #
    #    my $fullPath = $self->_internalFullPath();
    my $fullPath = $self->_filePath . '/' . $self->filename();

    return ( -e $fullPath );
}

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

    if ( !$self->{_filePath} ) {
        $self->{_filePath} = join( '/', REPORT_BASE_DIR, Common::Client::Current()->ClientNameClean() );
    }

    return $self->{_filePath};
}

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

    return $self->_filePath() . '/' . $self->_internalFilename();
}

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

    # !!! For now, these can be the same.
    #
    #    return $self->filename();

    # Let's go with something different.
    # We will want to zip up the final file.
    #
    return $self->periodID() . '_Pulse.txt';
}

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

    # Just a guess for now...
    #
    if ( $self->{_skipZip} ) {
        return $self->_internalFilename();
    }

    return $self->periodID() . '_Pulse.zip';
}

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

    return stat( $self->fullPath() )->size();
}

sub fullPath {
    my ($self) = @_;
    my $fullPath = $self->_filePath . '/' . $self->filename();

    return $fullPath;
}

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

    unlink $self->_internalFullPath();
    unlink $self->fullPath();
}

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

    # Might want to just silently fail, but for now let's throw an exception.
    #
    die "ERROR - report file already exists" if $self->exists();

    $self->_create();
}

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

    $self->_openFileForWriting();

    $self->_writeHeader();

    $self->_writeReportData();

    $self->_writeFooter();

    $self->closeFile();
}

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

    my $fullPath = $self->_internalFullPath();

    $self->{_fh} = IO::File->new("> $fullPath")
      or die "ERROR - unable to open $fullPath for writing: $!";

    binmode( $self->{_fh}, ':utf8' );
}

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

    my $fullPath = $self->fullPath();

    $self->{_fh} = IO::File->new("< $fullPath")
      or die "ERROR - unable to open $fullPath for reading: $!";
}

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

    if ( $self->{_fh} ) {
        close $self->{_fh};
        $self->{_fh} = undef;

        # Compress the file.
        #
        if ( !$self->{_skipZip} && -e $self->_internalFullPath() ) {
            my $command = kZIPPath . " -j " . $self->fullPath() . ' ' . $self->_internalFullPath();
            my $rVal    = system($command);
            if ( !$rVal ) {

                # 0 == success...
                #
                unlink $self->_internalFullPath();
            } else {
                die "ERROR - Unable to compress " . $self->_internalFullPath() . ": exit code $rVal:  $!";
            }
        }
    }
}

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

    my $time = time;
    my ( $year, $month, $day, $hour, $min, $sec, $doy, $dow, $dst ) = Date::Calc::Localtime($time);
    my $fileDate = sprintf( "%02d%02d%02d", $year, $month, $day );

    $self->_writeTextPadded( $time,     13 );
    $self->_writeTextPadded( $fileDate, 6 );

    # !!! No clue what the 'GSS Reporting Entity' is yet...
    #
    $self->_writeTextPadded( kGSSReportingEntity, 2 );

    $self->_writeTextPadded( 'HDR', 3 );

    # And the fun 371 characters of wasted space...
    #
    $self->_writeTextPadded( '',  371 );
    $self->_writeTextPadded( '0', 1 );

    # End the line.
    #
    my $fh = $self->{_fh};
    print $fh "\n";
}

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

    $self->{_unitCount} = 0;

    my $dataCollection = $self->_getCollection();

    while ( my $data = $dataCollection->next() ) {
        $self->_writeDataLine($data);
    }
}

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

    $self->_writeTextPadded( '',    21 );
    $self->_writeTextPadded( 'FTR', 3 );
    $self->_writeTextPadded( '',    31 );
    $self->_writeIntegerPadded( $self->{_unitCount}, 8 );
    $self->_writeTextPadded( '',  332 );
    $self->_writeTextPadded( '0', 1 );

    my $fh = $self->{_fh};
    print $fh "\n";
}

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

    # I think it's safe to assume that we'll have some query set up to grab the sales we care about.
    # Whether we're going to be joining other tables in this query, well, I just don't know yet.
    # Probably...
    #

    # !!! I don't want to re-use the crappy RPS::File::Sales interface.
    #
    return RPS::DB::Item::ReportQueries::EMIPulse->GetSalesForPeriod( $self->periodID() );
}

my @gColumns = ( {
        # Recording Identifier
        method => '_identifier',
        width  => 18,
    },
    {
        # Date of Sale
        method => '_columnAsDate',
        field  => 'date_end',
        width  => 6,
    },
    {
        # Date Invoiced
        width => 6,
    },
    {
        # GSS Reporting Entity
        value => kGSSReportingEntity,
        width => 2,
    },
    {
        # GSS Selling Company
        width => 3,
    },
    {
        # DSP/MSP Code
        width => 8,
    },
    {
        # etailer code
        # !!! Not sure where this will come from...
        width => 8,
    },
    {
        # ReleaseType
        method => '_releaseType',
        width  => 3,
    },
    {
        # Single or Multi-track or Other
        width => 1,
    },
    {
        # Quantity
        method => '_columnAsQuantity',
        field  => 'units',
        width  => 8,
    },
    {
        # Artist
        method => '_artist',
        width  => 40,
    },
    {
        # Title
        method => '_title',
        width  => 60,
    },
    {
        # ID Type
        method => '_idType',
        width  => 1,
    },
    {
        # Segment
        width => 3,
    },
    {
        # currency
        field => 'currency_code',
        width => 3,
    },
    {
        # net value
        method => '_netValue',
        width  => 20,
    },
    {
        # Filename
        field => 'filename',
        width => 35,
    },
    {
        # Material Grp
        width => 9,
    },
    {
        # Sales Type
        width => 4,
    },
    {
        # Sales Type Desc
        width => 20,
    },
    {
        # Item Cat
        width => 4,
    },
    {
        # Item Cat Desc
        width => 20,
    },
    {
        # Prod Hierarchy
        width => 18,
    },
    {
        # Prod Hierarchy Desc
        width => 40,
    },
    {
        # Profit Centre
        width => 10,
    },
    {
        # Profit Centre Desc
        width => 40,
    },
    {
        # Sales Org
        width => 4,
    },
    {
        # Destination Country
        field => 'country_code',    # <- just a guess...
        width => 3,
    },
);

sub _writeDataLine {
    my ( $self, $dbItem ) = @_;

    # There are 28 fields.
    # I doubt all of this stuff will end up in the DB::Item directly.
    #
    # The ultimate abstract way would be to create a method for each column.
    # That feels like overkill, since the majority of these columns will just
    # be outputting the value in the DB::Item.
    #
    # I'll use a config hash to make it slightly more abstract.
    #
    foreach my $column (@gColumns) {
        my $method = $column->{method};
        $method = '_columnAsText' unless $method;

        $self->$method( $column, $dbItem );
    }

    my $fh = $self->{_fh};
    print $fh "\n";
}

sub _columnAsText {
    my ( $self, $column, $dbItem ) = @_;
    assert( $column->{width} );

    my $value;
    if ( defined $column->{value} ) {
        $value = $column->{value};
    } elsif ( $column->{field} ) {
        my $f = $column->{field};
        $value = $dbItem->$f();
    }

    $self->_writeTextPadded( $value, $column->{width} );
}

sub _columnAsDate {
    my ( $self, $column, $dbItem ) = @_;
    assert( $column->{width} );
    assert( $column->{field} );

    my $f     = $column->{field};
    my $value = $dbItem->$f();

    my @chunks = split( '-', $value );
    my $y = substr( $chunks[0], 2, 2 );
    my $m = $chunks[1];
    my $d = $chunks[2];

    $self->_writeTextPadded( sprintf( "%02d%02d%02d", $y, $m, $d ), $column->{width} );
}

sub _columnAsQuantity {
    my ( $self, $column, $dbItem ) = @_;
    assert( $column->{width} );
    assert( $column->{field} );

    my $f     = $column->{field};
    my $value = $dbItem->$f();

    $self->{_unitCount} += $value;

    $self->_writeIntegerPadded( $value, $column->{width} );
}

sub _netValue {
    my ( $self, $column, $dbItem ) = @_;
    assert( $column->{width} );

    my $units = $dbItem->units();
    my $price = $dbItem->price();

    # Anything else I need to do here?  Currency conversion, etc?
    #
    $self->_writeDecimalPadded( ( $units * $price ), $column->{width} );
}

sub _identifier {
    my ( $self, $column, $dbItem ) = @_;
    assert( $column->{width} );

    # Which column we look at will depend on the product type.
    #
    my $id;
    if ( $dbItem->product_type eq 'T' ) {
        $id = $dbItem->digital_isrc;
    } else {
        $id = $dbItem->digital_icpn;
    }

    $self->_writeTextPadded( $id, $column->{width} );
}

sub _releaseType {
    my ( $self, $column, $dbItem ) = @_;
    assert( $column->{width} );

    # Which column we look at will depend on the product type.
    #
    my $type;
    if ( $dbItem->product_type eq 'T' ) {
        $type = 'S';
    } else {
        $type = 'M';
    }

    $self->_writeTextPadded( $type, $column->{width} );
}

sub _idType {
    my ( $self, $column, $dbItem ) = @_;
    assert( $column->{width} );

    # Which column we look at will depend on the product type.
    #
    my $type;
    if ( $dbItem->product_type eq 'T' ) {
        $type = 'I';
    } else {
        $type = 'U';
    }

    $self->_writeTextPadded( $type, $column->{width} );
}

sub _artist {
    my ( $self, $column, $dbItem ) = @_;
    assert( $column->{width} );

    # Which column we look at will depend on the product type.
    #
    my $name;
    if ( $dbItem->product_type eq 'T' ) {
        $name = $dbItem->track_artist;
    } else {
        $name = $dbItem->album_artist;
    }

    $self->_writeTextPadded( $name, $column->{width} );
}

sub _title {
    my ( $self, $column, $dbItem ) = @_;
    assert( $column->{width} );

    # Which column we look at will depend on the product type.
    #
    my $name;
    if ( $dbItem->product_type eq 'T' ) {
        $name = $dbItem->track_title;
    } else {
        $name = $dbItem->album_title;
    }

    $self->_writeTextPadded( $name, $column->{width} );
}

sub _writeTextPadded {
    my ( $self, $string, $width ) = @_;

    my $fh = $self->{_fh};

    # Truncate the string if necessary
    #
    $string = substr( $string, 0, $width );

    my $padding = $width - length($string);
    if ( length($string) ) {
        print $fh $string;
    }

    if ( $self->{_useTabs} ) {
        print $fh "\t";
    } else {
        if ($padding) {
            print $fh ' ' x $padding;
        }
    }
}

sub _writeIntegerPadded {
    my ( $self, $number, $width ) = @_;
    my $paddedString = sprintf( "%0" . $width . "d", $number );

    my $fh = $self->{_fh};
    print $fh $paddedString;

    if ( $self->{_useTabs} ) {
        print $fh "\t";
    }
}

sub _writeDecimalPadded {
    my ( $self, $number, $width ) = @_;
    assert( $width > 6 );

    # Going to assume that we reserve 6 characters for the denominator.
    # All the rest are used for the minus sign, if any, the numerator, and the decimal point.
    #
    # !!! Note, not using RSMath rounding here....
    #
    my $paddedString = sprintf( "%0" . $width . ".06f", $number );

    my $fh = $self->{_fh};
    print $fh $paddedString;

    if ( $self->{_useTabs} ) {
        print $fh "\t";
    }
}

1;
