package Support::Implementation::LicenseIncomeTemplate;
# 11/10/09 -- Added logic to prevent importer from dying if
# the expense type information is not specified.
# 5/14/10 -- Added 'rs-contract-id' support
# 5/19/10 -- Stripped whitespace from percentage columns; added
#   check to make sure that a percentage is present if a type
#   is specified.
# 8/31/11 -- Improved log message for payee not found
# 9/20/11 -- Added check for duplicate payee names
# 9/23/11 -- Added logic to iterate over duplicated payee names
# 6/30/15 -- Added XLSX support.
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 Common::Assert;
use Common::UTF8;
use Common::RSApp;
use Common::Consts;
use Common::Util qw( clean trimspaces);

use RPS::DB::Item::Expense;
use RPS::DB::Item::ExpenseName;
use RPS::DB::Item::ExpenseType;
#use RPS::DB::Item::AlbumContract;
use RPS::DB::Item::NewArtistContract;

use RPS::DB::Item::LicenseIncomeType;
use RPS::DB::Item::ArtistContractLicenseIncome;

use Support::Implementation::ImplementationUtil qw( report appendString checkMissingColumn printNull );

use RPS::DB::Item::Album;
use RPS::DB::Item::Track;

use Support::Implementation::ExcelReader;
use Support::Implementation::Excel2007Reader;
use Support::Implementation::TabDelimitedReader;
use Support::Implementation::SearchUtil;

use base 'Support::Implementation::Template';

use constant kQuiet  => 0;
use constant kNormal => 1;
use constant kDebug  => 3;
my $gReportLevel = kNormal;

binmode STDOUT, ":utf8";

#-----------------------------------------------------------------------
# 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
#-----------------------------------------------------------------------
my %gTemplateHeader = (
   "*Contract Title"                   => 0,  # A
   "Payee Name"                        => 1,  # B
   "Licensing Income Type"             => 2,  # C
   "License Income Net Revenue Rate %" => 3,  # D
   "Recoupable Expense Type"           => 4,  # E
   "Recoupable %"                      => 5,  # F
);

#--------------------------------------------------------------
# Maps column #'s to a unique key (headername).  The reverse of
# templateHeader, but with actual column #.
#--------------------------------------------------------------
my %gColumnMap = ();

#--------------------------------------------------------
# gHeaderDisplayed is a flag that we set if we've already
# displayed the template header during an exceptions dump
#--------------------------------------------------------
my $gHeaderDisplayed;

my $clientID;
my $execMode;

my $dbo;

sub new {
   my ($class, %args) = @_;
   my $self = bless {}, $class;
   return $self->_init(%args);
}


sub _init {
   my( $self, %args ) = @_;

   report("LicenseIncomeTemplate::_init -- args = ". Dumper(\%args));

   $self->SUPER::_init(%args);

   $execMode = $self->exec_mode if ( $self->exec_mode );

   return $self;
}

sub parseHeader {
   my $self = shift;
}

#----------------------------------
# Contains map of trackIDs to ISRCs
#----------------------------------
my %gTrackISRCMap;

# Reference to SearchUtil object
my $gSearchObj;

#----------------------------------------------
# Read-in all of the template data into memory.
#----------------------------------------------
sub loadMemory {
   my $self = shift;

   $clientID = $self->client_id;
   my $app = Common::RSApp->new( clientID => $clientID );

   $dbo = Common::RSApp::GetClientDB();

   my %a = (
      clientID => $clientID
   );

   #$gSearchObj = Support::Implementation::SearchUtil->new( %a );

   my $fileName = $self->name;

   if( $self->isExcel2003( $fileName ) || $self->isExcel2007($fileName) ) {

      my %data;
      my $reader;

      if( $self->isExcel2003( $fileName ) )
      {
         print("LicenseIncomeTemplate::loadMemory -- loading Excel2k3 $fileName into memory\n");

         # Read in the header
         $reader = Support::Implementation::ExcelReader->new(
            filename => $fileName,
            data => \%data,
            header => \%gTemplateHeader,
            columnmap => \%gColumnMap
         );
         $reader->scanExcelFile;

         # Try and parse it...
         _processData(\%data);
      }
      elsif( $self->isExcel2007( $fileName ) )
      {
         print("LicenseIncomeTemplate::loadMemory -- loading Excel2k7 $fileName into memory\n");

         # Read in the header
         $reader = Support::Implementation::Excel2007Reader->new(
            filename => $fileName,
            data => \%data,
            header => \%gTemplateHeader,
            columnmap => \%gColumnMap
         );
         $reader->scanExcelFile;

         # Try and parse it...
         _processData(\%data);
      }

   } elsif( $self->isTabDelimited( $fileName ) ) {
      report("LicenseIncomeTemplate::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 license
# data.
#-------------------------------------------------------------------
sub _processData {
   my($data) = @_;
   my $rows = $data->{rows};

   #----------------------------------
   # excount keeps track of exceptions
   #----------------------------------
   my %excount = (
   );

   #--------------------------------------
   # rowcount keeps track of line counters
   #--------------------------------------
   my %rowcount = ();

   #--------------------------------------
   # count keeps track of entities created
   #--------------------------------------
   my %count = (
      expense_type                   => 0,
      expense_name                   => 0,
      artist_contract_license_income => 0,
      license_income_type            => 0,
   );

   use constant kColumnContractTitle             => '*Contract Title';
   use constant kColumnArtistPayee               => 'Payee Name';
   use constant kColumnLicensingIncomeType       => 'Licensing Income Type';
   use constant kColumnLicensingIncomeNetRevRate => 'Licensing Income Net Revenue Rate %';
   use constant kColumnRecoupableExpenseType     => 'Recoupable Expense Type';
   use constant kColumnRecoupablePercent         => 'Recoupable %';

   use constant kColumnRSContractID              => 'rs-contract-id';  # TBD

   #-----------------------------------------------------------------------
   # 'columns' contains the list of columns that _must_ be in the template,
   # regardless of whether or not they contain any data.
   #-----------------------------------------------------------------------
   my @columns;
   push @columns, kColumnContractTitle;
   push @columns, kColumnArtistPayee;
   push @columns, kColumnLicensingIncomeType;
   push @columns, kColumnLicensingIncomeNetRevRate;
   push @columns, kColumnRecoupableExpenseType;
   push @columns, kColumnRecoupablePercent;

   #-----------------------------
   # Loop over each row (payee)
   #-----------------------------

   foreach my $row (@$rows) {

      #-----------------------------------
      # Get all of the template variables.
      #-----------------------------------
      my $rowid                     = $row->{'rowid'};

      my $contractTitle             = $row->{ kColumnContractTitle()             };
      my $payeeName                 = $row->{ kColumnArtistPayee()               };
      my $licensingIncomeType       = $row->{ kColumnLicensingIncomeType()       };
      my $licensingIncomeNetRevRate = $row->{ kColumnLicensingIncomeNetRevRate() };
      my $recoupableExpenseType     = $row->{ kColumnRecoupableExpenseType()     };
      my $recoupablePercent         = $row->{ kColumnRecoupablePercent()         };

      my $rsContractID              = $row->{ kColumnRSContractID()              };

      #------------------------------------------------
      # errorCode will hold zero or more error messages
      #------------------------------------------------
      my $errorCode;

      report("#### row($rowid) ".Dumper(\%$row));
      report("  DEBUG: contract(".   printNull($contractTitle)             .") "
         . "licIncomeType(".         printNull($licensingIncomeType)       .") "
         . "licIncomeRate(".         printNull($licensingIncomeNetRevRate) .") "
         . "recoupableExpenseType(". printNull($recoupableExpenseType)     .") "
         . "recoupablePercent(".     printNull($recoupablePercent)         .") "
         . "rsContractID(".     printNull($rsContractID)         .") "   # 5/14/10
      );

      ++$rowcount{total};

      #--------------------------------------------------------
      # Make sure all columns are in place and if the column is
      # prefixed with an asterisk, that it actually has data in
      # it.
      #--------------------------------------------------------
      my $s;
      foreach my $c (@columns) {

         # TODO: Should use ImplementationUtil:checkMissingColumn ...
         $s = _checkMissingColumn( $c, $row, \%excount );
         appendString( $errorCode, $s ) if ( $s );
      }

      #-----------------------
      # Remove unneeded spaces
      #-----------------------
      $payeeName              =~ s/^\s*//g if ( $payeeName );
      #$contractTitle          =~ s/^\s*//g if ( $contractTitle );
      $contractTitle          =~ s/\s*$//g if ( $contractTitle );
      $licensingIncomeType    =~ s/^\s+|\s+$//g if ( $licensingIncomeType ); # remove leading/trailing spaces
      $recoupableExpenseType  =~ s/^\s*//g if ( $recoupableExpenseType );
      $recoupablePercent      =~ s/^\s*//g if ( $recoupablePercent );
      $licensingIncomeNetRevRate  =~ s/^\s*//g if ( $licensingIncomeNetRevRate );

      # Clean-up
      $recoupablePercent          =~ s/%// if ( $recoupablePercent );
      $licensingIncomeNetRevRate  =~ s/%// if ( $licensingIncomeNetRevRate );

      #================================================#
      #                                                #
      #   Validate the percentage values, if present   #
      #                                                #
      #================================================#
      if ( $recoupablePercent &&
          ($recoupablePercent < 0 || $recoupablePercent > 100) ) {
         appendString( $errorCode, "Recoupable Percent out of range" );
         ++$excount{percent_out_of_range};
      }
      if ( $licensingIncomeNetRevRate &&
          ($licensingIncomeNetRevRate < 0 || $licensingIncomeNetRevRate > 100) ) {
         appendString( $errorCode, "Licensing Income Percent out of range" );
         ++$excount{lic_income_percent_out_of_range};
      }

      if ( $licensingIncomeType and '' ne $licensingIncomeType and !$licensingIncomeNetRevRate ) {
         appendString( $errorCode, "Missing License Income Percentage" );
         ++$excount{lic_income_percent_missing};
      }

      if ( $recoupableExpenseType and '' ne $recoupableExpenseType and !$recoupablePercent ) {
         appendString( $errorCode, "Missing Recoupable Percentage" );
         ++$excount{recoupable_percent_missing};
      }

#      if ( !$recoupablePercent and (!$recoupableExpenseType or '' eq $recoupableExpenseType)) 
      if ( $recoupablePercent and (!$recoupableExpenseType or '' eq $recoupableExpenseType)) {
         appendString( $errorCode, "Missing Recoupable Expense Type" );
         ++$excount{recoupable_expense_type_missing};
      }

      if ( $licensingIncomeNetRevRate and (!$licensingIncomeType or '' eq $licensingIncomeType)) {
         appendString( $errorCode, "Missing Licensing Income Type" );
         ++$excount{licensing_income_type_missing};
      }

      #===========================#
      #                           #
      #    Match Expense Info     #
      #                           #
      #===========================#

      #-------------------------------------------------------------------
      # Check if the expense type is valid.  Note: the name of the expense
      # is actually stored in the expense_name table, not expense_type.
      #
      # We assume that the expense name has already been setup prior to
      # trying to import expenses.
      #
      # Note: we check to see if the expense type has been setup on a
      # contract below.
      #-------------------------------------------------------------------
      my $expenseNameID;

      if ( $recoupableExpenseType && '' ne $recoupableExpenseType ) {
         my $enObj = RPS::DB::Item::ExpenseName->Lookup( name => $recoupableExpenseType );
         if ( not defined $enObj ) {

            #-------------------------------------------------------------
            # If the expense_name doesn't exist, then we need to create it
            # so that it can be used by the contract.  We do this below,
            # after we've located the contract (assuming that there were
            # no errors on the current template line).
            #-------------------------------------------------------------

         } else {
       
            $expenseNameID = $enObj->expense_name_id;
         }
      }

      #======================================================#
      #                                                      #
      #                Match Artist Payee                    #
      #                                                      #
      # Validate and match the artist payee information      #
      #                                                      #
      # This field is optional, but can only be filled-in if #
      # a contract title is filled-in as well.               #
      #                                                      #
      #======================================================#
      my $artistPayeeID; # set if the payee is found

      
      my @payeeList; # contains the payeeID(s) matching the payee name

      # If the contractID was specified, then skip the payee lookup -5/14/10
      if ( !$rsContractID ) {

         if ( $payeeName && '' ne $payeeName ) {

            if ( !$contractTitle || '' eq $contractTitle ) {
   # Removed 11/10/09 -- if we have a payeeID with no contract, then the
   # template line will apply to all of the payee's contracts.
   #            appendString( $errorCode, "Payee without contract" );
   #            ++$excount{payee_without_contract};
            } else {

               my $sql = "SELECT artist_payee_id, name FROM artist_payee "
                  . "WHERE name=" . $dbo->DBQuote($payeeName);
               my $sth = $dbo->DoCmd($sql);
               if ( $sth->rows == 0 ) {
                  appendString( $errorCode, "Payee not found" );
                  ++$excount{payee_not_found};
                  report("DEBUG:MATCH_ARTIST payee($payeeName) not found");
   # Removed 11/10/09 per CB.
   #            } elsif( $sth->rows > 1 ) {
   #               appendString( $errorCode, "Duplicate payee" );
   #               ++$excount{duplicate_payee};

   # 9/20/11: Adding duplicate payee check back in..
               } elsif( $sth->rows >= 1 ) {
#                  appendString( $errorCode, "Duplicate payee" );
#                  ++$excount{duplicate_payee};
#                  report("DEBUG:MATCH_ARTIST duplicate payee($payeeName)");

                  while( my($_payeeID) = $sth->fetchrow_array() )
                  {
                     push @payeeList, $_payeeID;
                  }

#               } else {
#                  my($zzPayeeID, $name) = $sth->fetchrow_array();
#                  $artistPayeeID = $zzPayeeID;
#                  report("DEBUG:MATCH_ARTIST found payee($name) payeeID($artistPayeeID)");
               }
            }

         } else {
            report("DEBUG:MATCH_ARTIST: no payee specified");
         }
      }
      # MATCH ARTIST PAYEE

      #=============================================#
      #                                             #
      #               Match Contract                #
      #                                             #
      # Validate and match the contract information #
      #                                             #
      #=============================================#
      my $contractID;


      #----------------------------------------------------------------
      # 'contracts' is a list of contractID(s) that will be affected by
      # the current template line.
      #----------------------------------------------------------------
      my @contracts;

      if ( $rsContractID && ('' ne $rsContractID) ) {

         #die("Hey, who said the contractID is active?!"); # XXX

         my $o = RPS::DB::Item::NewArtistContract->Lookup(
            artist_contract_id => $rsContractID,
         );
         if ( !$o ) {
            appendString( $errorCode, "Invalid contractID" );
            ++$excount{invalid_contractid};
         } else {
            $contractID = $o->artist_contract_id;
            push @contracts, $contractID;
         }

      } else {

         if ( $contractTitle && '' ne $contractTitle ) {

            #------------------------------------------------------------
            # Look for the contract.  If an artistPayeeID is available,
            # use it to further identify the contract, otherwise find
            # all contracts that have the specified contract title.
            #------------------------------------------------------------

            #----------------------------------------------------------------
            # Per CB, if the payee name wasn't unique, then we need
            # to examine all contract title - payee combinations. -ES 9/23/11
            #----------------------------------------------------------------
            my $payeeSQL;
            if ( @payeeList > 0 ) 
            {
               $payeeSQL = " AND artist_payee_id IN( ". join(",",@payeeList) . ") ";
            }

            my $sql = "SELECT artist_contract_id FROM new_artist_contract "
                    . "WHERE title="
                    . $dbo->DBQuote($contractTitle);

            #$sql .= " AND artist_payee_id=$artistPayeeID" if ($artistPayeeID);
            $sql .= $payeeSQL if ($payeeSQL);


            my $sth = $dbo->DoCmd($sql);

            if ( $sth->rows == 0 ) {
               if ( $artistPayeeID ) { # 8/23/10
                  report("_EXCEPTION: contract/payee combination not found '$contractTitle' :\n$sql");
                  appendString( $errorCode, "Contract title/payee combination not found" );
                  ++$excount{contract_title_payee_not_found};
               } else {
                  report("_EXCEPTION: contract not found '$contractTitle' :\n$sql");
                  appendString( $errorCode, "Contract not found" );
                  ++$excount{contract_not_found};
               }
            #} elsif( $sth->rows > 1 ) {
            #   report("_EXCEPTION: contract title not unique '$contractTitle'");
            #   appendString( $errorCode, "Duplicate contract" );
            #   ++$excount{duplicate_contract_title};
            } else {
               while( my ($contractID) = $sth->fetchrow_array() ) {
                  push @contracts, $contractID;
               }

               # XXX - DEBUG
               if ( @payeeList > 1 )
               {
                  report("   DEBUG: duplicate payee '$payeeName' matched ". (scalar @contracts)
                     . " contract(s) named '$contractTitle' : " . join(",",@contracts) );
               }
            }

         } else {

            #-----------------------------------------------------------------
            # 11/10/09 - If the contract title wasn't specified, exception out
            #-----------------------------------------------------------------
            appendString( $errorCode, "Missing contract title" );
            ++$excount{missing_contract_title};

#            #----------------------------------------------------------------
#            # If neither a contract title or artist payee was specified, then
#            # the template lines apply to _ALL_ artist contracts.
#            #----------------------------------------------------------------
#
#            #appendString( $errorCode, "Missing contract title" );
#            #++$excount{missing_contract_title};
#            my $contractSQL;
#            if ( !$artistPayeeID ) {
#               report("   DEBUG: no title or payee; defaulting to ALL contracts");
#            } else {
#               $contractSQL = "SELECT * FROM new_artist_contract "
#                  . "WHERE artist_payee_id=$artistPayeeID";
#            }
#
#            my $col = RPS::DB::Item::NewArtistContract->GetAll($contractSQL);
#my $limit=0;
#
#            while( my $c = $col->next() ) {
#
## XXX XXX XXX XXX XXX XXX XXX
##++$limit;
##next if ( $limit > 3 );
## XXX XXX XXX XXX XXX XXX XXX
#
#               push @contracts, $c->artist_contract_id;
#            }

         }
      }# MATCH CONTRACT

if ( @contracts ) {
   report("   DEBUG: Working with following contract(s):");
   report( "   " . join(", ",@contracts) );
}

      #=========================================#
      #                                         #
      #      Match Licensing Income Type        #
      #                                         #
      #   Look for the licensing income type.   #
      #                                         #
      #=========================================#
      my $licenseIncomeTypeID; # set if we can find the income type

      if ( $licensingIncomeType && '' ne $licensingIncomeType) {

         report("DEBUG:MATCH_INCOME_TYPE: searching for licensing income type...");


         my $iObj = RPS::DB::Item::LicenseIncomeType->Lookup(
            name => $licensingIncomeType,
         );
         if ( $iObj ) {
            $licenseIncomeTypeID = $iObj->license_income_type_id;
         } else {
            report("DEBUG:MATCH_INCOME_TYPE: licensing income type '$licensingIncomeType' not found...");
         }

      } else {
         report("DEBUG:MATCH_INCOME_TYPE: licensing income type not specified ... ");
         
         #---------------------------------------------------------------------
         # Sanity check -- if neither the licensing income type nor recoupable
         # expense type were specified, then what exactly are we supposed to do
         # with the contract?
         #---------------------------------------------------------------------

         #if ( !$contractTitle || '' eq $contractTitle )
         if ( !$recoupableExpenseType || '' eq $recoupableExpenseType ) {
            appendString( $errorCode, "Missing licensing income type and recoupable expense type" );
            ++$excount{missing_licensing_income_recoupable_types};
         }

      }# MATCH INCOME TYPE

      #=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      #                                                                   #
      # If there are any errors at this point, stop processing this line  #
      #                                                                   #
      #=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      if ( $errorCode ) {
         $row->{'error-code'} = $errorCode;
         ++$rowcount{rows_failed};
         next;
      }

      #--------------------------------------------------------------------
      # If a contract was not specified, then the we'll setup the license
      # income type and/or recoupable expense type on all contracts.
      #
      # If the contract was specified (but not the payee), then we'll setup
      # the license income type and/or recoupable expense type on all
      # contracts with that name.
      #
      # If the payee name was specified along with the contract, then setup
      # the license income type and/or recoupable expense type only on the
      # contract uniquely identified by the payee and contract title.
      #
      # WARNING: If there are multiple contracts being processed and an
      # error is found on one but not the others, then the others will be
      # changed while the one with an error generates an error.
      #--------------------------------------------------------------------

      foreach my $contractID (@contracts) {

         report("  ## Processing contract($contractID)");
         #----------------------------------------------------------------
         # entityID -- if we create an artist_contract_license_income,
         # license_income_type, expense_name and/or expense_type entity,
         # then we'll store the resulting ID information in 'entityID'.
         # If will be used later when generating the import status report.
         #----------------------------------------------------------------
         my $entityID;

         #----------------------------------------------------------
         #
         # Setup the recoupable expense type on the current contract
         #
         #----------------------------------------------------------
         if ( $recoupableExpenseType && '' ne $recoupableExpenseType ) {
            if ( !$expenseNameID ) {
               # Setup the expense name
               if ( $execMode ) {
                  my $o = RPS::DB::Item::ExpenseName->Create(
                     name => $recoupableExpenseType,
                  );
                  $o->save();
                  $expenseNameID = $o->expense_name_id;

                  report("   Created expense_name $expenseNameID "
                     . "'$recoupableExpenseType'");

                  ++$count{expense_name};
                  appendString( $entityID, "expenseName($expenseNameID)" );

               } else {
                  report("   Non-exec mode, skipping expense_name creation for '$recoupableExpenseType'");
               }
            }

            if ( $expenseNameID ) {
               # 8/23/09 - Per CB, if the expense type exists on the contract (regardless
               # of percentage) then this is an exception.
               #

               # 11/10/09 - Per CB, if the expense type and percentage doesn't exist on the
               # contract, we'll go ahead and create it.  E.g., the expense type can be
               # associated with different percentages on the same contract.
               my %args = (
                  expense_name_id    => $expenseNameID,
                  artist_contract_id => $contractID,
                  inactive           => 0,
                  #percent            => $recoupablePercent, 8/23
               );
               my $o = RPS::DB::Item::ExpenseType->Lookup( %args );
               if ( !$o ) {

                  # assume that the expense name is unique on the contract.
                  # 11/10/09 - Per CB, for the sake of determining uniqueness,
                  # we look at the expense type, contract and percentage.
                  # Hence the following line is redundant.
                  $args{percent} = $recoupablePercent;

                  if ( $execMode ) {
                     my $o = RPS::DB::Item::ExpenseType->Create( %args );
                     $o->save();
                     my $expenseTypeID = $o->expense_type_id;
                     report("   Created expense_type $expenseTypeID : ".Dumper(\%args));

                     ++$count{expense_type};
                     appendString( $entityID, "expenseType($expenseTypeID)" );

                  } else {
                     report("   Non-exec mode, skipping expense_type creation : ".Dumper(\%args));
                  }
               } else {
                  my $expenseTypeID = $o->expense_type_id;
                  my $existingPct = $o->percent;
                  report("   Error: expense_type $expenseTypeID already exists on contract $contractID!!!");

                  appendString( $errorCode, "Expense '$recoupableExpenseType $existingPct%' already exists on contract $contractID" );
                  #appendString( $errorCode, "Expense '$recoupableExpenseType' already exists on contract $contractID" );
                  ++$excount{expense_type_exists};

                  # Removed 11/10/09 per CB
                  #my $currentPercent = $o->percent;
                  #
                  #report("   Existing percent($currentPercent) ?= template($recoupablePercent)");
                  #
                  #if ( $currentPercent != $recoupablePercent ) {
                  #   appendString( $errorCode, "Recoupable percentage mismatch on contract $contractID" );
                  #   ++$excount{expense_type_recoupable_percentage_mismatch};
                  #}
               }
            }
         }

         #----------------------------------------------
         #
         # Setup the license income type on the contract
         #
         #----------------------------------------------
         if ( $licensingIncomeType && '' ne $licensingIncomeType ) {
            if ( !$licenseIncomeTypeID ) {
               # Setup the license income type
               if ( $execMode ) {
                  my $o = RPS::DB::Item::LicenseIncomeType->Create(
                     name => $licensingIncomeType,
                  );
                  $o->save();
                  $licenseIncomeTypeID = $o->license_income_type_id;

                  report("   Created license_income_type $licenseIncomeTypeID "
                     . "'$licensingIncomeType'");

                  ++$count{license_income_type};
                  appendString( $entityID, "licenseIncomeType($licenseIncomeTypeID)" );

               } else {
                  report("   Non-exec mode, skipping license_income_type creation for '$licensingIncomeType'");
               }
            }

            if ( $licenseIncomeTypeID ) {
               # 11/10/09 - Per CB, if the license income type and percentage
               # doesn't exist on the contract, we'll go ahead and create it.
               # E.g., the license income type can be associated with different
               # percentages on the same contract.
               #my %args = (
               #   license_income_type_id => $licenseIncomeTypeID,
               #   artist_contract_id     => $contractID,
               #   percent                =>  $licensingIncomeNetRevRate,
               #);

               # 8/23/10 - Per CB, if the license income type specified on the
               # template exists on the # contract (regardless of the percentage)
               # then this is an error.
               #
               my %args = (
                  license_income_type_id => $licenseIncomeTypeID,
                  artist_contract_id     => $contractID,
                  inactive               => 0,
               );
               my $o = RPS::DB::Item::ArtistContractLicenseIncome->Lookup( %args );
               if ( !$o ) {
                  $args{percent} = $licensingIncomeNetRevRate;
                  if ( $execMode ) {
                     $o = RPS::DB::Item::ArtistContractLicenseIncome->Create( %args );
                     $o->save();
                     my $id = $o->artist_contract_license_income_id;
                     report("   Created artist_contract_license_income $id : ".Dumper(\%args));

                     ++$count{artist_contract_license_income};
                     appendString( $entityID, "artistContractLicenseIncome($id)" );

                  } else {
                     report("   Non-exec mode, artist_contract_license_income creation : ".Dumper(\%args));
                  }
               } else {
                  my $id = $o->artist_contract_license_income_id;
                  my $pct = $o->percent;
                  report("   Warning: artist_contract_license_income $id already exists!!!");

                  appendString( $errorCode, "License income '$licensingIncomeType $pct%' already exists on contract $contractID" );
                  #appendString( $errorCode, "License income '$licensingIncomeType' already exists on contract $contractID" );
                  ++$excount{license_income_exists};
               }
            }
         }

         # Removed 11/10/09 per CB.
         #--------------------------------------------------------------------
         # It's possible for a template line to apply to multiple contracts,
         # and error-out on some (not all) of the contracts.  By setting
         # the error-code field, the error(s) will be exposes on the contracts
         # that had errors.
         #--------------------------------------------------------------------
         if ( $errorCode ) {
            $row->{'error-code'} = $errorCode;
            #++$rowcount{rows_failed};
         }

         # XXX
         # Note: this doesn't work as expected if there are multiple contracts
         # being affected by the current template row.  Basically, you only see
         # the entityIDs of the last contract that we process with the row.
         # XXX
         $row->{'rs-entity-id'} = $entityID;

      }# contract loop

      ++$rowcount{rows_imported};

   }#row loop



   #------------------------------------------------------------
   # Dump out the errors
   # TODO: Need to properly propagate the errors back to the user
   #------------------------------------------------------------
   _showExceptions( $rows );

   #-----------------------
   # Show import statistics
   #-----------------------
   report("##### S U M M A R Y #####");
   my $totalExceptions = 0;
   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 );
   report(" ");
   
   foreach my $c (keys %count) {
      my $v = $count{$c};
      printf("%30s %d\n", $c, $v);
   }

   report(" ");
   foreach my $c (keys %rowcount) {
      my $v = $rowcount{$c};
      printf("%30s %d\n", $c, $v);
   }
}#_processData

#------------------------------------------------------------------
# _checkMissingColumn - checks if a column is missing or not.  Also
# ensures that a required columns (prefixed with an asterisk) has
# data in it.  A string containing error(s) is returned, otherwise
# undef is returned if column is OK.
#------------------------------------------------------------------
sub _checkMissingColumn {
   my($colName, $rows, $excount) = @_;
   my $errStr;
   if ( ! exists $rows->{$colName} ) {
      report("_checkMissingColumn -- column '$colName' is missing");
      appendString($errStr, "Column '$colName' not found");

      my $colClean = clean($colName);
      my $key = "column_" . $colClean . "_not_found";
      ++$excount->{$key};
   }

   #------------------------------------------------------------------
   # If the column name starts with an asterisk then it can't be blank
   #------------------------------------------------------------------
#   if ( $colName =~ /^\*/ ) {
#      my $v = $rows->{$colName};
#      # strip out leading spaces
#      $v =~ s/^\s*//g;
#      if ( '' eq $v ) {
#         appendString($errStr, "Column '$colName' can't be blank");
#      }
#   }
   return $errStr;

}# _checkMissingColumn


#--------------------------------------------------------------------------
# _showExceptions was originally intended to _just_ show the template lines
# that exceptioned out.  It's been modified to output both exception and
# non-exception lines.  Exception message(s) will be placed in the error
# code column.  If a line is imported successfully, then the resulting
# expenseID will be stored in the import status column ("expenseID(###)"),
# otherwise this column will contain the string "__FAIL__".
#--------------------------------------------------------------------------
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};

      #----------------------------------------------------------
      # 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 $entityID = $row->{'rs-entity-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};
         my $val = ($row->{$v}) ? $row->{$v} : '';

         #----------------------------------------------------------
         # Do _not_ push the "Error Code" or "import-status" columns
         # if they were in the original template.
         #----------------------------------------------------------
         next if ( ("Error Code" eq $v) || ("import-status") eq $v );

         push @obuf, $val;
      }

      #my $importStatus = ($errorCode) ? "__FAIL__" : "expense($expenseID)";
      my $importStatus = ($errorCode) ? "__FAIL__" : "$entityID";

      my $ecString = ($errorCode) ? $errorCode : '';

      my $zzbuf = join("\t", "STATUS:", @obuf, $ecString, $importStatus);

      report($zzbuf);
   }
}#_showExceptions

#---------------------------------------------------
# _isValidDateFmt - return true if the supplied date
# is in the format YYYY-MM-DD, false otherwise
#---------------------------------------------------
sub _isValidDateFmt {
   my($dstr) = @_;
   my($yr,$mo,$dy) = split("-",$dstr);
   my $st = 1; # valid unless we detect otherwise

   $st = 0 if ( !$yr || !$mo || !$dy );
   $st = 0 if ( $mo && ( $mo !~ /^\d+$/ ));
   $st = 0 if ( $yr && ( $yr !~ /^\d+$/ ));
   $st = 0 if ( $dy && ( $dy !~ /^\d+$/ ));
   $st = 0 if ( length($yr) != 4 );
   return $st;
}

sub _reportError {
   my ($name, $obj) = @_;

   report("   _reportError: checking '$name' for errors");
   if ($obj && $obj->_hasError()) {
      #my ($e, $msg) = $obj->getError();
      #print "$name has an error: $e : $msg\n";

      my %xmlParams = $obj->getXMLParams();
      my $msg = defined $xmlParams{emsg} ? $xmlParams{emsg} : "MSG_NOT_AVAILABLE";
      my $e = $xmlParams{e};
      print "$name has an error:: $e :: $msg\n";
      return 1;
   }
   return undef;
}

1;
