#!/usr/bin/perl
# notif-escalator - notification escalation server

use strict;
use Fcntl;
use Getopt::Long;
use IO::Socket::UNIX;
use IO::Select;
use POSIX;
use Socket;
use Storable qw(freeze);
use Tie::RefHash;
use NOCpulse::Debug;
use NOCpulse::Log::LogManager;
use NOCpulse::Notif::AlertFile;
use NOCpulse::Notif::Escalator;
use NOCpulse::Notif::EscalatorOperation;
use NOCpulse::Notif::Strategy;
use NOCpulse::Notif::BroadcastStrategy;
use NOCpulse::Notif::EscalateStrategy;
use NOCpulse::Notif::RotateFirstEscalateStrategy;

use Data::Dumper;

umask(002);
my $program='notif-escalator';

my $np_cfg     = new NOCpulse::Config;
unless ($np_cfg) {
  print STDERR "FATAL ERROR: Unable to read config NOCpulse.ini\n";
  die ("Unable to read config NOCpulse.ini");
}

my $tmp_dir      = $np_cfg->get('notification','tmp_dir');
my $filename="$tmp_dir/escsock";   #STUB --- Flesh out from new NOCpulse.ini entry
my $HEADER_BYTES=8;

# Escalation interval
use constant ESCALATION_INTERVAL => 5;  #Do escalation no more than every 5 seconds
my $last_escalate=0;

# Logs
use constant LOG_INTERVAL => 60;  #For keepalive logging
my $last_log;                     #Last time keepalive was logged 
my $log_dir    = $np_cfg->get('notification','log_dir');
my $verboselog = "$log_dir/$program.log";
my $LOG_CONFIG = "( 
    '$program' => 3, 
    'NOCpulse::Notif::Escalator'       => 3 )";

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

# Setup logging
my ($Log,$stream) = &setup_logging();


# Listen to socket.
unlink $filename;
my $server = IO::Socket::UNIX->new( Local    => $filename,
                                    Type     => SOCK_STREAM,
                                    Listen   => 10 );
unless ($server) {
  $Log->log(1,"FATAL ERROR: Can't make server socket: $@\n");
  die "Can't make server socket: $@\n";
}

# begin with empty buffers
my %inbuffer =  ();
my %inbytes =   ();  # How many bytes are expected
my %outbuffer = ();
my %ready     = ();

tie %ready, 'Tie::RefHash';

nonblock ($server);
my $select = IO::Select->new($server);

#Set up for graceful exit
my $bailout = 0;
$SIG{'INT'} = $SIG{'TERM'} = sub { $bailout = 1; };
$SIG{'PIPE'}  = sub { $Log->log(1,"SIGPIPE caught\n") };

# If the escalator left a state preservation file use that, otherwise create a new escalator
my $STATE_FILE   = "$tmp_dir/escalator.state";

my $Escalator;
     $Log->log(1,"System startup requested pid $$\n");
if (-r $STATE_FILE) {
  $Log->log(3,"Opening escalator state file $STATE_FILE for system initialization...\n");
  $Escalator = NOCpulse::Notif::Escalator->from_file($STATE_FILE);
} else {
  $Log->log(3,"No escalator state file found, starting new system initialization...\n");
  $Escalator = NOCpulse::Notif::Escalator->new();
}
$Log->log(3,"Escalator created and initialized\n");
$Log->log(3,"Starting daemon loop\n");

my @files=NOCpulse::Notif::AlertFile->remove_locks($tmp_dir,$program);
if (scalar(@files)) { 
  $Log->log(3,"Removed stale locks\n");
  $Log->log(9,"@files\n");
}


# Main loop: check reads/accepts, check writes, check ready to process
while (1) {

    # check for system shutdown
    if ($bailout) {
        @files=NOCpulse::Notif::AlertFile->remove_locks($tmp_dir,$program,$$);
        if (scalar(@files)) { 
          $Log->log(3,"Removed stale locks\n");
          $Log->log(9,"@files\n");
        }
        $Log->log(1,"System shutdown requested\n");
        $Escalator->shut_down();
        $Log->log(1,"System shutdown completed\n");
        exit(0);
    }

    my $client;
    my $rv;
    my $data;

    # check for new information on the connections we have

    # anything to read or accept?
    $Log->log(9,"anything to read or accept?\n");
    foreach $client  ($select->can_read(1)) {

        if ($client == $server) {
            # accept a new connection
            $Log->log(9, "accept a new connection\n");

            $client = $server->accept();
            $select->add($client);
             nonblock($client);
        } else {
            # read data
            $Log->log(9, "read data\n");
            $data = '';
            $rv = $client->recv($data, POSIX::BUFSIZ, 0);

            unless (defined ($rv) && length $data) {
                # This would be the end of file, so close the client
                $Log->log(9, "close the client\n");
                delete $inbuffer{$client};
                delete $inbytes{$client};
                delete $outbuffer{$client};
                delete $ready{$client};

                $select->remove($client);
                close $client;
                next;
            }

            $inbuffer{$client} .= $data;    

            # test whether the data in the buffer or the data we
            # just read means there is a complete request waiting
            # to be fulfilled. If there is, set $ready{$client}
            # to the requests waiting to be fulfilled.
            $Log->log(9, "test for completed request\n");

            unless ($inbytes{$client}) {
            # We don't know how many bytes we should have, figure it out
              if (length($inbuffer{$client}) >= $HEADER_BYTES) {
                $inbytes{$client} = substr($inbuffer{$client},0,$HEADER_BYTES);
                substr($inbuffer{$client},0,$HEADER_BYTES) = '';
              }
            }
            

            if (length($inbuffer{$client}) == $inbytes{$client}) {
              my $op = NOCpulse::Notif::EscalatorOperation->from_string(
                $inbuffer{$client});
              if ($op) {
                push( @{$ready{$client}}, $op );
              } else {
                $Log->log(1,"ERROR: Something went wrong reading from the client\n");
              }
            }
        }
    }

    # Any complete requests to process?
    $Log->log(9, "handle completed requests\n");
    foreach $client (keys %ready) {
        handle($client);
    }

    # Buffers to flush?
    $Log->log(9, "buffers to flush?\n");
    foreach $client ($select->can_write(1)) {
        $Log->log(9,"Client is ", &Dumper($client),"\n");

        # Skip this client if we have nothing to say
        unless (exists $outbuffer{$client})  {
          $Log->log(9,"Skip this client.  We have nothing to say\n");
          next
        }
        if ($outbuffer{$client} =~ /^\s*$/) {
          $Log->log(9,"Skip this client.  Message is empty string\n");
          next
        }

        $Log->log(9,"Writing data...", $outbuffer{$client}, "\n");
        $rv = $client->send($outbuffer{$client}, 0);

        unless (defined $rv) {
            # Whine, but move on.
            $Log->log(3,"I was told I could write, but I can't: '", $outbuffer{$client}, "'\n");
            next;
        }

        if ($rv  == length $outbuffer{$client} || $! == POSIX::EWOULDBLOCK) {
            $Log->log(9,"Wrote all the data.  Deleting outbuffer for client.\n");
            substr($outbuffer{$client}, 0, $rv) = ' ';
            delete $outbuffer{$client} unless length $outbuffer{$client};
        } else {
            # Couldn't write all the data, and it wasn't because
            # it would have blocked. Shutdown and move on.
            $Log->log(3,"Couldn't write all the data, and it wasn't because ",
              "it would have blocked. Shutdown and move on: ",  $outbuffer{$client}, "\n");
            delete $inbuffer{$client};
            delete $outbuffer{$client};
            delete $ready{$client};
            $select->remove($client);
            close($client);
            next;
        }
    }

# Out of band data?
$Log->log(9, "out of band data?\n");
foreach $client ($select->has_exception(0)) { # arg is timeout
        # Deal with out-of-band data here, if you want to.
}


# Do any necessary escalations
if ((time() - $last_escalate) > ESCALATION_INTERVAL) {
  $Log->log(9, "Doing escalations\n");
  eval {
    $Escalator->escalate;
  };
  if ($@) {
    $Log->log(1, "ERROR: escalate failed: $@\n");
  }
  $Log->log(9, "Done with escalations\n");
  $last_escalate = time();
}  

# Squeak out a log message for gogo (keepalive)
my $current_time = time();
if (($current_time - $last_log) >= LOG_INTERVAL) {
  $Log->log(1,"$program polling...\n");
  $last_log=$current_time;
}

} #end while (MAIN LOOP)

############
sub handle {
############
# handle ($socket) deals with all pending requests for $client

   # requests are in $ready{$client}
   # send output to $outbuffer{$client}
    my $client = shift;

    foreach my $operation (@{$ready{$client}}) {
        # $request is the text of the request
        # put text of reply into $outbuffer{$client}

        eval {
          $operation->perform($Escalator);
        };
        if ($@) {
          $Log->log(1,"Error processing ", $operation->operation, "(" ,
          join(", ",@{$operation->parameters}), ": $@\n");
        }

        my $result = freeze($operation);
        my $length = length($result);
        my $string = sprintf("%${HEADER_BYTES}.${HEADER_BYTES}i%s",
            $length, $result);
        $outbuffer{$client} = $string;
    }
    delete $ready{$client};
}

##############
sub nonblock {
##############
# nonblock ($socket) puts socket into nonblocking mode
    my $socket = shift;
    my $flags;

    $flags = fcntl ($socket, F_GETFL, 0);
    unless ($flags) {
      $Log->log(1,"FATAL ERROR: Can't get flags for socket: $!\n");
      die "Can't get flags for socket: $!\n";
    }
    my $result = fcntl ($socket, F_SETFL, $flags | O_NONBLOCK);
    unless ($result) {
      $Log->log(1,"FATAL ERROR: Can't make socket nonblocking: $!\n");
      die "Can't make socket nonblocking: $!\n";
    }
}

###################
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);
    $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);
}
      
