package Support::Implementation::ArtistContractTemplateTwo;
# 4/9/10 - Changed price_level source to RSCOMMON.price_level
# 7/20/10 - Fixed album/track crossed to use 'x' instead of 'y'
# 10/27/10 - Added check for blank contract name
# 1/21/11 - Allow synonyms for album/track crossed
# 5/25/11 - Stripped trailing spaces from text fields.
# 6/29/12 - Fixed track crossing processing
# 8/31/12 - Improved contract title matching logic.
# 2/19/12 - Cleaned up album/track crossed fields.
# 2/6/14 - Added Excel 2007 support.
# 3/6/14 - Improved message for contract not found error
# 1/7/16 - Added check for missing track proration (FB12481)
# 2/24/16 - Remove whitespace from album/track ID and track proration
# 8/30/16 - Calculate track proration if not specified (FB15593)
# 7/18/17 - Added Naxos (390) to the list of auto-proration clients (the others
#   are eOne (294) and TheOrchard (372). FB19532
# 7/21/17 - Added Red Bull (403) to the auto-prorate club (FB19666).
# 9/15/17 - Added Light In The Attic (394) to the auto-prorate club (FB20314).
# 9/21/17 - Make everyone a member of the auto-prorate club (FB20519).
# 4/1/19 - Added duplicate track title check
# 4/29/20 - Limit new_artist_contract search to active (deleted=0) contracts
#
use strict;
use warnings;

use lib '/app/tools/common/lib';
use lib '/app/tools/rps/lib';

use IO::File;
use Data::Dumper;
use Date::Calc;

use Spreadsheet::ParseExcel;

#use Support::Implementation::ImplementationUtil qw(_findTrack _findAlbum report
#   appendString kAlbumNotFound kNonUniqueAlbumName kTrackNotFound);
use Support::Implementation::ImplementationUtil qw( report appendString checkMissingColumn printNull );

use Common::Util qw(clean trimspaces);

use Common::Assert;
use Common::UTF8;
use Common::RSApp;

use RPS::DB::Item::NewArtistContract;
use RPS::DB::Item::NewArtistContractTerm;
use RPS::DB::Item::IncomeSource;
use RPS::DB::Item::ContractRateType;
use RPS::DB::Item::ReserveLiquidation;

use RPS::DB::Item::Album;
use RPS::DB::Item::Track;
use RPS::DB::Item::AlbumContract;
use RPS::DB::Item::TrackContract;

use RPS::DB::Item::ArtistPayee;
use RPS::DB::Item::Payor;
use RPS::DB::Item::Region;

use Support::Implementation::ExcelReader;
use Support::Implementation::Excel2007Reader;
use Support::Implementation::TabDelimitedReader;

#use Support::Implementation::SearchUtil qw ( report );
use Support::Implementation::SearchUtil;

use base 'Support::Implementation::Template';

use constant kQuiet  => 0;
use constant kNormal => 1;
use constant kDebug  => 3;
my $gReportLevel = kNormal;

use constant kDuplicateTerm       => 1;
use constant kDefaultTermNotLast  => 2;

binmode STDOUT, ":utf8";


#use constant kClassAttributes => qw( name exec_mode client_id );

#-----------------------------------------------------------------------
# templateHeader maps column names to their default column number.
# The actual column number-header name map is stored in columnMap.
# Note(s):
# 1) Column names are case sensitive
# 2) Spaces are ignored; you must manually remove spaces from the names
#    stored in templateHeader.
# 
# TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO
#
# Should modify logic to reject template if the column names don't
# match _exactly_.  See line 78 of ExcelReader.pm (scanExcelFile); this
# is where spaces are removed from the column name.
#-----------------------------------------------------------------------
my %gTemplateHeader = (
   "contract-name"   => 0, # A
   "contract-id"     => 1, # B
   "rs-album-id"     => 2, # C
   "album-name"      => 3, # D
   "catalog-number"  => 4, # E
   "album-crossed"   => 5, # F
   "rs-track-id"     => 6, # G
   "track-name"      => 7, # H
   "track-proration" => 8, # I
   "track-crossed"   => 9, # J
);

#--------------------------------------------------------------
# Maps column #'s to a unique key (headername).  The reverse of
# templateHeader, but with actual column #.
#--------------------------------------------------------------
my %gColumnMap = ();
sub parseHeader {
   my $self = shift;
}

#my $app; # app singleton
my $dbo;
my $dbh;
my $cdbo;
my $clientID;
my $execMode;

my %gRegionMap;
my %gIncomeSourceMap;
my %gChannelMap;
my %gPriceLevelMap;
my %gContractRateTypeMap;

sub new {
   my ( $class, %args ) = @_;
   my $self = bless {}, $class;

   $self->{dbo} = $dbo;

   return $self->_init(%args);
}

sub _init {
   my ($self, %args) = @_;

   $self->SUPER::_init(%args);

   $execMode = $self->exec_mode if ( $self->exec_mode );

   #my $clientID = $self->client_id; XXX
   $clientID = $self->client_id;

   my $app = Common::RSApp->new( clientID => $clientID );

   $dbo = Common::RSApp::GetClientDB();
   $dbh = $dbo->DBH;

   $cdbo = Common::RSApp::GetCommonDB();

   #------------------------------
   # Build a list of valid regions
   #------------------------------
   my $sql = "SELECT region_id, name FROM region";
   my $sth = $dbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gRegionMap{ lc $name } = $id;
   }

   #-------------------------------------
   # Build a list of valid income sources
   #-------------------------------------
   $sql = "SELECT income_source_id, format, name FROM income_source";
   $sth = $cdbo->DoCmd($sql);
   while( my($id,$format,$name) = $sth->fetchrow_array() ) {
      my $val = join("\t",$format,$id);
      $gIncomeSourceMap{ lc $name } = $val;
   }

   #-------------------------------
   # Build a list of valid channels
   #-------------------------------
   $sql = "SELECT channel_id, name FROM channel";
   $sth = $dbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gChannelMap{ lc $name } = $id;
   }
   $gChannelMap{all} = 0;

   #-----------------------------------
   # Build a list of valid price levels
   #-----------------------------------
   $sql = "SELECT price_level_id, name FROM price_level";
   $sth = $cdbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gPriceLevelMap{ lc $name } = $id;
      $gPriceLevelMap{album} = $id if ( $name =~ m/^album/ );
      $gPriceLevelMap{track} = $id if ( $name =~ m/^track/ );
   }
   $gPriceLevelMap{all} = 0;

   #------------------------------------------
   # Build a list of valid contract rate types
   #------------------------------------------
   $sql = "SELECT contract_rate_type_id, name FROM contract_rate_type";
   $sth = $cdbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gContractRateTypeMap{ lc $name } = $id;

      # store synonyms that are used by the template specification 1.1
      $gContractRateTypeMap{ retail } = $id if ( $name =~ m/retail/i );
      $gContractRateTypeMap{ wholesale } = $id if ( $name =~ m/wholesale/i );
      $gContractRateTypeMap{ revenue } = $id if ( $name =~ m/revenue/i );
      $gContractRateTypeMap{ "net revenue" } = $id if ( $name =~ m/revenue/i );
      $gContractRateTypeMap{ fixed } = $id if ( $name =~ m/fixed/i );
   }

   return $self;
}

sub getHeader {
   my $self = shift;
   #shift->{gTemplateHeader};
   return %gTemplateHeader;
}

sub getColumnMap {
   my $self = shift;
   return %gColumnMap;
}

sub getDbo {
   my $self = shift;
   return $dbo;
}

# Reference to SearchUtil object
my $gSearchObj;

my $gClientID;

#--------------------------------------------------------------
# Read-in all of the template data into memory.  Once it's been
# read, try to parse it and
#--------------------------------------------------------------
sub loadMemory {
   my $self = shift;

   my $clientID = $self->client_id;
   $gClientID = $clientID;
   my $app = Common::RSApp->new( clientID => $clientID );
#
   $dbo = Common::RSApp::GetClientDB();
   $dbh = $dbo->DBH;
#
   $cdbo = Common::RSApp::GetCommonDB();

   $gSearchObj = Support::Implementation::SearchUtil->new(
      clientID => $clientID,
   );

   my $fileName = $self->name;

   if( $self->isExcel2003( $fileName ) || $self->isExcel2007( $fileName ) ) {
      print("ArtistContractTemplate::loadMemory -- loading Excel2k3 $fileName into memory\n");

      #--------------------------
      # Read in the 1st worksheet
      #--------------------------
      my %data;
      my $reader;

      if( $self->isExcel2003( $fileName ) )
      {
         $reader = Support::Implementation::ExcelReader->new(
            filename => $fileName,
            data => \%data,
            header => \%gTemplateHeader,
            columnmap => \%gColumnMap,
            tab => 1,
         );
      }
      else
      {
         $reader = Support::Implementation::Excel2007Reader->new(
            filename => $fileName,
            data => \%data,
            header => \%gTemplateHeader,
            columnmap => \%gColumnMap,
            tab => 1,
         );
      }
      $reader->scanExcelFile();

      #----------------------------------------------------------------------------
      # Print the report header
      #
      # TODO: This should be dynamically built using the gColumnMap hash, but
      # then I'd have to rework the "REPORT" rows in the _validate routine so
      # that the values lined up under the correct column.  Wait.. scratch
      # that, I've have to modify the contractBuffer hash to store each
      # template line as a hash of key-value pairs (currently it just stores
      # each template line as a tab-delimited string).  Anyway, we'll save
      # this for the next rev...
      #----------------------------------------------------------------------------
#      report(join("\t", "STATUS:",
#         "contract-name"  , # A
#         "contract-id"    , # B
#         "rs-album-id"    , # C
#         "album-name"     , # D
#         "catalog-number" , # E
#         "album-crossed"  , # F
#         "rs-track-id"    , # G
#         "track-name"     , # H
#         "track-proration", # I
#         "track-crossed"  , # J
#      ));
      #---------------------
      # ..Now parse the data
      #---------------------
      _processData(\%data);

      #return %data;
   } elsif( $self->isTabDelimited( $fileName ) ) {
      #die("Whoa.... I don't know how to do this with tab-delimited files!!!\n");
      report("ArtistContractTemplate::loadMemory -- processing tab-delimited file");

      my %data;
      my $reader = Support::Implementation::TabDelimitedReader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap
      );

      $reader->scanTabbedFile;
      _processData(\%data);

   }
}

#-------------------------------------------------------------------
# _processData is where the real work is done.  It takes the generic
# information stored in the supplied array of hashes and decodes it.
# In this case, it assumes that the supplied data contains contract
# data.
#-------------------------------------------------------------------
sub _processData {

#   die("ArtistContractTemplate -- _processData must be defined in subclass");
   my($data) = @_;
   my $rows = $data->{rows};

   #-----------------------------------------------------------------------
   # Hash of all contracts seen.  We'll use this hash to detect if template
   # lines aren't grouped together properly.
   #-----------------------------------------------------------------------
   my %seenContractMap = ();
#
#   my @errorList = ();
#
#   #--------------------------------------------
#   # Get the regions associated with this client
#   #--------------------------------------------
#   my $sql = "SELECT region_id, name FROM region";
#   my $sth = $dbo->DoCmd($sql);
#   my @regionList;
#   while( my($regionID, $name) = $sth->fetchrow_array() ) {
#      my $regionInfo = { "region_id" => $regionID, "name" => $name };
#      push @regionList, $regionInfo;
#   }
#
#
#   #--------------------------------------------------------------
#   # If a contract has conflicting terms, then it'll appear in the
#   # following hash.
#   #--------------------------------------------------------------
#   my %contractErrorMap = ();
#
#   #------------------------------------------------------------
#   # Map of rate and rate reduction for each contract by region.
#   #------------------------------------------------------------
#   my %rateMap = ();
#

   my %excount = (
      contract_not_found  => 0,
      album_not_found     => 0,
      tracK_not_found     => 0,
   );

   #--------------------------------------
   # rowcount keeps track of line counters
   #--------------------------------------
   my %rowcount = (
      rows_failed => 0,
      total       => 0,
   );

   #----------------------------------------------------------
   # entities: if we create any RPS entities, we'll keep track
   # of the totals in this hash
   #----------------------------------------------------------
   my %entities = (
      album_contract => 0,
      track_contract => 0,
   );

   #================================================================
   # We'll use the following variables to keep track of the contract
   # whose terms we are processing.
   #================================================================

   # Keep track of the number of contracts processed
   my $contractCounter = 0;

   #--------------------------------------------------------------------
   # errorCode will hold one or more error messages for the current line
   #--------------------------------------------------------------------
   my $errorCode;

   use constant kColumnContractName     => 'contract-name';
   use constant kColumnClientContractID => 'contract-id';
   use constant kColumnContractID       => 'rs-contract-id';
   use constant kColumnAlbumID          => 'rs-album-id';
   use constant kColumnAlbumName        => 'album-name';
   use constant kColumnCatalogNumber    => 'catalog-number';
   use constant kColumnAlbumCrossed     => 'album-crossed';
   use constant kColumnTrackID          => 'rs-track-id';
   use constant kColumnTrackName        => 'track-name';
   use constant kColumnTrackProrate     => 'track-proration';
   use constant kColumnTrackCrossed     => 'track-crossed';

   my @requiredColumns;
   push @requiredColumns, kColumnContractName;
   #push @requiredColumns, kColumnClientContractID;
   #$push @requiredColumns, kColumnContractID;
   push @requiredColumns, kColumnAlbumID;
   push @requiredColumns, kColumnAlbumName;
   push @requiredColumns, kColumnCatalogNumber;
   push @requiredColumns, kColumnAlbumCrossed;
   push @requiredColumns, kColumnTrackID;
   push @requiredColumns, kColumnTrackName;
   push @requiredColumns, kColumnTrackCrossed;

   #----------------------------------------------------------
   # hash of contract-specific keys to contractIDs
   # Each tab-delimited key is a combination of the following:
   #
   #   clean(title)
   #   clean(clientContractID)
   #----------------------------------------------------------
   my %cleanTitleMap;
   my $sql = "SELECT artist_contract_id, title, client_contract_id FROM new_artist_contract";
   my $sth = $dbo->DoCmd($sql);
   while( my( $_contractID, $_title, $_clientContractID ) = $sth->fetchrow_array() )
   {
       my $_cleanContractID = ($_clientContractID) ? clean($_clientContractID) : "";
       #my $key = join( "\t", clean($_title), clean($_clientContractID) );
       my $key = join( "\t", clean($_title), $_cleanContractID );

       if( ! exists $cleanTitleMap{$key} )
       {
           $cleanTitleMap{$key} = $_contractID;
       }
       else
       {
           $cleanTitleMap{$key} = "DUPLICATE";
           report("FYI: Found existing key for contract($_title) clientContractID(". ($_clientContractID || "")
               . " [contractID = $_contractID]");
           #die("ERROR: Found existing key for contract($_title) clientContractID($_clientContractID)");
       }
   }

   #---------------------------------------------------------------------------
   # Loop over each row (album or track contract), and store the term information in the
   # contractBuffer hash.  Once we detect that we're done reading terms for a
   # particular contract, go ahead and try to validate the contract information
   # in the hash.  If it passes muster, create the contract and the terms.
   #---------------------------------------------------------------------------
   foreach my $row (@$rows) {
#
      #die("STOP\n") if ( $contractCounter > 5 );

      #-------------------------------------------------------
      # Get all of the template variables.
      # Note: the keys listed are defined in %gTemplateHeader.
      #-------------------------------------------------------
      my $rowid             = $row->{rowid};

      my $contractName      = $row->{ kColumnContractName()     };
      my $clientContractID  = $row->{ kColumnClientContractID() };
      my $contractID        = $row->{ kColumnContractID()       };
      my $rsAlbumID         = $row->{ kColumnAlbumID()          };
      my $albumName         = $row->{ kColumnAlbumName()        };
      my $catalogNumber     = $row->{ kColumnCatalogNumber()    };
      my $albumCrossed      = $row->{ kColumnAlbumCrossed()     };
      my $rsTrackID         = $row->{ kColumnTrackID()          };
      my $trackName         = $row->{ kColumnTrackName()        };
      my $trackProration    = $row->{ kColumnTrackProrate()     };
      my $trackCrossed      = $row->{ kColumnTrackCrossed()     };

      report("#### [$rowid]  name($contractName) "
         . "album(". printNull($albumName) . ") "
         . "cat#(". printNull($catalogNumber) .")\n" . Dumper( \%$row ));

      undef $errorCode;

      ++$rowcount{total};

      $contractName  = trimspaces( $contractName )  if ( $contractName );
      $catalogNumber = trimspaces( $catalogNumber ) if ( $catalogNumber );
      $albumName     = trimspaces( $albumName )     if ( $albumName );
      $trackName     = trimspaces( $trackName )     if ( $trackName );
      $albumCrossed  = trimspaces( $albumCrossed )  if ( $albumCrossed );
      $trackCrossed  = trimspaces( $trackCrossed )  if ( $trackCrossed );
      $rsAlbumID     = trimspaces( $rsAlbumID    )  if ( $rsAlbumID    );
      $rsTrackID     = trimspaces( $rsTrackID    )  if ( $rsTrackID    );
      $trackProration = trimspaces( $trackProration ) if ( $trackProration );

      #-----------------------------------
      # Make sure all columns are in place
      #-----------------------------------
      my $s;
      foreach my $c (@requiredColumns) {
         $s = checkMissingColumn( $c, $row, \%excount );
         appendString( $errorCode, $s ) if ( $s );
      }

      if ( $errorCode ) {
         die("ERROR: template is missing the following column(s):\n$errorCode\n");
      }

      #------------------
      # Find the contract
      #------------------
      my $artistContractID;

      if ( defined $contractName and '' ne $contractName )
      {
         my %cArgs = (
            'title' => $contractName,
         );
         $cArgs{client_contract_id} = $clientContractID  if( $clientContractID && '' ne $clientContractID );
         $cArgs{artist_contract_id} = $contractID  if( $contractID && '' ne $contractID );
         $cArgs{deleted}            = 0;

         report("DEBUG: looking for contract: ".Dumper(\%cArgs) );

#if ( $gClientID == 51 ) { # NETTWERK
#   delete $cArgs{title} if ( $contractID && $contractID == 1079 );
#}
         my $cObj = RPS::DB::Item::NewArtistContract->Lookup( %cArgs );
         if ( !$cObj ) {

            # 8/31/12: Try to look for the contract using the clean title/contractID
            my $_cleanContractID = ($clientContractID) ? clean($clientContractID) : "";
            my $k = join("\t", clean($contractName), $_cleanContractID );

            if( exists $cleanTitleMap{$k} )
            {
                if( $cleanTitleMap{$k} ne 'DUPLICATE' )
                {
                    $artistContractID = $cleanTitleMap{$k};
                    report("   * Found contract $artistContractID using clean title/clientContractID");
                }
                else
                {
                    report("ERROR: Duplicate contract($contractName) with clientContractID($clientContractID)");
                    appendString( $errorCode, "Duplicate Contract found");
                    ++$excount{duplicate_contract_found};
                }
            }
            else
            {
                report("ERROR: Contract '$contractName' (contract ID '$_cleanContractID') not found");
                appendString( $errorCode, "Contract not found");
                ++$excount{contract_not_found};
            }

         } else {
            $artistContractID = $cObj->artist_contract_id;
         }
      }
      else
      {
         report("ERROR: No contract name in line $rowid");
         appendString( $errorCode, "Missing contract name");
         ++$excount{missing_contract_name};
      }

      my $albumID;  # set only if album-level contract is under consideration
      my $trackID;  # set only if track-level contract is under consideration
      my $errFlag;  # set only if album/track error is detected

      #---------------------------------------------------------------------
      # Check for illegal characters in crossed columns.  Per 1.2 spec, it's
      # either blank or 'x'. -ES 7/20/10
      #---------------------------------------------------------------------

      #if ( $trackCrossed and $trackCrossed !~ m/x/i )
      if ( ! _validCrossed($trackCrossed) )
      {
         appendString( $errorCode, "Invalid track crossed value");
         ++$excount{invalid_track_crossed_value};
      }

      #if ( $albumCrossed and $albumCrossed !~ m/x/i )
      if ( ! _validCrossed($albumCrossed) )
      {
         appendString( $errorCode, "Invalid album crossed value");
         ++$excount{invalid_album_crossed_value};
      }

      if ( !$rsTrackID && !$trackName ) {

         #-----------------------------
         # Process album-level contract
         #-----------------------------
         if ( $rsAlbumID ) {
            my $aObj = RPS::DB::Item::Album->Lookup(
               album_id => $rsAlbumID,
            );
            if ( $aObj ) {
               $albumID = $aObj->album_id;

               # Make sure the info matches the template
               if( $albumName && $aObj->title ne $albumName ) {
                  report("MISMATCH ERROR: albumID $albumID title '". $aObj->title . "' doesn't match '$albumName'!\n");
                  appendString( $errorCode, "Album name mismatch");
                  ++$excount{album_name_mismatch};
               }
               if( $catalogNumber && $aObj->catalog_number ne $catalogNumber ) {
                  report("MISMATCH ERROR: albumID $albumID catno '". $aObj->catalog_number . "' doesn't match '$catalogNumber'!\n");
                  appendString( $errorCode, "Catalog number mismatch");
                  ++$excount{catalog_number_mismatch};
               }
            } else {
               appendString( $errorCode, "Invalid albumID");
               ++$excount{albumid_not_found};
            }
         } else {

            #----------------------------------------------------------
            # Check if we're missing both album _and_ track information
            #----------------------------------------------------------
            #if ( $albumName && '' eq $albumName && $catalogNumber && '' eq $catalogNumber ) 
            if ( ( !$albumName || ($albumName && '' eq $albumName ) ) &&
                 ( !$catalogNumber || ($catalogNumber && '' eq $catalogNumber) ) &&
                 ( !$rsAlbumID || ($rsAlbumID && '' eq $rsAlbumID) ) &&
                 ( !$trackName || ($trackName && '' eq $trackName) ) &&
                 ( !$rsTrackID || ($rsTrackID && '' eq $rsTrackID) ) )
            {
               report("ERROR: no album or track information in rowid($rowid)!\n");
               appendString( $errorCode, "No album or track specified");
               ++$excount{missing_album_track};
            } else {
               #---------------
               # Find the album
               #---------------
               my %aArgs = (
                  album_name => $albumName,
               );
               $aArgs{catalog_number} = $catalogNumber if ( $catalogNumber && '' ne $catalogNumber );

               #($albumID, $errFlag) = _findAlbum(  $dbo, %aArgs );
               ($albumID, $errFlag) = $gSearchObj->findAlbum( %aArgs );

               if ( $errFlag ) {
                  report("   errFlag($errFlag) ***");
                  if ( $errFlag == Support::Implementation::SearchUtil::kAlbumNotFound ) {
                     report("ERROR: findAlbum returned album not found : ". Dumper(\%aArgs) );
                     appendString( $errorCode, "Album not found");
                     ++$excount{album_not_found};
                  } elsif ( $errFlag == Support::Implementation::SearchUtil::kNonUniqueAlbumName ) {
                     appendString( $errorCode, "Non-unique album name");
                     ++$excount{nonunique_album_name};
                  } else {
                     die("ERROR: _findAlbum returned unknown errorFlag($errFlag)\n");
                  }
               } else {
                  report("   albumID($albumID)");
               }
            }
         }
      } else {

         #-----------------------------
         # Process track-level contract
         #-----------------------------
         if ( $rsTrackID ) {
            my $tObj = RPS::DB::Item::Track->Lookup(
               track_id => $rsTrackID,
            );
            if ( $tObj ) {
               $trackID = $tObj->track_id;

               # Make sure the info matches the template
               if( $trackName && $tObj->title ne $trackName ) {
                  report("MISMATCH ERROR: trackID $rsTrackID title '". $tObj->title . "' doesn't match '$trackName'!\n");
                  appendString( $errorCode, "Track name mismatch");
                  ++$excount{track_name_mismatch};
               }

#               # Calculate track proration on the fly (FB15593) XXX XXX XXX
#               # Note that for now, this is enabled on a per-client basis: 294 (eOne), 372 (TheOrchard)
#               if( ( !$trackProration || '' eq $trackProration ) &&
#                   $clientID =~ /^(294|372)$/ )
#               {
#                   my $sql = "SELECT track_id FROM track WHERE album_id = ". $tObj->album_id;
#                   my $sth = $dbo->DoCmd($sql);
#                   $trackProration = $sth->rows;
#                   $row->{ kColumnTrackProrate() } = $trackProration;
#                   report("D: FB15593 clientID $clientID: albumID($rsAlbumID) trackID($rsTrackID) --> proration($trackProration)"); # XXX
#               }

            } else {
               appendString( $errorCode, "Invalid trackID");
               ++$excount{trackid_not_found};
            }
         } else {
            #---------------
            # Find the track
            #---------------
            my %tArgs = (
               track_name => $trackName,
               album_name => $albumName,
            );
            $tArgs{catalog_number} = $catalogNumber if ( $catalogNumber && '' ne $catalogNumber );

            ($trackID, $errFlag) = $gSearchObj->findTrack( %tArgs );

            if ( $errFlag ) {
               report("   errFlag($errFlag) ***");
               if ( $errFlag == Support::Implementation::SearchUtil::kTrackNotFound ) {
                  report("ERROR: findTrack returned track not found : ". Dumper(\%tArgs) );
                  appendString( $errorCode, "Track not found");
                  ++$excount{track_not_found};
               } elsif ( $errFlag == Support::Implementation::SearchUtil::kAlbumNotFound ) {
                  report("ERROR: findTrack returned album not found : ". Dumper(\%tArgs) );
                  appendString( $errorCode, "Album not found");
                  ++$excount{album_not_found};
               } elsif ( $errFlag == Support::Implementation::SearchUtil::kNonUniqueAlbumName ) {
                  appendString( $errorCode, "Non-unique album name");
                  ++$excount{nonunique_album_name};
               } elsif ( $errFlag == Support::Implementation::SearchUtil::kDuplicateTrack ) {
                  appendString( $errorCode, "Duplicate track title");
                  ++$excount{duplicate_track_title};
               } else {
                  die("ERROR: _findTrack returned unknown errorFlag($errFlag)\n");
               }
            } else {
               report("   trackID($trackID)");
            }

         }


         # Make sure we have a track proration (FB12481)
         #
         if ( !$trackProration || '' eq $trackProration )
         {
            # Calculate track proration on the fly (FB15593) XXX XXX XXX
            # Note that for now, this is enabled on a per-client basis: 294 (eOne), 372 (TheOrchard)
            # Enabled auto-proration for 390 (Naxos; FB19532)
            # Enabled auto-proration for 403 (Red Bull; FB19666)
            # Enabled auto-proration for 394 (Light In The Attic; FB20314)
            # We're making this a feature for everyone (FB20519)
            #

            if( $trackID )
            {
               my $tObj = RPS::DB::Item::Track->Lookup( track_id => $trackID );

               my $_albumID = $tObj->album_id;
               my $sql = "SELECT track_id FROM track WHERE album_id = $_albumID";
               my $sth = $dbo->DoCmd($sql);
               $trackProration = $sth->rows;
               $row->{ kColumnTrackProrate() } = $trackProration;
               report("D: FB15593 clientID $clientID: albumID($_albumID) trackID($trackID) --> proration($trackProration)"); # XXX
            }
            #else
            #{
            #   report("D: FB15593 clientID $clientID: track not found, suppressing missing track proration message"); # XXX
            #}
         }
      }

      #----------------------------------------
      # Stop processing if there are any errors
      #----------------------------------------
      if ( $errorCode && '' ne $errorCode ) {
         $row->{'error-code'} = $errorCode if ( $errorCode && '' ne $errorCode );
         ++$rowcount{rows_failed};
         next;
      }

      #--------------------------------------------
      # Create the appropriate contract association
      #--------------------------------------------
      if ( $albumID ) {
         my %args = (
            artist_contract_id => $artistContractID,
            album_id => $albumID,
         );
         # limit search to albumID and contractID
         #$args{cross_collateralized} = 1 if ( $albumCrossed && $albumCrossed =~ /y/i );

         my $acID;
         my $acObj = RPS::DB::Item::AlbumContract->Lookup( %args );
         if ( !$acObj ) {
            if ( $execMode ) {

               #$args{cross_collateralized} = 1 if ( $albumCrossed && $albumCrossed =~ /x/i );

               #if ( ($albumCrossed && $albumCrossed =~ /x/i ) or
               #     ($trackCrossed && $trackCrossed =~ /x/i ) ) 
               if ( _isCrossed($albumCrossed) || _isCrossed($trackCrossed) ) {
                  $args{cross_collateralized} = 1;
               }

               $acObj = RPS::DB::Item::AlbumContract->Create( %args );
               $acObj->save();
               $acID = $acObj->album_contract_id;
               ++$entities{album_contract};
               report("  Created album_contract $acID : ". Dumper(\%args));
               $row->{'rs-albumcontract-id'} = $acID;
            } else {
               report("  Non-exec mode, skipping album_contract creation: ". Dumper(\%args));
            }
         } else {
            $acID = $acObj->album_contract_id;
            report("   WARNING: album_contract $acID exists!!! : ".Dumper(\%args));
            appendString( $errorCode, "Already attached to album");
            ++$excount{already_attached_to_album};
         }
      } elsif( $trackID ) {
         my %args = (
            artist_contract_id => $artistContractID,
            track_id => $trackID,
         );
         # limit search to trackID and contractID
         #$args{cross_collateralized} = 1 if ( $trackCrossed && $trackCrossed =~ /y/i );
         #$args{prorate_track_count} = $trackProration if ( $trackProration && '' ne $trackProration );
         my $tcID;
         my $tcObj = RPS::DB::Item::TrackContract->Lookup( %args );
         if ( !$tcObj ) {
            if ( $execMode ) {

               #$args{cross_collateralized} = 1 if ( $trackCrossed && $trackCrossed =~ /x/i );
               $args{cross_collateralized} = 1 if ( _isCrossed($trackCrossed) );

               $args{prorate_track_count} = $trackProration if ( $trackProration && '' ne $trackProration );
               $tcObj = RPS::DB::Item::TrackContract->Create( %args );
               $tcObj->save();
               $tcID = $tcObj->track_contract_id;
               ++$entities{track_contract};
               report("  Created track_contract $tcID : ". Dumper(\%args));
               $row->{'rs-trackcontract-id'} = $tcID;
            } else {
               # include these keys so that we have a complete arg list in the log
               #
               $args{cross_collateralized} = 1 if ( _isCrossed($trackCrossed) );
               $args{prorate_track_count} = $trackProration if ( $trackProration && '' ne $trackProration );
               report("  Non-exec mode, skipping track_contract creation: ". Dumper(\%args));
            }
         } else {
            $tcID = $tcObj->track_contract_id;
            report("   WARNING: track_contract $tcID exists!!! : ".Dumper(\%args));
            appendString( $errorCode, "Already attached to track");
            ++$excount{already_attached_to_track};
         }
      } else {
         die("ERROR: neither albumID nor trackID is set!!!\n");
      }

      #----------------------------------------------------------------------
      # If any errors occurred during the album/track creation, log them here
      #----------------------------------------------------------------------
      if ( $errorCode && '' ne $errorCode ) {
         $row->{'error-code'} = $errorCode if ( $errorCode && '' ne $errorCode );
         ++$rowcount{rows_failed};
      }

      
      ++$contractCounter;
   }# template row loop

   report("DONE reading template rows...");

   _showExceptions( $rows );

   #-----------------------
   # Show import statistics
   #-----------------------
   report("##### S U M M A R Y #####");
   report("Entities Created:");
   foreach my $c (keys %entities) {
      my $v = $entities{$c};
      printf("%30s %6d\n", $c, $v);
   }
   report("Exceptions:");
   my $totalExceptions = 0;
   my $totalRows = (scalar @$rows);

   foreach my $c (keys %excount) {
      my $v = $excount{$c};
      printf("%30s %6d\n", $c, $v);
      $totalExceptions += $v;
   }
   printf("%30s %s\n", " ", "-------" );
   printf("%30s %6d\n", "Total Exceptions", $totalExceptions );
   printf("%30s %6d\n", "Total Rows", $totalRows );
   report(" ");

   report("Row Processing Summary:");
   foreach my $c (sort{ $a cmp $b } keys %rowcount) {
      my $v = $rowcount{$c};
      printf("%30s %6d\n", $c, $v);
   }

}#_processData

# _isCrossed: returns 1 if passed value is "x" or starts with "y"
sub _isCrossed {
   my($v) = @_;
   my $retval;
   if ($v && ($v =~ /^x/i || $v =~ /^y/i ) ) {
      $retval = 1;
   }
   return $retval;
}

# _validCrossed: returns 1 if passed value is a valid crossing value
# (e.g., is blank, "x", or starts with "y" or "n")
sub _validCrossed {
   my($v) = @_;
   my $retval;
   if ($v && ($v =~ /^x/i || $v =~ /^y/i || $v =~ /^n/i ) ) {
      $retval = 1;
   }
   $retval = 1 if ( !$v );

   return $retval;
}

sub _showExceptions {
   my ( $rows ) = @_;

   #---------------------------
   # Build the exception header
   #---------------------------
   my @header;
   for( my $i = 0; $i < (keys %gColumnMap); $i++ ) {
      # Get the key at the specified column
      my $v = $gColumnMap{$i};
      next if ( !$v ); # strip blank columns
      #----------------------------------------------------------
      # Do _not_ push the "Error Code" or "import-status" columns
      # if they were in the original template.  We'll re-create
      # them on the fly.
      #----------------------------------------------------------
      next if ( ("Error Code" eq $v) || ("import-status") eq $v );
      #
      push @header, $v;
   }
   #
   report("STATUS:\t".join("\t", @header, "Error Code", "import-status"));
   #--------------------------------------
   # Now dump out the rows that had errors
   #--------------------------------------
   foreach my $row (@$rows) {
      my $rowid = $row->{rowid};
      my $errorCode = $row->{'error-code'};
      my $warningCode = $row->{'warning-code'};

      my $albumContractID = $row->{'rs-albumcontract-id'};
      my $trackContractID = $row->{'rs-trackcontract-id'};
      #my $licenseID = $row->{'rs-license-id'} || '---';
   
      #----------------------------------------
      # Output the row data in the proper order
      #----------------------------------------
      my @obuf;
      for( my $i = 0; $i < (keys %gColumnMap); $i++ ) {
         # Get the key at the specified column
         my $v = $gColumnMap{$i};
         next if ( !$v ); # strip blank columns
         my $val = (defined $row->{$v}) ? $row->{$v} : '';
         #----------------------------------------------------------
         # Do _not_ push the "Error Code" or "import-status" columns
         # if they were in the original template.  We'll re-create
         # these columns below.
         #----------------------------------------------------------
         next if ( ("Error Code" eq $v) || ("import-status") eq $v );
   
         push @obuf, $val;
      }
   
      my $importStatus;
      my $ecString;

      if ( $errorCode ) {
         $importStatus = "__FAIL__";
         $ecString = ($errorCode) ? $errorCode : '';
      } else {
         $importStatus = "___XXX___";
         $importStatus = "albumcontract($albumContractID)" if ( $albumContractID );
         $importStatus = "trackcontract($trackContractID)" if ( $trackContractID );
         $ecString = ($warningCode) ? $warningCode : '';
      }

      my $zzbuf = join("\t", "STATUS:", @obuf, $ecString, $importStatus);
      report($zzbuf);
   }
} #_showExceptions

#sub _xappendString {
#   my($str,$v) = @_;
#
#   if ( $str ) {
#      my $cur = $str;
#      my $newstring = "$cur; $v";
#      $_[0] = $newstring;
#   } else {
#      $_[0] = $v;
#   }
#}# _xappendString
#
#sub report {
#   my($text, $level) = @_;
#   $level = kNormal unless $level;
#   if ( $level <= $gReportLevel ) {
#      print $text . "\n";
#   }
#}

1;
