###################################################################
# Copyright (C) 2006 RoyaltyShare, Inc.  All Rights Reserved
###################################################################
package Common::CronScript;

use strict;

use lib '/app/tools/common/lib';
use base 'Common::Script';

use Common::Assert;
use Common::Email;
use Common::Client;
use Common::RSApp;

use Date::Format;
use File::Basename;
use Sys::Hostname;
use Carp;
use IO::Scalar;
use Date::Format;
use Proc::ProcessTable;
use Getopt::Long;
use Data::Dumper;

use File::Pid;

use constant kPidDir => '/var/run/royaltyshare';

# How long can a process live before an error is reported;
use constant kDefaultNotificationTimeout => 3600;

sub _pidFileDir     { '/var/run/royaltyshare' }
sub _pidFile        { basename($0) . '.pid' }
sub _pidFilePath    { my $self = shift; return $self->_pidFileDir . "/" . $self->_pidFile; }
sub _notifyFile     { basename($0) . "-notified" }
sub _notifyFilePath { my $self = shift; return $self->_pidFileDir . "/" . $self->_notifyFile; }

sub _maxAge { my $self = shift; return $self->{_max_age} ? $self->{_max_age} : kDefaultNotificationTimeout }

sub _errorReportSubject {
    my $host = Common::RSApp::GetRealHostname();
    return "($host) Error Report $0";
}

sub _reportSubject {
    my $host = Common::RSApp::GetRealHostname();
    return "($host) Summary Report $0";
}

sub _senderEmail { '"CronScript" <do-not-reply@royaltyshare.com>' }

sub _runInfo {
    my $host = Common::RSApp::GetRealHostname();
    return "Host: $host";
}

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

    $self->{_report_email} = $args{report_email};
    $self->{_error_email}  = $args{error_email};

    $self->{_max_age} = $args{max_age};

    $self->_initLogs();

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

sub DESTROY {
    my $self = shift;
    $self->{_pid}->remove() if ( $self->{_pid} );
    $self->{_pid} = undef;
}

sub _initLogs {
    my $self = shift;

    my $stdout;
    my $stderr;
    my $io;

    if ( $self->{_report_email} ) {
        $self->{_stdout} = \$stdout;
        $io = tie *STDOUT, 'IO::Scalar', $self->{_stdout};
    }

    if ( $self->{_error_email} ) {
        $self->{_stderr} = \$stderr;
        $io = tie *STDERR, 'IO::Scalar', $self->{_stderr};
    }
}

sub start {
    my $class = shift;
    my $self = ref($class) ? $class : $class->new(@_);

    $self->_parse_options();

    $self->_initLogs();

    if ( defined( $self->{_list} ) ) {
        $self->list_clients( $self->{_list} );
    } elsif ( $self->{_help} ) {
        $self->usage();
    } else {
        $self->run();
    }
}

sub run {
    my $self = shift;

    eval {
        if ( $self->is_already_running() ) {
            Common::Log::Debug("Another process is already running, bailing");
        } else {
            $self->run_process();
        }
    };

    if ($@) {
        print STDERR "$@\n";
        if ( $self->{_error_subject} ) {
            $self->{_error_subject} = "PROCESS FAILED: $self->{_error_subject}";
        } else {
            $self->{_error_subject} = "PROCESS FAILED!";
        }
        $self->_send_errors();
    }

    else {
        $self->_send_report();
        $self->_send_errors();
    }
}

sub is_already_running {
    my $self = shift;
    my $pidfile;

    # Don't do any testing if we are ignoring run controls
    return if ( $self->param('ignore') );

    my $pidFile    = $self->_pidFilePath;
    my $notifyFile = $self->_notifyFilePath;

    assert($pidFile);
    assert($notifyFile);

    $self->debug("Checking to see if another process is already running.");

    # Save this in the object so when it goes out of scope the PID file is removed
    # automagically
    $self->{_pid} = new File::Pid( { file => $pidFile } ) || die;

    # Do we already have a PID file?
    # since File::Pid is too stupid to check for the existence of the pidfile
    # before it tries to check whether the process for the PID contained there
    # within is actually running, we need to help it out. if we really wanted to
    # be thorough, we'd check for a well formed PID, but we're still making a
    # better effort than the module that's supposedly built to handle all of this.
    if ( -e $pidFile && $self->{_pid}->running() ) {
        $self->{_pid} = undef;
        $self->debug(" - found another process running ($pidFile)");
        $self->_stale_process_notification();
        return 1;
    } else {

        # If we aren't running then make sure there aren't any stray notification
        # files laying around.
        unlink $notifyFile;
    }

    $self->{_pid}->write() || die "Could not write PID file ($pidFile)";
    return undef;
}

sub print {
    my $self = shift;

    return unless (@_);

    # Also send it to stderr if we are sending the error report in email
    $self->debug(@_) if ( $self->{_error_email} );

    $self->SUPER::print(@_);
}

sub log {
    my $self = shift;

    return unless (@_);

    # Also send it to stderr if we are sending the error report in email
    $self->debug(@_) if ( $self->{_error_email} );

    $self->SUPER::log(@_);
}

sub _send_report {
    my $self    = shift;
    my $body    = ${ $self->{_stdout} } if ( $self->{_stdout} );
    my $subject = $self->_reportSubject;
    untie *STDOUT;

    if ( $self->{_report_email} && $body ) {
        $body = ${ $self->{_stdout} };
        Common::Email->SendAWS(
            to      => $self->{_report_email},
            from    => $self->_senderEmail,
            subject => "$subject - " . time2str( "%Y-%m-%d", time ),
            body    => $body
        );
    }
}

sub _send_errors {
    my $self    = shift;
    my $body    = ${ $self->{_stderr} } if ( $self->{_stderr} );
    my $subject = $self->_errorReportSubject;
    untie *STDERR;

    if ( $self->{_error_email} && $body ) {
        if ( $self->_runInfo() ) {
            $body = $self->_runInfo . "\n\n$body";
        }

        Common::Email->SendAWS(
            to      => $self->{_error_email},
            from    => $self->_senderEmail,
            subject => "$subject - " . time2str( "%Y-%m-%d", time ),
            body    => $body
        );
    }
}

sub _stale_process_notification {
    my $self = shift;

    my $pidFile    = $self->_pidFilePath;
    my $notifyFile = $self->_notifyFilePath;

    assert($pidFile);
    assert($notifyFile);

    # Don't do anything if we've already notified someone.
    if ( -e $notifyFile ) {
        Common::Log::Debug(" + Stale process found, but notification already sent");
        return;
    }

    # The "other" process could have finished by now, ensure the PID file still
    # exists
    return unless ( -r $pidFile );

    open( INFILE, $pidFile ) || die "cant open $self->{_pid_file} ";
    my $pid = <INFILE> || die "No process ID found";
    close(INFILE);

    my $age = $self->_get_process_age();

    # Is the process too old?
    my $maxAge = $self->_maxAge();
    if ( $age && $age > $maxAge ) {
        print STDERR "Possible stale process detected.\n"
          . " - Program: "
          . basename($0) . "\n"
          . " - Pid: $pid"
          . " - Age: $age seconds,  max allowed: $maxAge seconds\n";

        open( OUT, ">$notifyFile" )
          || die "Can't create notification file: $notifyFile";
        print OUT "$$";
        close(OUT);
    }
}

sub _get_process_age {
    my $self = shift;
    my $process;

    my $pidFile = $self->_pidFilePath;

    assert($pidFile);

    $self->debug(" - checking running process age");

    return undef unless ( -e $pidFile );
    open( INFILE, $pidFile ) || die "cant open $pidFile ";
    my $pid = <INFILE> || die "No process ID found";
    close(INFILE);

    $self->debug(" - process pid: $pid");

    my $table = new Proc::ProcessTable();
    foreach ( @{ $table->table } ) {
        $process = $_ if ( $_->pid == $pid );
    }

    my $process_age = time - $process->start if ($process);

    $self->debug(" - process age: $process_age") if ($process_age);
    return $process_age;
}

sub _common_options {
    my $self = shift;

    my $options = $self->SUPER::_common_options;

    $options->{report_email} = {
        short       => 'r',
        description => 'Send summary report to this email instead of STDOUT',
        parameter   => 's',
    };

    $options->{error_email} = {
        short       => 'x',
        description => 'Send error report to this email instead of STDERR',
        parameter   => 's',
    };

    $options->{ignore} = {
        short       => 'i',
        description => 'Ignore run controls (allow multiple instances to run)'
    };

    return $options;
}

sub _getProductionClientIDs {
    return Common::RSDB::GetAllBookPubClientIDs();
}

sub _getWebProductionClientIDs {
    return Common::RSDB::GetBookPubClientIDsByHost( Common::RSApp::GetRealHostname() );
}

sub _getLocalClientIDs {
    return Common::RSDB::GetLocalBookPubClientIDs();
}

sub _isValidClientType {
    my $self = shift;
    Common::RSDB::_isBookPubClient(shift);
}

1;
