package Distribution::PackagerNew;

use strict;
use Carp;

use XML::LibXML;
use XML::SAX::ParserFactory;

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

use lib '/app/tools/distribution/lib';
use Distribution::DistributionProcess::ServiceAccount;
use Distribution::DB::Item::ProductDistribution;
use Distribution::DistributionProcess;
use Distribution::Job::Parameters;
use Distribution::Config;
use Distribution::Util;

use lib 'C:/app/tools/mediaserve/lib';
use MediaServe::Audio::Converter;

use Data::Dumper;
use Digest::MD5;
use File::Copy;
use File::Find;
use File::Path;
use File::Temp;
use Audio::FLAC;
use Template;
use File::Basename;

use base 'Distribution::DistributionObject';

use constant ERROR_SERVICE_COUNTRY_DENY =>
  "Album cannot be delivered here because the album's territory restrictions conflict with retailer's supported sales area.";
use constant ERROR_SERVICE_GENRE_DENY => "Album genres not accepted by service";
use constant ERROR_SERVICE_UTF8_DENY  => "Album UTF8 characters not accepted by service";

sub _init {
    my $self = shift;
    my %args = @_;

    $self->SUPER::_init(@_);

    $self->{_timestamp} = time();

    $self->{_customWritePath} = $args{custom_write_path};
    $self->{_noDelivery}      = $args{no_delivery};

    # get the acct info
    $self->{_acctInfo} = new Distribution::DistributionProcess::ServiceAccount(
        serviceID => $self->{_serviceID},
        clientID  => $self->{_clientID}
    );

    # get the job information
    $self->{_distributionProcess} = new Distribution::DistributionProcess( distributionProcessID => $self->{_distributionProcessID} );

    # parse the distribution process data into it's own object
    my $xml            = $self->{_distributionProcess}->XMLData();
    my $countries_only = $self->_countriesOnly();
    my $require_allowed => $self->_requireAllowed();

    $self->{_distributionProcessData} = new Distribution::Job::Parameters(
        XML             => $xml,
        service_id      => $self->{_serviceID},
        countries_only  => $countries_only,
        require_allowed => $require_allowed
    );

    # Force the distribution configuration to be read
    Common::RSApp::GetConfig('distribution');

    return $self;
}

sub run {
    my $self = shift;

    unless ( $self->_preValidate() && $self->_validate() && $self->_postValidate() ) {
        $self->_processFailure( "Failed validaton", 10 );
    }

    unless ( $self->_preStage() && $self->_stage() && $self->_postStage() ) {
        $self->_processFailure( "Failed staging", 20 );
    }

    unless ( $self->_prePackage() && $self->_package() && $self->_postPackage() ) {
        $self->_processFailure( "Failed packaging", 30 );
    }

    unless ( $self->_preManifest() && $self->_manifest() && $self->_postManifest() ) {
        $self->_processFailure( "Failed manifest", 40 );
    }

    unless ( $self->_preChecksum() && $self->_checksum() && $self->_postChecksum() ) {
        $self->_processFailure( "Failed checksum", 50 );
    }

    unless ( $self->_preFinalize() && $self->_finalize() && $self->_postFinalize() ) {
        $self->_processFailure( "Failed finalization", 60 );
    }

    return 1;
}

#######
##      Operation States
#######

sub _preValidate  { return 1; }
sub _postValidate { return 1; }

sub _validate {
    my $self = shift;
    my $error;

    Common::Log::Debug("Doing Pre-packaging validation");

    confess "No distribution process data object"
      unless ( $self->{_distributionProcessData} );

    confess "No distribution process object"
      unless ( $self->{_distributionProcess} );

    confess "Distribution process not found in the database"
      unless ( $self->{_distributionProcess}->DistributionProcessID() );

    $error = $self->{_distributionProcess}->validate();
    confess "$error" if ($error);

    my $service_id = $self->{_distributionProcess}->ServiceID();
    confess "Service id given ($self->{_serviceID}) doesn't match distribution job service_id ($service_id)"
      unless ( $self->{_serviceID} == $service_id );

    return 1;
}

sub _preStage  { return 1; }
sub _postStage { return 1; }

sub _stage {
    my $self = shift;

    Common::Log::Debug("Starting Staging Process");

    return ( $self->_createLocalBasePath() && $self->_createDistributionPackagePath() );
}

sub _prePackage { return 1; }

sub _postPackage {
    my $self             = shift;
    my @albums_processed = @{ $self->{_albumsProcessed} };

    unless (@albums_processed) {
        Common::Log::Print "No albums processed";
    }

    return 1;
}

sub _package {
    my $self = shift;
    my $album;
    my $denial_msg;
    my $album_dir;
    my @albums_processed;

    foreach $album ( @{ $self->{_distributionProcessData}->albums() } ) {
        Common::Log::Debug("Packaging album id: $album->{'catalog-number'}");

        $denial_msg = $self->_canDeliverToService($album);
        if ($denial_msg) {
            $self->_processDenial( $album, $denial_msg );
        } else {
            $self->_convertTerritories($album);

            $album_dir = $self->_createLocalAlbumPath($album);

            if (   $self->_processImage($album)
                && $self->_processAudio($album)
                && $self->_processMetadata($album) ) {

                push( @albums_processed, $album );
            } else {
                $self->_cleanup_dir($album_dir);
            }
        }

        $self->{_albumsProcessed} = \@albums_processed;
    }

    return 1;
}

sub _preManifest  { return 1; }
sub _postManifest { return 1; }

sub _manifest {
    my $self = shift;

    my $destXMLFile = $self->_localManifestPath();
    return 1 unless ($destXMLFile);

    Common::Log::Debug("*** Processing manifest ***");
    Common::Log::Debug(" - Manifest file: $destXMLFile");

    unless ( $self->_processedAlbumsCount() ) {
        Common::Log::Debug(" - No albums processed, not creating manifest");
        return 1;
    }

    if ( $self->{_manifest} ) {
        my $xml = Common::WriteXML::GetXMLString( $self->{_manifest}, $self->_manifestRoot(), skip_array_indexing => 1 );

        $self->_writeFile( $self->_localManifestPath(), $xml );
    } else {
        Common::Log::Print("WARNING: No manifest data found");
    }

    return 1;
}

sub _preChecksum  { return 1; }
sub _postChecksum { return 1; }

sub _checksum {
    my $self = shift;
    my $output;
    my $checksumFile = $self->_localChecksumPath();

    my @albums_processed = @{ $self->{_albumsProcessed} };
    return 1 unless (@albums_processed);

    Common::Log::Debug("*** Processing Checksum File ***");
    return 0 unless ( $self->{_checksums} );

    foreach my $path ( sort keys( %{ $self->{_checksums} } ) ) {
        $output .= sprintf( "%s\t%s\n", $path, $self->{_checksums}->{$path} );
    }

    $self->_writeFile( $checksumFile, $output );

    return 1;
}

sub _preFinalize  { return 1; }
sub _postFinalize { return 1; }

sub _finalize {
    my $self = shift;

    my @albums_processed = @{ $self->{_albumsProcessed} };
    return 1 unless (@albums_processed);

    $self->_setPackageDate();
    $self->_enqueueDelivery() unless ( $self->{_noDelivery} );

    return 1;
}

#######
##      Packager States
#######

sub _processAudio {
    my $self   = shift;
    my $album  = shift;
    my $volume = shift;
    my $length = 0;

    return 0 unless ( $self->_validateAlbumData($album) );

    foreach $volume ( @{ $album->{volume} } ) {
        my $result = $self->_processVolume( $album, $volume );

        $length += $volume->{'audio-duration'} if ( $volume->{'audio-duration'} );

        return unless ($result);
    }

    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime( $self->{_timestamp} );

    $album->{'generation-date'}        = sprintf( "%04d-%02d-%02d %02d:%02d:%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec );
    $album->{'audio-duration'}         = $length;
    $album->{'audio-duration-iso8601'} = Distribution::Util::seconds_to_iso8601( $album->{'audio-duration'} );

    $self->_addToManifest($album);

    return 1;
}

sub _processVolume {
    my $self   = shift;
    my $album  = shift;
    my $volume = shift;
    my $track;
    my $length = 0;

    return 0 unless ( $self->_validateVolumeData($volume) );

    $self->_processVolumeMapping( $album, $volume );

    foreach $track ( @{ $volume->{track} } ) {
        my $result = $self->_processTrack( $album, $volume, $track );

        $length += $track->{'audio-duration'} if ( $track->{'audio-duration'} );

        return unless ($result);
    }

    my $count = scalar @{ $volume->{'track'} };
    $volume->{'volume-track-count'} = $count;

    $volume->{'audio-duration'}         = $length;
    $volume->{'audio-duration-iso8601'} = Distribution::Util::seconds_to_iso8601( $volume->{'audio-duration'} );

    $self->_addToManifest( $album, $volume );

    return 1;
}

sub _processTrack {
    my $self   = shift;
    my $album  = shift;
    my $volume = shift;
    my $track  = shift;

    return 0 unless ( $self->_validateTrackData($track) );

    $self->_processTrackMapping( $album, $volume, $track );

    my $originalAudioPath = $self->_localOriginalAudioPath( $album, $volume, $track );
    my $resultAudioPath = $self->_localAudioFilePath( $album, $volume, $track );
    Common::Log::Debug("*** Converting Audio ***");
    Common::Log::Debug(" - AUDIO ORIG: $originalAudioPath");
    Common::Log::Debug(" - AUDIO DEST: $resultAudioPath");

    my $converter = $self->_audioConverterObject( filename => $originalAudioPath );

    unless ( $converter->write($resultAudioPath) ) {
        $self->_processFailure( "Failed to convert audio: " . $converter->error(), 16 );
    }

    my $basePath = $self->_localBasePath();

    $track->{'audio-path'} = $resultAudioPath;
    $track->{'audio-path'} =~ s/$basePath\/?//;
    $track->{'audio-filename'} = basename($resultAudioPath);

    $track->{'audio-checksum'} = $self->_get_file_checksum($resultAudioPath);
    $track->{'audio-size'}     = $self->_get_file_size($resultAudioPath);

    $track->{'audio-duration'}         = $self->_getFLACDuration($originalAudioPath);
    $track->{'audio-duration-iso8601'} = Distribution::Util::seconds_to_iso8601( $track->{'audio-duration'} );

    unless ( -e $resultAudioPath ) {
        print STDERR "Failed to convert audio: $resultAudioPath\n";
        return 0;
    }

    $self->_addToChecksum( $resultAudioPath, $track->{'audio-checksum'} );

    $self->_addToManifest( $album, $volume, $track );

    return 1;
}

sub _processImage {
    my $self  = shift;
    my $album = shift;

    assert($album);

    $self->_processImageMapping($album);

    my $originalImagePath = $self->_localOriginalImagePath($album);
    my $resultImagePath   = $self->_localImagePath($album);
    my $dir               = dirname($resultImagePath);
    my $file              = basename($resultImagePath);

    Common::Log::Debug("ORIGINAL IMAGE: $originalImagePath");
    Common::Log::Debug("IMAGE: $resultImagePath");

    my $converter = $self->_imageConverterObject($originalImagePath);
    $converter->write( $self->_imageFormat(), $dir, $file )
      or $self->_processFailure( "Failed to convert image: " . $converter->error(), 70 );

    my $basePath = $self->_localBasePath();

    $album->{'image-path'} = $resultImagePath;
    $album->{'image-path'} =~ s/$basePath\/?//;
    $album->{'image-filename'} = basename($resultImagePath);

    $album->{'image-checksum'} = $self->_get_file_checksum($resultImagePath);
    $album->{'image-size'}     = $self->_get_file_size($resultImagePath);

    $self->_addToChecksum( $resultImagePath, $album->{'image-checksum'} );

    return 1;
}

sub _processMetadata {
    my $self  = shift;
    my $album = shift;
    my $metadata;

    $self->_processAlbumMapping($album);

    my $metadataFile = $self->_localMetadataPath($album);

    my $origXMLFile = $self->_localBasePath() . '/' . basename($metadataFile) . ".original";

    my $structureFile = $self->_localBasePath() . '/' . basename($metadataFile) . ".structure";

    Common::Log::Debug("*** Processing metadata ***");
    Common::Log::Debug("METADATA: $metadataFile");
    assert($album);

    $album->{'volume-count'} = scalar @{ $album->{'volume'} };

    my $count = 0;
    foreach my $volume ( @{ $album->{'volume'} } ) {
        $count += scalar @{ $volume->{'track'} };
    }
    $album->{'track-count'} = $count;

    # If debugging, then dump the xml original datastructure to a file
    $self->_writeFile( $origXMLFile, $self->{_distributionProcess}->XMLData() )
      if ( $ENV{DEV_SERVER} );

    # If debugging, then dump the album datastructure to a file
    if ( $ENV{DEV_SERVER} ) {
        my $data = $album->asHashRef();
        $self->_writeFile( "$structureFile", Dumper($data) );
    }
    $album->{'track-count'} = $count;

    # If debugging, then dump the xml original datastructure to a file
    $self->_writeFile( $origXMLFile, $self->{_distributionProcess}->XMLData() )
      if ( $ENV{DEV_SERVER} );

    # If debugging, then dump the album datastructure to a file
    if ( $ENV{DEV_SERVER} ) {
        my $data = $album->asHashRef();
        $self->_writeFile( "$structureFile", Dumper($data) );
    }

    if ( $self->_localTemplatePath() =~ /xsl$/ ) {
        $metadata = $self->_processMetadataTransform($album);
    } else {
        $metadata = $self->_processMetadataTemplate($album);
    }

    if ( $self->_prettyPrintXML() ) {
        $self->_writeFile( $metadataFile, Distribution::Util::xml_pp($metadata) );
    } else {
        $self->_writeFile( $metadataFile, $metadata );
    }

    return unless ( $metadata && $self->_validateMetadata($metadataFile) );

    $self->_addToChecksum($metadataFile);

    return 1;
}

sub _validateMetadata {
    my $self = shift;
    my $metadataFile = shift || return undef;
    my $success;

    if ( $self->_xsdFile() ) {
        $success = $self->_validateWithXSD($metadataFile);
    } else {
        $success = $self->_validateWithoutXSD($metadataFile);
    }

    if ($success) {
        Common::Log::Debug(" - Validation successful");
    } else {
        Common::Log::Debug("** FAILED XML Validation **");
        File::Copy::copy( $metadataFile, "out.xml" );
        $self->_processFailure( "Failed packaging - XML validation failed", 35 );
    }

    return defined($success);
}

sub _validateWithoutXSD {
    my $self         = shift;
    my $metadataFile = shift || return undef;
    my $xsd          = $self->_localXSDPath();

    Common::Log::Debug("Validating XML ");
    Common::Log::Debug("  - XML: $metadataFile");

    my $parser = new XML::LibXML;
    my $doc    = $parser->parse_file($metadataFile);

    return $doc;
}

sub _validateWithXSD {
    my $self         = shift;
    my $metadataFile = shift || return undef;
    my $xmlDoc       = $self->_validateWithoutXSD($metadataFile) || return;

    my $xsd = $self->_localXSDPath();

    unless ( -e $xsd ) {
        Common::Log::Debug("** FAILED XML Validation **");
        Common::Log::Debug(" XSD does not exist: $xsd");
        return;
    }

    Common::Log::Debug("  - XSD: $xsd");

    my $validator = XML::LibXML::Schema->new( location => $xsd ) || Common::Log::Debug(" ** Failed: $! **");

    eval { $validator->validate($xmlDoc) };

    if ($@) {
        Common::Log::Debug("** FAILED XML Validation **");
        Common::Log::Debug($@);
        return;
    }

    return 1;
}

sub _processMetadataTransform {
    my $self  = shift;
    my $album = shift;
    my $metadata;

    my $java  = Common::RSApp::GetConfig( distribution => 'windows_java' );
    my $saxon = Common::RSApp::GetConfig( distribution => 'windows_saxon' );

    my $templateFile = $self->_localTemplatePath();
    Common::Log::Debug("TRANSFORM: $templateFile");

    my $xmlSource = Common::WriteXML::GetXMLString( $album, 'album', skip_array_indexing => 1 );

    my $sourceFile = File::Temp->new( UNLINK => 1, SUFFIX => ".xml" );
    my $destFile   = File::Temp->new( UNLINK => 1, SUFFIX => ".xml" );

    binmode( $sourceFile, ":utf8" );
    binmode( $destFile,   ":utf8" );

    $self->_processFailure( "Could not write to temp files for metadata translation", 22 )
      unless ( $sourceFile && $destFile );

    print $sourceFile $xmlSource;

    # If debugging, then dump the album datastructure to a file
    if ( $ENV{DEV_SERVER} ) {
        my $metadataFile = $self->_localMetadataPath($album);

        my $sourceFile = $self->_localBasePath() . '/' . basename($metadataFile) . ".src";

        my $data = $album->asHashRef();
        $self->_writeFile( "$sourceFile", $xmlSource );
    }

    my $cmd = "$java -jar $saxon -o $destFile $sourceFile $templateFile 2>&1";

    Common::Log::Debug(" - Running Command: $cmd");

    my $error = `$cmd`;
    if ($error) {
        Common::Log::Print(" -- Translation Failed: $error --");
        $self->_processDenial( $album, "Packager Failure (code: 8)" );
        return;
    }

    while (<$destFile>) {
        $metadata .= $_;
    }

    unlink($sourceFile);
    unlink($destFile);

    return $metadata;
}

sub _processMetadataTemplate {
    my $self     = shift;
    my $album    = shift;
    my $metadata = '';

    my $templateFile = $self->_localTemplatePath();

    my $tt = new Template( { ABSOLUTE => 1, EVAL_PERL => 1 } );

    my $data = $album->asHashRef();

    Common::Log::Debug("TEMPLATE: $templateFile");

    $tt->process( $templateFile, $data, \$metadata );

    if ( !$metadata ) {
        print STDERR $tt->error . "\n";
        return 0;
    }

    return $metadata;
}

sub _processImageMapping {
    my ( $self, $album ) = @_;
}

sub _processAlbumMapping {
    my ( $self, $album ) = @_;

    $album->{'vendor-id'} = $self->{_acctInfo}->Code();

    $self->_processFailure( "Vendor ID not defined", 12 )
      unless ( !$self->_requireVendorID || $album->{'vendor-id'} );
}

sub _processVolumeMapping {
    my ( $self, $album, $volume ) = @_;
}

sub _processTrackMapping {
    my ( $self, $album, $volume, $track ) = @_;
}

#######
##      Accessor functions
#######
sub _localBasePath {
    my $self = shift;

    $self->{_localBasePath} = $self->_createLocalBaseName()
      unless ( $self->{_localBasePath} );

    return $self->{_localBasePath} if ( $self->{_localBasePath} );
}

sub _localDistributionPath {
    my $self = shift;
    return $self->_localBasePath . '/' . $self->_distributionPackageName();
}

sub _distributionPackageName {
    my $self = shift;

    $self->{_distributionPackageName} = $self->_createDistributionPackageName()
      unless ( $self->{_distributionPackageName} );

    return $self->{_distributionPackageName};
}

sub _localXSDPath {
    my $self = shift;
    my $xsd = Common::RSApp::GetConfig( 'distribution', 'windows_xsd_dir' );

    return $xsd . '/' . $self->_xsdFile();
}

sub _xsdFile { }

sub _localTemplatePath {
    my $self = shift;
    my $template =
      $self->_templateFile =~ /xsl$/
      ? Common::RSApp::GetConfig( 'distribution', 'windows_transform_dir' )
      : Common::RSApp::GetConfig( 'distribution', 'windows_template_dir' );

    return $template . '/' . $self->_templateFile();
}

sub _templateFile { confess "Must be overloaded"; }

sub _localAlbumPath {
    my $self = shift;
    my $album = shift || confess "album required";

    return $self->_localDistributionPath . '/' . $self->_albumPackageName($album);
}

sub _albumPackageName {
    my $self = shift;
    my $album = shift || confess "album required";

    assert( $album->{upc} );

    return $album->{upc};
}

sub _localOriginalImagePath {
    my $self = shift;
    my $album = shift || confess "album required";

    return $album->{'image-path-windows'};
}

sub _localImagePath {
    my $self = shift;
    my $album = shift || confess "album required";

    return $self->_localAlbumPath($album) . '/' . $self->_albumImageName($album);
}

sub _albumImageName {
    my $self   = shift;
    my $album  = shift || confess "album required";
    my %args   = @_;
    my $format = $args{format} || "%s.%s";

    assert( $album->{upc} );

    return sprintf( $format, $album->{upc}, $self->_albumImageExtension($album) );
}

sub _albumImageExtension {
    return 'jpg';
}

sub _localMetadataPath {
    my $self = shift;
    my $album = shift || confess "album required";

    return $self->_localAlbumPath($album) . '/' . $self->_albumMetadataName($album);
}

sub _albumMetadataName {
    my $self = shift;
    my $album = shift || confess "album required";

    assert( $album->{upc} );

    return $album->{upc} . ".xml";
}

sub _localManifestPath {
    my $self = shift;

    return $self->_localBasePath() . '/' . $self->_manifestFile()
      if ( $self->_manifestFile() );

    return;
}

sub _manifestFile      { }
sub _manifestRoot      { 'Feed' }
sub _manifestAlbumRoot { 'Album' }

sub _localOriginalAudioPath {
    my $self   = shift;
    my $album  = shift || confess "album required";
    my $volume = shift || confess "volume required";
    my $track  = shift || confess "track required";

    return $track->{'audio-path-windows'};
}

sub _localAudioFilePath {
    my $self   = shift;
    my $album  = shift || confess "album required";
    my $volume = shift || confess "volume required";
    my $track  = shift || confess "track required";

    return $self->_localAlbumPath($album) . "/" . $self->_albumAudioFileName( $album, $volume, $track );
}

sub _albumAudioFileName {
    my $self   = shift;
    my $album  = shift || confess "album required";
    my $volume = shift || confess "track required";
    my $track  = shift || confess "track required";

    my %args = @_;
    my $format = $args{format} || "%s_%02d_%02d.%s";

    return
      sprintf( $format, $album->{upc}, $volume->{sequence}, $track->{sequence},
        $self->_albumAudioFileExtension( $album, $volume, $track ) );
}

sub _albumAudioFileExtension {
    return shift->_audioConverterObject()->Extension();
}

sub _imageConverterObject {
    my $self     = shift;
    my $filename = shift;

    assert($filename);

    my $package = $self->_imageConverter();

    eval "require $package";
    confess "Failed to load converter: $@" if ($@);

    my $obj = $package->new(
        filename => $filename,
        $self->_imageConverterOptions()
    );

    if ( !$obj || $obj->error() ) {
        $self->_processFailure( $obj->error(), 16 );
    }

    return $obj;
}

sub _imageConverter { 'MediaServe::Image::Converter' }

sub _imageConverterOptions { }

sub _imageFormat { confess "Must be overloaded" }

sub _audioConverterObject {
    my $self     = shift;
    my %args     = @_;
    my $filename = $args{filename};
    my $package  = $args{converter} || $self->_audioConverter();
    my $options  = $args{options} || $self->_audioConverterOptions();

    eval "require $package";
    confess "Failed to load converter: $@" if ($@);

    my $args = $options ? $options : {};
    my $obj = $package->new( filename => $filename, %$args );

    if ( !$obj || $obj->error() ) {
        $self->_processFailure( $obj->error(), 15 );
    }

    return $obj;
}

sub _audioConverter { confess "must be overloaded"; }

sub _audioConverterOptions { }

sub _albumManifest { }

sub _volumeManifest { }

sub _trackManifest { }

sub _localChecksumPath {
    my $self = shift;
    return $self->_localBasePath() . '/' . $self->_checksumFile();
}

sub _checksumFile { 'checksum' }

sub _serviceCountries { }

sub _serviceGenreRestricted { }

sub _serviceUTF8Restricted { }

sub _worldWideTerritory { }

sub _countriesOnly { }

sub _requireAllowed { }

sub _requireMappedGenres { }

sub _requireVendorID { 1 }

sub _requireMappedArtistRoles { }

sub _prettyPrintXML { 1 }

#######
##      Helper functions
#######

sub _createLocalBasePath {
    my $self = shift;
    my $dir  = $self->_localBasePath();

    Common::Log::Debug("Creating local base path: $dir");

    confess "$dir already exists" if ( -e $dir );    # houston, we have a problem!
    mkdir_local($dir);
}

sub _createLocalBaseName {
    my $self         = shift;
    my $package_base = $self->{_customWritePath}
      || Common::RSApp::GetConfig( 'distribution', 'windows_packaging_write_path' );

    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime( $self->{_timestamp} );
    my $date_pid = sprintf( "%4d%02d%02d%02d%02d%02d_%d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $$ );

    return join( '/', $package_base, $self->{_serviceID}, $self->{_clientID}, $date_pid );

}

sub _createDistributionPackagePath {
    my $self = shift;

    my $dir = $self->_localDistributionPath();
    Common::Log::Debug("Creating distribution package path: $dir");

    mkdir_local($dir) unless ( -d $dir );
}

sub _createLocalAlbumPath {
    my $self  = shift;
    my $album = shift;

    assert($album);

    my $dir = $self->_localAlbumPath($album);
    Common::Log::Debug("Creating album package path: $dir");

    mkdir_local($dir) unless ( -d $dir );
    return $dir;
}

sub _createDistributionPackageName {
    my $self   = shift;
    my %args   = @_;
    my $format = $args{format} || "%d%02d%02d_%02d%02d%02d";

    my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime( time() );

    return sprintf( $format, $year + 1900, $mon + 1, $mday, $hour, $min, $sec );
}

sub _processDenial {
    my $self    = shift;
    my $album   = shift;
    my $message = shift;

    assert($album);

    Common::Log::Print("Album denial: can not deliver $album->{'catalog-number'} to service");
    Common::Log::Print("  denial message: $message");

    $album->setDenied($message);
}

sub _canDeliverToService {
    my $self  = shift;
    my $album = shift;
    my $error;

    if ( !$self->_isValidCountryList() ) {
        $self->_processFailure( "Invalid Country Code", 37 );

    } elsif ( !$self->_canBeSoldIn($album) ) {
        $error = ERROR_SERVICE_COUNTRY_DENY;

    } elsif ( !$self->_isValidGenre($album) ) {
        $error = ERROR_SERVICE_GENRE_DENY;

    } elsif ( !$self->_isValidCharset($album) ) {
        $error = ERROR_SERVICE_UTF8_DENY;

    } else {

        #$error = "Fall through";
    }

    return $error;
}

sub _isValidCountryList {
    my $self             = shift;
    my @serviceCountries = $self->_serviceCountries;

    return 1 unless (@serviceCountries);

    my $allCountries = new Distribution::Catalog::CountryList;
    my @allCountries = @{ $allCountries->Countries() };

    my $lc = List::Compare->new( {
            lists    => [ \@serviceCountries, \@allCountries ],
            unsorted => 1,
        }
    );

    my @unknownCountries = $lc->get_unique();

    if (@unknownCountries) {
        Common::Log::Debug("  unknown countries: @unknownCountries");
        return;
    } else {
        return 1;
    }

}

sub _isValidCharset {
    my $self  = shift;
    my $album = shift;

    # If we are restricted to ASCII then fail if there is a character > ASCII 128.
    if ( $self->_serviceUTF8Restricted() ) {
        my $xml = Common::WriteXML::GetXMLString($album);
        $xml =~ s/[\x00-\x80]//g;
        return !$xml || length($xml) == 0 ? 1 : undef;
    }

    return 1;
}

sub _isValidGenre {
    my $self  = shift;
    my $album = shift;

    if ( $self->_serviceGenreRestricted() ) {
        return $album->canDeliverGenre( $self->{_serviceID} );
    }

    return 1;
}

sub _canBeSoldIn {
    my $self             = shift;
    my $album            = shift;
    my $global           = 1;
    my $direct           = 1;
    my @serviceCountries = $self->_serviceCountries();

    return $album->canBeSoldInCountry( \@serviceCountries, $self->_worldWideTerritory() );
}

sub _writeFile {
    my $self     = shift;
    my $destFile = shift;
    my $data     = shift;

    assert($destFile);
    assert($data);

    Common::Log::Debug(" - Writing data to $destFile");

    open( OUTFILE, ">>$destFile" ) || confess "Can not write to output file $destFile: $!";
    binmode( OUTFILE, ":utf8" );

    print OUTFILE $data;
    close(OUTFILE);
}

sub _addToChecksum {
    my $self     = shift;
    my $path     = shift;
    my $checksum = shift;

    $checksum = $self->_get_file_checksum($path)
      unless ($checksum);

    assert( $checksum, "No checksum" );

    $self->{_checksums} = {} unless ( $self->{_checksums} );

    $self->{_checksums}->{$path} = $checksum;
}

sub _addToManifest {
    my $self   = shift;
    my $album  = shift;
    my $volume = shift;
    my $track  = shift;
    my $method;
    my $albumRoot = $self->_manifestAlbumRoot();

    if ($track) {
        $method = '_trackManifest';
    } elsif ($volume) {
        $method = '_volumeManifest';
    } else {
        $method = '_albumManifest';
    }

    if ( $self->$method( $album, $volume, $track ) ) {
        $self->{_manifest}->{$albumRoot} = []
          unless ( $self->{_manifest}->{$albumRoot} );

        push( @{ $self->{_manifest}->{$albumRoot} }, $self->$method($album) );
    }
}

sub _setPackageDate {
    my $self = shift;

    Common::Log::Debug("*** Setting packaged date ***");

    foreach my $album ( @{ $self->{_albumsProcessed} } ) {
        Distribution::DB::Item::ProductDistribution->setPackageDate( $album->{'product-distribution-id'} );
    }

}

sub _enqueueDelivery {
    my $self = shift;

    Common::Log::Debug("*** Setting up package for delivery ***");

    my $process = Distribution::DistributionProcess->new( distributionProcessID => $self->{_distributionProcessID} );

    $process->enqueueDelivery(
        client_id      => $self->{_clientID},
        base_dir       => $self->_localBasePath(),
        package        => $self->_distributionPackageName(),
        force_delivery => 1
    );

}

sub _processFailure {
    my $self = shift;
    my $msg  = shift || "Unknown error";
    my $code = shift || 99;

    my $error = "Packaging Issue ($code) - RoyaltyShare resolving";
    my $id    = $self->{_distributionProcessID};

    assert( $id, "Distribution process id required" );

    print STDERR "Packaging failure: (code: $code) $msg\n";

    Distribution::DB::Item::DistributionProcess->setDenied( $id, $error );

    $self->_cleanup_dir( $self->_localBasePath() );

    exit $code;
}

sub _convertTerritories {
    my $self  = shift;
    my $album = shift;

    Common::Log::Debug(" - Converting Territories");

    if ( $self->_countriesOnly() ) {
        $album->removeRegions( $self->_countriesOnly() );
    }

    if ( $self->_requireAllowed() ) {
        $album->invertDeniedCountries( $self->_requireAllowed() );
    }

    if ( $self->_serviceCountries() ) {
        $album->setServiceCountries( $self->_serviceCountries() );
    }

    if ( $self->_worldWideTerritory() ) {
        $album->setWorldWideTerritory( $self->_worldWideTerritory() );
    }
}

sub _getFLACDuration {
    my $self = shift;
    my $file = shift;

    Common::Log::Debug(" - Reading Flac Information: $file");

    my $flac     = new Audio::FLAC($file);
    my $flacInfo = $flac->info();

    return 0 unless ($flacInfo);

    my $totalSamples = $flacInfo->{TOTALSAMPLES};
    my $sampleRate   = $flacInfo->{SAMPLERATE};

    return int( $totalSamples / $sampleRate );
}

sub _processedAlbumsCount {
    my $self = shift;

    return $self->{_albumsProcessed} ? scalar @{ $self->{_albumsProcessed} } : 0;
}

sub _isDistributionPackageUsed {
    my $self         = shift;
    my $name         = shift;
    my $package_base = Common::RSApp::GetConfig( 'distribution', 'windows_packaging_write_path' );
    my $base         = join( '/', $package_base, $self->{_serviceID} );

    Common::Log::Debug(" - Looking for $name in $base");

    return 1 unless ( -e $base );
    opendir( D1, $base ) || die "can't opendir - $!";
    foreach my $cid ( readdir(D1) ) {
        next unless ( -d "$base/$cid" );
        opendir( D2, "$base/$cid" ) || die "can't opendir - $!";
        foreach my $key ( readdir(D2) ) {
            next unless ( -d "$base/$cid/$key" );
            opendir( D3, "$base/$cid/$key" ) || die "can't opendir - $!";
            if ( grep /^$name$/, readdir(D3) ) {
                closedir(D3);
                closedir(D2);
                closedir(D1);

                return 1;
            }
            closedir(D3);
        }
        closedir(D2);
    }
    closedir(D1);

    return;
}

sub _validateAlbumData {
    my $self  = shift;
    my $album = shift;

    assert($album);

    if ( $self->_requireMappedGenres() ) {
        die "Genre maps not complete"
          unless ( $self->_isValidGenreObject( $album->{'genre-object'}, $album->{'genre-secondary-object'} ) );
    }

    if ( $self->_requireMappedArtistRoles() ) {
        die "Artist role maps not complete" unless ( $self->_isValidArtistRoles($album) );
    }

    return 1;
}

sub _validateVolumeData { 1 }

sub _validateTrackData {
    my $self  = shift;
    my $track = shift;

    assert($track);

    if ( $self->_requireMappedGenres() ) {
        die "Genre maps not complete"
          unless ( $self->_isValidGenreObject( $track->{'genre-object'}, $track->{'genre-secondary-object'} ) );
    }

    if ( $self->_requireMappedArtistRoles() ) {
        die "Artist role maps not complete" unless ( $self->_isValidArtistRoles($track) );
    }

    return 1;
}

sub _isValidGenreObject {
    my $self            = shift;
    my $genre           = shift;
    my $genre_secondary = shift;
    my $result          = 1;

    $result = undef unless ( $genre && defined( $genre->{map} ) );

    if ($genre_secondary) {
        $result = undef unless ( defined( $genre_secondary->{map} ) );
    }

    unless ($result) {
        Common::Log::Print( Dumper($genre) );
        Common::Log::Print( Dumper($genre_secondary) );
    }

    return $result;
}

sub _isValidArtistRoles {
    my $self   = shift;
    my $obj    = shift;
    my $result = 1;

    assert($obj);

    my $artistList = $obj->{'additional-artists'} || [];

    if ( ref($artistList) ne 'ARRAY' ) {
        $artistList = [$artistList];
    }

    foreach my $artistHash (@$artistList) {
        $result = undef unless ( defined( $artistHash->{role}->{map} ) );
    }

    unless ($result) {
        Common::Log::Print( Dumper($artistList) );
    }

    return $result;
}

1;
