#!/usr/bin/perl

use strict;
use FileHandle;
use Getopt::Long;
use GDBM_File;
use Net::SMTP;
use Date::Format;
use NOCpulse::Config;
use NOCpulse::Debug;
use NOCpulse::Log::Logger;
use NOCpulse::Notif::Acknowledgement;
use NOCpulse::Notif::AlertDB;
use NOCpulse::Notif::EscalatorInterface;
use NOCpulse::Notif::FileQueue;
use NOCpulse::Notif::SendInfo;

use Data::Dumper;

umask(022);

use constant DEF_UNIX_DATE_FMT => '%m-%d-%Y %H:%M:%S';

my $CONFIG    = new NOCpulse::Config;    # Config object
my $QUEUE_DIR =
  $CONFIG->get('notification', 'ack_queue_dir');    # Dir for queuing alerts
my $tmp_dir         = $CONFIG->get('notification', 'tmp_dir');
my $SEND_GDBM       = "$tmp_dir/sendhistory.gdbm";
my $verboselog      = $CONFIG->get('notification', 'ack_handler_log');
my $SOCKET_FILENAME =
  "$tmp_dir/escsock";    # TBD -- replace with config file entry

# Get command line options
my $logopt = {};
GetOptions('log=s%' => $logopt);

#Set up for graceful exit
my $bailout = 0;
$SIG{'INT'} = $SIG{'TERM'} = sub { $bailout = 1; };

# Set up logging
my $last_time = time();
my $current_time;
my $program    = 'ack-processor';
my $LOG_CONFIG = "( '$program' => 3)";
my ($Log, $acklog) = &setup_logging();
$Log->log(1, "Starting up $0\n");

# Collection of backlog information for the escalator
my @backlog;

# Connect to the smtp server
my $MX = $CONFIG->get('mail', 'mx');
my $TIMEOUT = 30;
my $smtp;

# Send history
my %send_history;

# CFDB connection
my $db = NOCpulse::Notif::AlertDB->new;

my $AckQ =
  NOCpulse::Notif::FileQueue->new(
                              'directory'  => $QUEUE_DIR,
                              'item_class' => 'NOCpulse::Notif::Acknowledgement'
  );

while (1) {

  # If a shutdown was requested, exit gracefully
  if ($bailout) {
    $Log->log(1, "System shutdown requested\n");
    $smtp->quit() if $smtp;
    exit(0);
  }

  # Check the status of the smtp server
  $smtp = &check_smtp_connection($smtp);

  my $ack = $AckQ->peek();

  if ($ack) {

    # Send id database
    my $result = tie(%send_history, 'GDBM_File', $SEND_GDBM, O_RDONLY, 0644);
    unless ($result) {
      $Log->log(1, "FATAL ERROR: Unable to find send history $!\n");
      die "Unable to find send history";
    }

    # Process this acknowledgement
    $Log->log(1, $ack->as_string);
    $Log->log(1, "\n[end of message]\n");

    my $send_id = $ack->send_id;

    my $send_info;
    my $string = $send_history{$send_id};
    if ($string) {
      $send_info = NOCpulse::Notif::SendInfo->from_string($string) if $string;
      $Log->log(
        1,
"Unable to parse send history via NOCpulse::Notif::SendInfo for $send_id from '$string'\n"
        )
        unless $send_info;
    } else {
      $Log->log(1, "Send history not in dbm for $send_id\n");
    }

    if ($send_info) {

      # We have valid send history, process the acknowledgement
      &process_ack($ack);

      if ($ack->is_bounce) {

        # We have a redirect
        $Log->log(1, "This is a bounce\n");

      } elsif ($ack->is_redirect) {

        # We have a redirect
        $Log->log(1, "Acknowledgement contains a redirect\n");
        my $result = &process_redirect($ack, $send_info) if $ack->is_redirect;
        $ack->redirect_result($result);

      } else {

        # Just an acknowledgement, nothing fancy
        $Log->log(1, "Acknowledgement does not contain a redirect\n");
      }

    } else {

      # Send info not found for this send id
      $ack->ack_result("Send id not found\n");
      $Log->log(1, "Send history not found for this message\n");
    }

    eval { $ack->reply($smtp); };
    if ($@) {
      $Log->log(1, "ERROR: ack->reply. $@\n");
    }

    $AckQ->dequeue();

    $Log->log(1, "---\n");    # Show the end of processing this ack

  } else {

    # There is no current acknowledgement to process, wait ...
    sleep(1);
    $current_time = time();
    if ($current_time - $last_time > 60) {
      $Log->log(1, "waiting for data....\n");
      $last_time = $current_time;

      #process the backlog
      if (@backlog) {
        $Log->log(1, "processing escalator backlog\n");
        @backlog = &process_backlog(@backlog);
      }

    } ## end if ($current_time - $last_time...

    untie(%send_history);
  }    # end if ($ack)
}    # end while

### SUBROUTINES ###

#################
sub process_ack {
#################
  my ($ack) = @_;

  if ($ack->operation) {

    my $err;
    eval {
      my $escalator_if =
        NOCpulse::Notif::EscalatorInterface->new(
                                           socket_filename => $SOCKET_FILENAME);
      $err = $escalator_if->ack($ack->operation, $ack->send_id);
    };
    if ($@) {
      my @array = (1, 'ack', $ack->operation, $ack->send_id);
      push(@backlog, \@array);
      $err = "escalator ack error ($@)";
    }

    $Log->log(1, $ack->operation, " ", $ack->send_id, ": $err\n");
    unless ($err) {
      $ack->ack_result('Success!');
    }
  } ## end if ($ack->operation)
} ## end sub process_ack

######################
sub process_redirect {
######################
  my ($ack, $send_info) = @_;

  $Log->log(1, &Dumper($send_info), "\n");

  my $error_msg = $ack->not_valid_redirect;
  if ($error_msg) {
    $Log->log(1, 'RedirectViaAck Error Send [',
              $ack->send_id, ']: ', $error_msg, "\n");
    return $error_msg;
  }

  # Determine whether to block all probes for the host or just a single check
  my @match_values;
  if ($ack->redirect_scope eq 'host') {

    # Check the probe, if it's a satellite health check, it cannot be
    # redirected as a host probe
    my ($command_group, $msg);
    my ($errno, $ref) =
      $db->dbexecute('select_command_by_probe_id', $send_info->probeId);
    unless ($errno) {
      my $row = shift(@$ref);
      (undef, undef, undef, $command_group) = @$row if $row;
    }
    unless ($command_group) {
      my $msg =
        sprintf("Unable to find command for probeId for %s",
                $send_info->probeId, $ack->redirect_type);
      $Log->log(1, 'RedirectViaAck Error Send [',
                $ack->send_id, ']: ', $msg, "\n");
      return
'Unable to find service check(s).  Please try again later or use the web interface for more options.';
    }
    if ($command_group eq 'satellite') {
      my $msg = "Cannot host redirect a satellite check " . $ack->redirect_type;
      $Log->log(1, 'RedirectViaAck Error Send [',
                $ack->send_id, ']: ', $msg, "\n");
      return 'Error unable to '
        . $ack->redirect_type
        . ' all host notifications via a satellite health check. Try a "check" level suspend or use the web interface for more options.';
    }

    # Find the service probes associated with the host for which
    # this notification was issued
    ($errno, $ref) =
      $db->dbexecute('same_host_probes_from_probeid', $send_info->probeId);
    unless ($errno) {
      foreach my $row (@$ref) {
        push(@match_values, shift(@$row));
      }
    }
  } else {
    push(@match_values, $send_info->probeId);
  }
  unless (@match_values) {
    my $msg = "Unable to find probeId(s) for " . $ack->redirect_type;
    $Log->log(1, 'RedirectViaAck Error Send [',
              $ack->send_id, ']: ', $msg, "\n");
    return
'Unable to find service check(s).  Please try again later or use the web interface for more options.';
  }

  # Determine the expiration date
  unless ($ack->duration_in_seconds) {
    my $msg = "Zero duration" unless $ack->duration_in_seconds;
    $Log->log(1, 'RedirectViaAck Error Send [',
              $ack->send_id, ']: ', $msg, "\n");
    return $msg;
  }
  my $time       = time();
  my $expiration = $time + $ack->duration_in_seconds;

  # Get a redirect id
  my ($errno, $ref) = $db->dbexecute('select_next_redirect_recid');
  if ($errno) {
    my $msg = "Unable to get redirect recid: $ref";
    $Log->log(1, 'RedirectViaAck Error Send [',
              $ack->send_id, ']: ', $msg, "\n");
    return
'Internal database error.  Please try again later or use the web interface for more options.';
  }
  my $value       = shift(@$ref);
  my $redirect_id = shift(@$value);

  # Map the redirect type to the format the db expects
  my $redir_type;
  if ($ack->redirect_type eq 'suspend') {
    $redir_type = 'BLACKHOLE';
  } elsif ($ack->redirect_type eq 'autoack') {
    $redir_type = 'ACK';
  } else {
    $redir_type = uc($ack->redirect_type);
  }

  # Create the description and reason for the redirect
  my $desc = sprintf(
                     "%s %s (%s) %s %s",
                     $ack->redirect_type, $ack->redirect_scope,
                     join(',', @match_values), $ack->duration,
                     $ack->destination
                    );
  $desc = substr($desc, 0, 25);
  my $reason =
    sprintf("Email ack/notif filter by %s, date: %s, notification id: %02d%s",
            $ack->from, $ack->date, $ack->server_id, $ack->send_id);
  $reason =~ s/\n//g;

  # Create the basic redirect
  my ($err, $errstring) =
    $db->dbexecute(
                   'create_redirect',
                   $redirect_id,
                   $send_info->customerId,
                   $send_info->contactId,
                   $redir_type,
                   $desc,
                   $reason,
                   time2str(DEF_UNIX_DATE_FMT, $time),
                   time2str(DEF_UNIX_DATE_FMT, $expiration),
                   'system'
                  );
  if ($err) {
    $Log->log(1, 'RedirectViaAck Error Send [',
              $ack->send_id, ']: ', $errstring, "\n");
    return
'Internal database error.  Please try again later or use the web interface for more options.';
  }

  # Create the redirect criteria
  foreach my $match_value (@match_values) {
    ($err, $errstring) =
      $db->dbexecute('create_redirect_criterion', $redirect_id, 'PROBE_ID',
                     $match_value);
    if ($err) {
      $Log->log(1, 'RedirectViaAck Error Send [',
                $ack->send_id, ']: ', $errstring, "\n");

      ##$db->dbexecute('create_archive_master',1,'DEL','REDIRECTS','RECID');
      $db->dbexecute('delete_redirect', $redirect_id);

      ##$db->dbexecute('delete_archive_master',1,'DEL','REDIRECTS','RECID');
      return
'Internal database error.  Please try again later or use the web interface for more options.';
    } ## end if ($err)
  } ## end foreach my $match_value (@match_values)

  # Create the email destination, if applicable
  if ($ack->destination) {
    ($err, $errstring) =
      $db->dbexecute('create_redirect_email_target',
                     $redirect_id, $ack->destination);
    if ($err) {
      $Log->log(1, 'RedirectViaAck Error Send [',
                $ack->send_id, ']: ', $errstring, "\n");

      ##$db->dbexecute('create_archive_master',1,'DEL','REDIRECTS','RECID');
      $db->dbexecute('delete_redirect', $redirect_id);

      ##$db->dbexecute('delete_archive_master',1,'DEL','REDIRECTS','RECID');
      ##$db->dbexecute('create_archive_master',1,'DEL','REDIRECTS','RECID');
      $db->dbexecute('delete_redirect_criteria', $redirect_id);

#$    $db->dbexecute('delete_archive_master',1,'DEL','REDIRECT_CRITERIA','REDIRECT_ID');
      return
'Internal database error.  Please try again later or use the web interface for more options.';
    } ## end if ($err)
  } ## end if ($ack->destination)

  if ($db->commit) {
    return
'Internal database error.  Please try again later or use the web interface for more options.';
  }

  $Log->log(1, 'RedirectViaAck Created Send [',
            $ack->send_id, "] created redirect $redirect_id: $desc, $reason\n");

  return 'Success!';
} ## end sub process_redirect

###################
sub setup_logging {
###################
  # Set up logging. The log_config property should be a string
  # representing a Perl hash, e.g.
  # 'notifserver' => 2, 'NOCpulse::Notif::Escalator' => 4

  my $Log = NOCpulse::Log::Logger->new($program);

  # Don't prefix messages with the caller because this is the top call level.
  $Log->show_method(0);

  my $stream =
    NOCpulse::Log::LogManager->instance->stream(
                                                FILE       => $verboselog,
                                                APPEND     => 1,
                                                TIMESTAMPS => 1,
                                                LEVEL      => 3
                                               );
  $stream->autoflush(1);

  NOCpulse::Log::LogManager->instance->configure(eval($LOG_CONFIG))
    if $LOG_CONFIG;

  # The --log argument from the command line overrides the NOCpulse.ini
  # setting.
  NOCpulse::Log::LogManager->instance->configure(%{$logopt})
    if scalar(keys(%{$logopt}));

  return ($Log, $stream);
} ## end sub setup_logging

#####################
sub process_backlog {
#####################
  my @list = shift;
  my @new_list;

  my $ref;
  while ($ref = shift(@list)) {
    my ($count, $method, @args) = @$ref;
    $Log->log(1, "processing backlog: ($count) $method(@args): ");
    eval {
      my $escalator_if =
        NOCpulse::Notif::EscalatorInterface->new(
                                           socket_filename => $SOCKET_FILENAME);
      $escalator_if->$method(@args);
    };
    if ($@ && $count < 5) {
      $Log->log(1, "failed\n");
      $count++;
      push(@new_list, [ $count, $method, @args ]);
    } else {
      $Log->log(1, "success\n");
    }
  } ## end while ($ref = shift(@list...
  return @new_list;
} ## end sub process_backlog

###########################
sub check_smtp_connection {
###########################
  my $smtp = shift;

  while (1) {
    unless ($smtp) {
      $Log->log(1, "Connecting to $MX\n");
      $smtp =
        Net::SMTP->new(
                       $MX,
                       Timeout => $TIMEOUT,
                       Debug   => 1
                      );
      unless ($smtp) {
        $Log->log(1, "ERROR: Cannot connect to $MX: $@... waiting\n");
        sleep(60);
        next;
      }
    } ## end unless ($smtp)
    if ($smtp->ok()) {
      last;
    } else {

      # Something is wrong, reconnect
      if ($smtp->hello($MX)) {

        # Got a good connection
        $Log->log(1, "WARNING: Reconnected to $MX\n");
        last;
      } else {
        $Log->log(1, "ERROR: Unable to reconnect to $MX, ",
                  $smtp->code, ": ", $smtp->message, "\n");
        $smtp = undef;
        sleep(60);
      }
    } ## end else [ if ($smtp->ok())
  } ## end while (1)
  return $smtp;
} ## end sub check_smtp_connection

__END__

=head1 NAME

ack_proecessor.pl - script for preprocessing enqueued email acknowledgements to files.

=head1 SYNOPSIS

ack_processor.pl filters out bounces and other emails that are not processable as acknowledgements or notification filters..


=head1 DESCRIPTION

This script reads the inbound mail queue enqueued by ack_enqueuer.pl and queues up acknowledgements that can be processed by the notification system.  It handles any message that the notification system cannot process and will reject.

=head1 BUGS

No known bugs.

=head1 AUTHOR

Karen Jacqmin-Adams <kja@redhat.com>

Last update: $Date: 2005-05-27 20:18:01 $

=head1 SEE ALSO

B<ack_enqueuer.pl>
B<notifserver.pl>
B<NOCpulse::Notif::FileQueue>

=cut
