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

package Report::Dynamic;
use strict;

use Data::Dumper;

use lib '/app/tools/appuser/lib';
use lib '/app/tools/common/lib';
use lib '/app/tools/report/lib';
use lib '/app/tools/rps/lib';
use lib '/app/tools/job/lib';
use base 'Common::XMLObject';

use Job::Job;
use RPS::Report::Dynamic::Factory;

#use IO::Handle;
use IO::File;
use Common::Log;
use Common::Date;
use Common::Assert;
use Common::Email;
use Common::Util;
use File::Path;

#use Text::CSV::Encoded;
use Text::CSV;
use Report::Dynamic::Parameters;
use Report::Dynamic::Factory;
use Report::DB::Item::Report;
use Report::DB::Item::ReportNotify;
use Report::Dynamic::Job::RunReport;
use AppUser::User::User;

use constant kMaxLines => 65535;

#
#  ((((((((((  Public Interface
#

# Returns the displayable Group name.
#
sub Group {
    my ($class) = @_;
    assert( 0, 'override to return the group name (pretty name)' );
}

# Returns the displayable Type name.
#
sub Type {
    my ($class) = @_;
    assert( 0, 'override to return the type name (pretty name)' );
}

# Returns the filename Type name.
#
sub FilenameType {
    return undef;
}

# How many days should the report live?
sub DaysToExpire { 30 }

sub UserLevelIsValid {
    my ( $class, $userLevel ) = @_;
    assert( 0, 'override' );
}

sub ClientTypeMask {
    my ($class) = @_;

    # override to return a type mask (built from Common::Client constants)
    # to be used to determine what clients can execute what commands.
    # In general I expect this to only return a single bit.
    #
    assert( 0, 'override' );
}

#
# To load a report with a reportID, use the Factory class!
# use Report::Factory;
# my $report = Report::Factory->Fetch($reportID);
#

# Search for a report that has not been run and has matching parameters.
sub Search {
    my ( $class, %args ) = @_;

    my $params = $args{parameters};
    assert($params);

    my $collection = Report::DB::Item::Report->GetAllNotComplete( group => $class->Group, type => $class->Type );

    while ( my $report = $collection->next() ) {

        # TODO, make this more generic.  We shouldn't have RPS specific stuff here.
        # This will be an issue when migrating this to bookpub land.
        my $obj = RPS::Report::Dynamic::Factory->Fetch( $report->report_id ) || next;
        return $obj if ( $obj->parameters() == $params );
    }

    return;
}

# $class->QueueReport(parameters => $parametersObj);
#
# Creates a new Report, and queues a Job to run it.
# Returns a reference to the Report object.
# - Some day we might want to return the Job object, too...
#
sub QueueReport {
    my ( $class, %args ) = @_;
    my $notifyUserID = $args{notify};

    my $params = $args{parameters};
    assert($params);

    # Note: On a duplicate notifyUserID should be added to report_notify if they
    #       aren't already there for that user.
    # $duplicateReport->addNotification( $notifyUserID ) if( $notifyUserID );
    my $duplicateReport = $class->Search( parameters => $params );
    if ($duplicateReport) {
        $duplicateReport->addNotification($notifyUserID) if ($notifyUserID);

        # !!! Just going to return the duplicate report for now.
        # !!! We may need to re-evaluate this if we need to produce some sort of
        # !!! message to the user that there is already a report with that characteristic.
        # !!! Perhaps at the very least we should hilight the new/old report.
        #
        return $duplicateReport;
    }

    # Create a fresh, _blank_ report object.
    #
    my $newReport = $class->new();

    # This is the only 'normal' place where the path and filename
    # get 'filled in'.
    #
    my $baseName = $args{filename};
    if ( !$baseName ) {
        $baseName = $params->ReportName();
        $baseName = $class->DefaultBaseName($params) if !$baseName;
        $baseName = _datifyName( $class, $baseName );
    }
    $newReport->_setFileName($baseName);

    my $path = $args{path};
    if ( !$path ) {
        $path = $class->DefaultFilePath();
    }
    $newReport->_setFilePath($path);

    $newReport->_setGroup( $class->Group() );
    $newReport->_setType( $class->Type() );

    # Call save on this sucker to create the database record.
    # (and generate an id).
    # - default state should be distinct...
    #
    $newReport->_save();

    my $id = $newReport->reportID();

    # Save the parameters, using the id we just retrieved.
    #
    $params->save($id);

    # Ok, so we have the initial database state set up.  Now we can create the Job and queue it.
    #
    my $newJob = Report::Dynamic::Job::RunReport->new( reportID => $id, subclass => $class->_JobSubclass() );
    $newJob->enqueue();

    # Update the report record with the new job id (and change the state to 'queued')
    #
    my $jobID = $newJob->id();
    $newReport->_setJobID($jobID);

    $newReport->_setStatus(Report::DB::Item::Report::kStatusWaiting);

    $newReport->addNotification($notifyUserID) if ($notifyUserID);

    return $newReport;
}

sub Timestamp {
    my $class = shift;
    my $date  = Common::Date->now();
    my ( $sec, $min, $hour ) = localtime(time);

    return sprintf( "%s_%02d%02d%02d", $date->asString("%Y%m%d"), $hour, $min, $sec );
}

sub DefaultBaseName {
    my ( $class, $params ) = @_;

    # This can be overridden, I suppose.
    #
    # We'll construct a name based on group and type for now.
    # Not sure if the spec spells this out...

    my $name = $class->FilenameType;

    if ( !$name ) {
        $name = $class->Type;
    }

    my @options = $params->specifiedOptions();
    if ( scalar @options ) {
        foreach my $option (@options) {

            #$option = Common::Util::clean_name($option);
            $name .= ' ' . $option;
        }
    }

    my @criteria = $params->specifiedCriteria();
    if ( scalar @criteria ) {
        foreach my $criterion (@criteria) {

            #$option = Common::Util::clean_name($option);
            $name .= ' ' . $criterion;
        }
    }

    return $name;
}

sub _datifyName {
    my ( $class, $name ) = @_;

    $name .= ' ' . substr( $class->Timestamp(), 0, 8 );
    $name =~ s/\s+/_/g;

    # Doing this once to try to remove any extra underscores.
    $name =~ s/_\W//g;

    # And again just to be safe.
    $name =~ s/\W//g;

    return $name;
}

sub ReportDir { '/app/shared/dynamic_reports' }
sub TempDir   { '/app/data/dynamic_reports' }

sub DefaultFilePath {
    my ($class) = @_;

    # This can be overridden, I suppose.
    #
    # We'll construct a name based on group and type for now.
    # Not sure if the spec spells this out...

    return $class->ReportDir . '/' . Common::RSApp::GetClientID();
}

sub DefaultTempFilePath {
    my ($class) = @_;

    return $class->TempDir . '/' . Common::RSApp::GetClientID();
}

# my $paramObj = $class->NewParameters();
#
# Returns a fresh ReportParameters object, configured
# to work correctly with this class of Report.
#
sub NewParameters {
    my ($class) = @_;

    return $class->_ParamClass()->new();
}

# my $paramsObj = $self->parameters();
#
# Returns a ReportParams object reference containing
# this Report's parameters.
#
sub parameters {
    my ($self) = @_;

    # We'll lazily cache these.
    #
    if ( !$self->{_parameters} ) {
        my $class = ref($self);
        $self->{_parameters} = $class->_ParamClass()->new( reportID => $self->reportID() );
    }
    return $self->{_parameters};
}

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

sub status { shift->{Status} }

sub filename { shift->{FileName} }

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

# $reportObj->expireReport();
#
# Remove a file from the file system and mark it as deleted if the file is expired
#
sub expireReport {
    my ($self) = @_;

    Log->info( " -- Checking if report id " . $self->reportID . " is expired." );
    Log->debug( "    ** Status: " . $self->{Status} );

    if ( $self->_isExpired() ) {
        Log->info("   - report expired.  removing.");
        $self->_removeReport();

        $self->_setStatus(Report::DB::Item::Report::kStatusDeleted);
    }
}

# Return a IO:Handle to a report file based on type
sub fetch {
    my $self = shift;
    my %args = @_;
    my $type = $args{type};

    my $path = $self->{FilePath} . "/" . $self->reportID . ".$type";

    assert($type);

    if ( -e $path ) {

        # JPK - It's a mistake to instantiate an IO::Handle directly in most cases.
        # We should use IO::File instead, which inherits from IO::Handle.
        #
        #        my $fh = new IO::Handle;
        #        open( $fh, $path ) || die "Unable to read file: $!";

        my $fh = IO::File->new( $path, 'r' );
        return $fh;
    }

    return;
}

# Add a user id to the notification list for status updates
sub addNotification {
    my $self = shift;
    my $userID = shift || die;

    Report::DB::Item::ReportNotify->AddUserID( report_id => $self->reportID, user_id => $userID );
}

# Delete a report
sub delete {
    my $self = shift;

    $self->_removeReport();

    $self->_setStatus(Report::DB::Item::Report::kStatusDeleted);

    return 1;
}

# Cancel a report
sub cancel {
    my $self = shift;

    my $job = Job::Job->CreateFromJobID( $self->{JobID} ) if ( $self->{JobID} );

    # delete the report if the job is already finished
    $job ? $job->kill() : $self->_removeReport();

    $self->_setStatus(Report::DB::Item::Report::kStatusCancelled);

    return 1;
}

# $reportObj->createReport();
#
# Create the report file.
# This is the interface that will be ultimately called by the Job and associated Process.
#
sub createReport {
    my ($self) = @_;

    # TODO: Check run state here.  Only run when in waiting state
    $self->_setStatus(Report::DB::Item::Report::kStatusRunning);

    $self->_run();

    $self->_setStatus(Report::DB::Item::Report::kStatusAvailable);
}

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

    Common::Log::Print("starting report generation");

    $self->_preCreateFiles();
    $self->_createCSVFile();
    $self->_createExcelFile();

}

# Is the report passed its expiration date.
# This is based on date_modified, do we want to do this of some other date?
sub _isExpired {
    my $self       = shift;
    my $expireDate = new Common::Date( $self->{DateModified}->access() );
    my $today      = Common::Date->now();

    Log->debug("  - expire check pre-math: today: $today  expire: $expireDate");
    $expireDate->addDays( $self->DaysToExpire() );
    Log->debug("  - expire check post-math: today: $today  expire: $expireDate");

    Log->debug("  - expire check: $expireDate < $today");

    return ( $expireDate < $today );
}

sub _removeReport {
    my $self = shift;
    my $path = $self->{FilePath};
    my $id   = $self->reportID;

    Log->info( "   - checking for files in: $path, id: " . $id );

    opendir( DIR, $path ) || die $!;

    while ( my $file = readdir(DIR) ) {
        if ( $file =~ /^$id\./ ) {
            Log->debug("   - unlink $path/$file");
            unlink("$path/$file") || die "unlink failed for '$path/$file': $!";
        }
    }

    close(DIR);
}

sub _getAvailableFileTypes {
    my $self = shift;
    my $path = $self->{FilePath};
    my $id   = $self->reportID;
    my @result;

    # Create the path if it doesn't exist already.
    #
    if ( !-d $path ) {
        File::Path::mkpath($path) or die "ERROR: Unable to create path $path: $!";
    }
    opendir( DIR, $path ) || die $!;

    while ( my $file = readdir(DIR) ) {
        if ( $file =~ /^$id\.(\w+)$/ ) {
            push @result, $1;
        }
    }

    close(DIR);
    return \@result;
}

#
#  ((((((((((  Private Interface
#

#
# MUST override section.
#

# TODO
sub _header {
    assert( 0, 'override this' );
}

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

    # !!! We're going to leave the structure of the logic here very vague in the base class.
    # !!! Most of our reports will inherit from Dynamic::SingleQuery, which _does_ do something here.
    #
    assert( 0, 'override' );
}

sub _JobSubclass {
    my ($class) = @_;

    # By default we'll construct the subclass from group and type.
    # Going to clean the resulting string, though, since I don't want to
    # risk using any ol' string as a Job subclass.
    #
    return Common::Util::clean( $class->Group() . '_' . $class->Type() );
}

# I expect virtually all Report classes to use the same Parameters class.
# But we'll have this extra layer of abstraction if necessary.
#
sub _ParamClass {
    assert( 0, 'override' );
}

#
# ((((((((   Stuff you can override
#

sub _preCreateFiles {

    # Implement this if you have to do work on the data before you output it
}

#
# ((((((((   Stuff you probably won't need to override.
#

sub _init {
    my ( $self, %args ) = @_;

    $self->SUPER::_init(%args);

    # Let's move away a little bit from my regular MO.
    # If we always assume we have a DB Item (even if it's blank unsaved one...)
    # that simplifies storage logic a lot.
    #
    my %properties;
    $self->{_dbItem} = $args{dbItem};
    if ( !$self->{_dbItem} ) {
        $self->{_dbItem} = $self->_newDBItem(%args);
    }

    $self->_initProperties();

    return $self;
}

sub _newDBItem {
    my ( $self, %args ) = @_;

    # Passing args along, but these are not intended to
    # auto-populate fields in the Report being created.
    # Rather, these args are to modify the behavior of _newDBItem()

    my $item = Report::DB::Item::Report->Create();
}

sub _initProperties {
    my ($self) = @_;
    assert( $self->{_dbItem} );

    # So, we'll want to de-reference the db item.  All reports will share
    # the main 'report' table.
    # If necessary, subclasses can also use extra tables, on the assumption that
    # these will be joined using report_id - So subclasses who need to add additional
    # state will override this method to read these additional columns.
    #

    $self->{ReportID} = $self->{_dbItem}->report_id;
    $self->{Group}    = $self->{_dbItem}->report_group;
    $self->{Type}     = $self->{_dbItem}->report_type;
    $self->{Status}   = $self->{_dbItem}->status();
    $self->{JobID}    = $self->{_dbItem}->job_id;
    $self->{FileName} = $self->{_dbItem}->file_name;
    $self->{FilePath} = $self->{_dbItem}->file_path;

    $self->{NotifyActiveUser} = $self->_notifySet();

    # Even though we're just an XMLObject, and not a FormObject, we can still utilize the
    # Scalar classes for formatting our output.
    #
    $self->{CreatedBy}    = Common::FormObject::Scalar::UserName->new( value => $self->{_dbItem}->created_by );
    $self->{DateCreated}  = Common::FormObject::Scalar::DateTime->new( value => $self->{_dbItem}->date_created );
    $self->{DateModified} = Common::FormObject::Scalar::DateTime->new( value => $self->{_dbItem}->date_modified );

    if ( $self->{ReportID} ) {
        $self->{Downloads}->{Type} = $self->_getAvailableFileTypes();
    }
}

sub _notifySet {
    my $self = shift;
    return unless ( $self->reportID && Common::RSApp->GetActiveUserID );
    return Report::DB::Item::ReportNotify->IsNotify( user_id => Common::RSApp->GetActiveUserID(), report_id => $self->reportID );
}

sub _setJobID {
    my ( $self, $jobID ) = @_;
    $self->{JobID} = $jobID;
    $self->{_dbItem}->job_id($jobID);
}

sub _setStatus {

    # !!! TODO
    # !!! Have this call save itself
    # !!! Notify upon certain state changes.

    my ( $self, $status ) = @_;

    # Only notify on a status change.
    $self->_notifyOnStatusUpdate($status)
      if ( !$self->{Status} || $self->{Status} ne $status );

    $self->{Status} = $status;
    $self->{_dbItem}->status($status);

    $self->_save();
}

sub _setFileName {
    my ( $self, $baseName ) = @_;
    $self->{FileName} = $baseName;
    $self->{_dbItem}->file_name($baseName);
}

sub _setFilePath {
    my ( $self, $path ) = @_;
    $self->{FilePath} = $path;
    $self->{_dbItem}->file_path($path);
}

sub _setGroup {
    my ( $self, $group ) = @_;
    $self->{Group} = $group;
    $self->{_dbItem}->report_group($group);
}

sub _setType {
    my ( $self, $type ) = @_;
    $self->{Type} = $type;
    $self->{_dbItem}->report_type($type);
}

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

    # If we always remember to update the _dbItem when state changes, this
    # becomes a trivial method.
    #
    $self->{_dbItem}->save();

    # Re-set our properties.
    #
    $self->_initProperties();
}

# Convenience function to convert code-friendly names to human-friendly names
# By default we don't convert anything.  Override as needed.
sub _readable {
    my $self = shift;
    my $s    = shift;
    return $s;
}

# Get optional data for use in constructing the report footer.
# Note: _getValue() may return a somewhat terse value.  If you need something
# more detailed, you may want to override this method and/or use _readable()
# to convert the code-friendly values to something easier to read.
sub _optionalFooterData {
    my ($self) = @_;
    my @footer;

    my $p = $self->parameters();

    return undef if( ref($p->{OptionalData}) eq 'HASH' );  # this report has no optional data

    my $data = $p->OptionalData();
    if( $data ) {
        foreach my $param ( keys %{ $data } ) {
            next if ( $param =~ /^_/ );
            my $field = $data->{$param};
            push @footer,  $self->_readable( $field->_getValue() );
        }
    }
    return \@footer;
}

# Default _footer; override if necessary
sub _footer {
    my ($self) = @_;

    my $dateType;
    my @footer;

    # Gather optional data
    #
    my $optionalData = $self->_optionalFooterData();
    push @footer, @{$optionalData} if($optionalData);

    # Gather date criteria
    #
    my $dateTypeParam = (exists $self->parameters()->{Criteria}->{DateType}) ?  $self->parameters()->{Criteria}->DateType() : undef;
    if ($dateTypeParam) {

        if ( $dateTypeParam =~ /None/i ) {
            push @footer, 'Date criteria: None';
        } else {
            my $startDate = $self->parameters()->{Criteria}->StartDate();
            my $endDate = $self->parameters()->{Criteria}->EndDate();

            my @filter;

            if( $startDate ) {
                push @filter,  "from $startDate";
            }
            if( $endDate ) {
                push @filter, "to $endDate"
            }

            # Only show date type if a date was actually entered
            push @footer, $dateTypeParam . ' ' . join(' ', @filter) if($startDate || $endDate);
        }
    }
    return \@footer;

}

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

    assert( $self->filePath() );
    assert( $self->reportID() );

    my $fileDirectory = $self->filePath();
    my $fileName      = $self->reportID() . '.csv';

    # Create the path if it doesn't exist already.
    #
    if ( !-d $fileDirectory ) {
        File::Path::mkpath($fileDirectory) or die "ERROR: Unable to create path $fileDirectory: $!";
    }

    Common::Log::Print(" creating csv file: $fileDirectory/$fileName");

    open( CSV_FILE, "> $fileDirectory/$fileName" ) or die "ERROR: $!";
    binmode( CSV_FILE, ":utf8" );

    $self->_outputHeaderCSV(*CSV_FILE);

    $self->_outputDataCSV(*CSV_FILE);

    $self->_outputFooterCSV(*CSV_FILE);
    close(CSV_FILE);
}

sub _outputDataCSV {
    my ( $self, $outFH ) = @_;

    while ( my $outputLineData = $self->_nextReportLineData() ) {
        $self->_outputDataLineCSV( *CSV_FILE, $outputLineData );
    }
}

# Should be able to make this generic, assuming _header() is implemented correctly.
#
sub _outputHeaderCSV {
    my ( $self, $outFH ) = @_;

    $self->_outputLineCSV( $outFH, $self->_header() );
}

sub _outputFooterCSV {
    my ( $self, $outFH ) = @_;
    $self->_outputLineCSV( $outFH, [''] ); # blank line

    my $params   = $self->parameters();
    my $baseName = $self->filename();

    # Output common footer elements
    $self->_outputLineCSV( $outFH, [ $baseName ] );
    $self->_outputLineCSV( $outFH, [ $self->Group() ] );
    $self->_outputLineCSV( $outFH, [ $self->Type() ] );

    # Output report-specific footer elements
    foreach my $e ( @{$self->_footer()} ) {
        $self->_outputLineCSV( $outFH, [ $e ] );
    }
}

sub _outputDataLineCSV {
    my ( $self, $outFH, $outDataArrayRef ) = @_;

    # This just calls straight through to _outputLiveCSV.
    # But subclasses may want to override this method to monkey around
    # with outgoing lines of report data.
    #
    $self->_outputLineCSV( $outFH, $outDataArrayRef );
}

sub _outputLineCSV {
    my ( $self, $outFH, $outDataArrayRef ) = @_;

    # Remember that we want to properly escape the csv data, and support utf8.
    # Probably can lean on a module like Text::CSV::Encoded or some such.

    if ( !$self->{_csv} ) {
        $self->{_csv} = Text::CSV->new({ binary => 1, auto_diag => 1 });
    }

    # JPK - We have an issue with Text::CSV having problems with embedded carriage returns.
    # Doesn't happen on all dev boxes, but it does seem to happen on staging64.
    # Basically, if the last line contains a carriage return, it will drop that last line.
    # So as a work-around we'll strip out CR or NL from each line.
    #
    my @scrubbedData;
    foreach my $column (@$outDataArrayRef) {
        $column =~ s/[\n\r]//g;
        push @scrubbedData, $column;
    }

    if ( !$self->{_csv}->print( $outFH, \@scrubbedData ) ) {
        # auto_diag will automatically print diag info; next line shows what failed to print
        die "ERROR: \$csv->print failed: ". join("\t", @scrubbedData);
    }
    print $outFH "\n";
}

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

    my $fileDirectory = $self->filePath();
    my $excelFileName = $self->reportID() . '.xls';
    my $csvFileName   = $self->reportID() . '.csv';

    # The csv file must already be there...
    #
    my $csvFullPath = $fileDirectory . '/' . $csvFileName;
    if ( !-f $csvFullPath ) {
        Log->warn("file $csvFullPath does not exist!");
        return;
    }

    # Before we proceed, let's see just how humonguous the current report file is.
    # If it's larger than 65535 lines, we won't create an excel file.
    # The most fool-proof way to do this is to shell out and use 'wc'.
    #
    my $wcOut     = `wc -l $csvFullPath`;
    my @bits      = split( / /, $wcOut );
    my $lineCount = $bits[0];
    if ( $lineCount > kMaxLines ) {
        Common::Log::Print(" Line count = $lineCount : Not creating Excel file");

        # Instead, let's make a compressed version of the csv file
        my $zipFullPath = $fileDirectory . '/' . $self->reportID();

        # Need to rename the csv file before adding it to the zip.
        my $prettyFileName = $self->filename() . '.csv';
        my $prettyFullPath = $fileDirectory . '/' . $prettyFileName;
        system("ln -s $csvFullPath $prettyFullPath");
        system("zip -j $zipFullPath $prettyFullPath");
        system("rm $prettyFullPath");

        return;
    }

    # Get the excel column template, if any.
    #
    my $excelTemplate = $self->_getExcelTemplate();

    # !!! Hard-coding this for now.
    my $converterScriptPath = '/app/tools/report/bin/csv2excel.pl';

    # Invoke the conversion script.
    #
    my $excelFullPath = $fileDirectory . '/' . $excelFileName;
    my $commandLine   = $converterScriptPath . " -i $csvFullPath -o $excelFullPath";
    if ($excelTemplate) {
        $commandLine .= ' -t "' . $excelTemplate . '"';
    }

    #    Log->info(" creating excel file with command: $commandLine");
    Common::Log::Print(" creating excel file with command: $commandLine");
    system($commandLine);

}

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

    # By default we don't provide a column template.
    #
    return undef;
}

sub _notifyOnStatusUpdate {
    my $self   = shift;
    my $status = shift;

    assert($status);

    if ( $status eq Report::DB::Item::Report::kStatusAvailable ) {
        return $self->_notifyOnAvailable();
    }

    elsif ( $status eq Report::DB::Item::Report::kStatusError ) {
        return $self->_notifyOnError();
    }

    elsif ( $status eq Report::DB::Item::Report::kStatusCancelled ) {
        return $self->_notifyOnCancel();
    }

}

sub _notifySender { 'donotreply@royaltyshare.com' }

sub _notifyOnAvailable {
    my $self    = shift;
    my $sender  = $self->_notifySender;
    my $subject = $self->_notifyOnAvailableSubject();
    my $body    = $self->_notifyOnAvailableBody();

    $self->_notifyUser( sender => $sender, subject => $subject, body => $body );
}

sub _notifyOnAvailableSubject { 'Report Generation Complete' }

sub _notifyOnAvailableBody {
    my $self = shift;

    # !!! 'Show' is not really the right place to go.  Doesn't even work right.
    # !!! I still want to fix it, though.
    # !!! Also, why would we want to have rps-specific URLS here in what should be an abstract base class?
    # !!! At the very least these ought to be in the config file.  But I'm thinking that these various notify methods
    # !!! should be in the Factory class, which anchors the Report mechanism in each Application.
    #
    #    my $url  = "https://" . Common::RSApp::GetClientVHost() . ".royaltyshare.com/rps/report/show?ReportID=" . $self->reportID;
    my $url  = "https://" . Common::RSApp::GetClientVHost() . ".royaltyshare.com/rps/report/list?Highlight=" . $self->reportID;
    my $body = "A report you requested has completed and can now be downloaded from your website.

To download the report visit $url\n\n";

    return $body;
}

sub _notifyOnError {
    my $self    = shift;
    my $sender  = $self->_notifySender;
    my $subject = $self->_notifyOnErrorSubject();
    my $body    = $self->_notifyOnErrorBody();

    $self->_notifyUser( sender => $sender, subject => $subject, body => $body );
}

sub _notifyOnErrorSubject { 'Report Generation Error' }

sub _notifyOnErrorBody {
    'A report you requested failed to complete due to an error.';
}

# We should notify on cancel because the notification list may contain more
# then just the person that canceled the report.
sub _notifyOnCancel {
    my $self    = shift;
    my $sender  = $self->_notifySender;
    my $subject = $self->_notifyOnCancelSubject();
    my $body    = $self->_notifyOnCancelBody();

    $self->_notifyUser( sender => $sender, subject => $subject, body => $body );
}

sub _notifyOnCancelSubject { 'Report Generation Canceled' }

sub _notifyOnCancelBody {
    my $self = shift;
    my $url  = "https://" . Common::RSApp::GetClientVHost . ".royaltyshare.com/rps/report/list?Highlight=" . $self->reportID;

    my $currentUser = AppUser::User::User->new( userID => Common::RSApp->GetActiveUserID() );
    my $body =
        'A report you requested was canceled by '
      . $currentUser->FirstName . " "
      . $currentUser->LastName . ".\n\n"
      . "If you would like to requeue the report go to $url\n\n";

    return $body;
}

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

    assert( $args{body} );
    assert( $args{sender} );
    assert( $args{subject} );

    my $collection = Report::DB::Item::ReportNotify->GetAllByReportID( $self->reportID );

    while ( my $dbitem = $collection->next() ) {
        my $user = AppUser::User::User->new( userID => $dbitem->active_user_id );
        assert($user);

        next unless ( $user->Email );

        Common::Email->SendAWS(
            to      => $user->Email,
            from    => 'do-not-reply@royaltyshare.com',
            subject => $args{subject},
            body    => $args{body}
        );
    }
}

1;
