package Monitor::bb;

require 5.002;
require Exporter;

use strict;
use vars qw(@ISA @EXPORT $VERSION);
use Socket qw( SOCK_STREAM PF_INET inet_aton sockaddr_in inet_ntoa INADDR_ANY);
use FileHandle;
use Carp;

@ISA = qw(Exporter Monitor::Monitor);
$Monitor::bb::vardump = 0;


sub new
{
    my ($this, $host, $timeout, $port) = @_;
    my  $class = ref($this) || $this;
    my  $self = {};

    print "Monitor::bb new()\n" if( $BigSister::common::dl>1 );
    bless($self, $class);

    ($timeout = 8) if( ! $timeout );
    $self->{"timeout"} = $timeout;

    ($port = "bb") if( ! $port );
    unless( $port =~ /^\d+$/ ) { $port = (getservbyname($port, 'tcp'))[2]; }
    ($port = 1984) if( ! $port );

    $self->{"port"} = $port;
    $self->{"host"} = $host;
    $self->{"proto_num"} = (getprotobyname('tcp'))[2];
    $self->{"fh"} = FileHandle->new();
    $self->{"FQDN_sep"} = $BigSister::common::domain_encode;
    $self->{"FQDN"} = 1;
    $self->{"bind_ip"} = sockaddr_in(0, INADDR_ANY);
    $self->{"retries"} = [];
    $self->{"perf_resolveinterval"} = 60;

    return($self);
}


sub perfdata {
    my( $self, $file ) = @_;
    my @perf = ();

    $file = BigSister::common::find_file($file);
    open( _IN, "<$file" );
    while( <_IN> ) {
	chomp;
	print STDERR "bb:perfdata: adding $_\n" if( $BigSister::common::dl>7 );
	push( @perf, $_ );
    }
    $self->{"perfraw"} = \@perf;
    $self->{"perf"} = [];
    $self->{"perf_resolve"} = 0;
    close _IN;
}


sub options {
    my( $self, @options ) = @_;
    my $opt;

    $self->{"options_set"} = 1;
    foreach $opt ( @options ) {
	if( grep( $opt eq $_, "perf", "status", "group" ) ) {
	    $self->{"option-$opt"} = 1;
	}
    }
}



sub perfdata_resolve {
    my( $self ) = @_;
    my( $var, $pattern, $dump );

    return if( time - $self->{"perf_resolve"} < $self->{"perf_resolveinterval"} );
    1 while( unlink "$BigSister::common::root/var/uxmon_variables" );
    open( DUMP, ">$BigSister::common::fs{'var'}/uxmon_variables" );
    $Monitor::bb::vardump = $dump;
    my @vars = ();
    my @names = ();
    my $host;
    foreach $host (keys %uxmon::monitor_variables) {
	push( @names, map( $host.":".$_, keys %{$uxmon::monitor_variables{$host}} ) );
	my( $var, $val );
	while( ($var,$val) = each %{$uxmon::monitor_variables{$host}} ) {
	    print DUMP "$host:$var = $val\n";
	}
    }
    close(DUMP);
    foreach $pattern ( @{$self->{"perfraw"}} ) {
	eval {
	    push( @vars, grep( /^$pattern$/, @names) );
	}
    }
    $self->{"perf"} = \@vars;
    print STDERR "perfdata_resolve: patterns resolved to ".($#vars+1)." variables\n";
    $self->{"perf_resolve"} = time;
    $self->{"perf_resolveinterval"} += 480;
    if( $self->{"perf_resolveinterval"} > 4*3600 ) {
	$self->{"perf_resolveinterval"} = 4*3600;
    }
}



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

    $self->{"FQDN"} = $fqdn;
}

sub bind_ip {
    my( $self, $ip ) = @_;

    $self->{"bind_ip"} = sockaddr_in(0, inet_aton($ip));
}

sub bb_compat {
    my( $self, $compat ) = @_;

    $self->{"compat"} = $compat;
    $self->{"FQDN_sep"} = ($compat?",":$BigSister::common::domain_encode);
}


sub set_time {
    my( $self, $time ) = @_;

    $self->{"override_time"} = $time;
}


sub run {
    my( $self, @args ) = @_;
    $self->report( @args );
}


sub report
{
    my ($self, $status, $comment, $join) = @_;
    my( $date, $item, $item_status, $ip, $shitem, $add, $i );

    my @retries = ();
    $ip = inet_aton( $self->{"host"} );
    unless( $ip ) {
	print "to bb: error, host unknown: ".$self->{"host"}."\n" if( $BigSister::common::dl>1 );
	return;
    }
    $self->_start_report( $ip ) unless( $self->{"compat"} );
    $date = time unless( $date = $self->{"override_time"} );
    if( (! $self->{"compat"}) && ((! $self->{"options_set"}) || $self->{"option-group"}) ) {
	while( ($item, $item_status) = each( %$join ) ) {
	    next unless( @$item_status );
	    $shitem = substr( $item, 1 );
	    $add = substr( $item, 0, 1 );
	    ($add eq "+") && ($add = "join");
	    ($add eq "-") && ($add = "leave");
	    ($add eq "D") && ($add = "displayname");
	    $self->report_hw( "$add $shitem ".join(" ",@$item_status) );
	}
    }

    if( defined( $self->{"perf"} ) && ! $self->{"compat"} &&
	((! $self->{"options_set"}) || $self->{"option-perf"})) {
	my $var;

	$self->perfdata_resolve();
	foreach $var ( @{$self->{"perf"}} ) {
	    $var =~ /^(.*?):(.*)$/;
	    my $msg = "perf ".time." $var ".($uxmon::monitor_variables{$1}->{$2});
	    $msg =~ s/\n/|>/g;
	    $self->report_hw( $msg ) || push( @retries, $msg );
	}
    }
    my @fastretries = ();
    if( (! $self->{"options_set"}) || $self->{"option-status"} ) {
	while( ($item,$item_status) = each( %$status ) ) {
	    print "to bb: $item: $item_status (".$$comment{$item}.")\n" if( $BigSister::common::dl>3 );
	    # bs's bbd has a length restriction; try to avoid tripping it
	    if( length( $$comment{$item} ) > 29000 ) {
		print "bb::report: $item probably too big, chopping\n"
		    if( $BigSister::common::dl > 5 );
		$$comment{$item} = substr( $$comment{$item}, 0, 29000 );
	    }
	    $self->_start_report( $ip ) if( $self->{"compat"} );
	    my $msg = $self->report_one( $ip, $item, $item_status, $$comment{$item}, $date );
	    push(@fastretries, $msg) if $msg ne '';
	    $self->_end_report( $ip ) if( $self->{"compat"} );
	}
    }
    my $retryref = $self->{"retries"};
    if( @retries ) {
	print "bb::report: queueing ", scalar @retries, " retries\n"
	    if( $BigSister::common::dl > 5 );
	push( @$retryref, @retries );
    }
    print "bb::report: retrying ", scalar @$retryref, " now\n"
	if( @$retryref && $BigSister::common::dl > 5 );
    for( $i=$#$retryref+1; $i>=0; $i-- ) {
	if( $self->report_hw( $retryref->[$i] )) {
	    splice( @$retryref, $i, 1 );
	}
    }
    # retry sending "fastretries" right now and drop them if we still fail to
    # send them
    foreach $status (@fastretries) {
	$self->report_hw( $status );
    }
    print "bb::report: ", scalar @$retryref, " queued to retry later\n"
	if @$retryref && $BigSister::common::dl > 5;
	
    $self->_end_report unless( $self->{"compat"} );
}



sub sysreport {
    my( $self, $msg ) = @_;

    unless( $self->{"connected"} ) {
	return(0) if( $self->{"connectfailure"} );
	my $ip = inet_aton( $self->{"host"} );
	unless( $ip ) {
	    print "to bb: error, host unknown: ".$self->{"host"}."\n" if( $BigSister::common::dl>1 );
	    return(0);
	}
	$self->_start_report( $ip );
	$self->{"connectfailure"} = ! $self->{"connected"};
    }
    return(0) unless( $self->{"connected"} );
    my $result = $self->report_hw( $msg );
    if( $self->{"compat"} ) {
	$self->_end_report();
    }
    return $result;
}



sub _start_report
{
    my ($self, $ip) = @_;
    my ($timeout, $saddr );
    my( $port, $res );

    $timeout = $self->{"timeout"};
    $port = $self->{"port"};

    socket($self->{"fh"}, &PF_INET(), &SOCK_STREAM(), $self->{"proto_num"}) || return( 0 );
    my ($tp, $ti) = sockaddr_in($self->{"bind_ip"});
    print "Binding to ", inet_ntoa( $ti ), "\n" if ( $BigSister::common::dl );
    bind($self->{"fh"}, $self->{"bind_ip"}) || print "Could not bind\n";
    $saddr = sockaddr_in($port, $ip);

    $res = $self->{"connected"} = 0;
    eval {
	$SIG{'ALRM'} = sub { die "Monitor::bb:_start_report timed out" };
	Platform::bsalarm($timeout); 
	    
	if( Platform::bsconnect($self->{"fh"}, $saddr) ) {
	    $self->{"connected"} = $res = 1;
	} else {
	    die "connect: $!\n";
	}
	Platform::bsalarm(0);
    };
    print $@ if $@;
    Platform::bsalarm(0);
    close $self->{"fh"} unless( $res );
    $Monitor::bb::bsize = 0;
    return $res;
}



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

    select( undef, undef, undef, 0.5 );
    close( $self->{"fh"} );
    select( undef, undef, undef, 0.1 );
    $self->{"connected"} = 0;
    $self->{"connectfailure"} = 0;
    $Monitor::bb::bsize = 0;
}



sub report_one
{
    my ($self, $ip, $item, $status, $comment, $date) = @_;
    my $msg;

    # the original code lost if the machine didn't have an fqdn to start with.
    # that has been fixed in BS_unix.pm, but we also do so here because we
    # might be reporting for someone else (forwarded messages, remote syslog,
    # etc.)
    if( $self->{"FQDN"} !~ /^([nf0]|of)/i ) {
	$item =~ s/(\.|\Q$BigSister::common::domain_encode\E)/./g;
    }
    else {
        my @tok = split( /(\.|\Q$BigSister::common::domain_encode\E)/, $item );
        my $check = pop @tok;
        $item = join( $self->{"FQDN_sep"}, @tok ).".".$check;
    }

    if( $self->{"compat"} ) {
	$comment =~ s/\|\>/\n/g;
    }
    else {
	$comment =~ s/\n/\|\>/g;
    }
    # this didn't do the retry thing, which is just bad.
    # unfortunately, the retry stuff is local to report()...
    $msg = "status $item ".($BigSister::common::status_texts{$status})." ($date) ".localtime($date)." $comment";
    if( $self->report_hw( $msg ) ) {
      return "";
    } else {
      return $msg;
    }
}

sub report_hw
{
    my ($self, $msg ) = @_;
    my ($timeout, $saddr );
    my( $res );

    $timeout = $self->{"timeout"};
    return(0) unless( $self->{"connected"} );

    # bbd disconnects rudely if we send more than 30000 bytes per connection.
    # disconnect and reconnect if we're going to go over.
    print "reporter: at $Monitor::bb::bsize send @{[length( $msg )]}\n"
      if( $BigSister::common::dl > 6 );
    if( $Monitor::bb::bsize + length( $msg ) >= 30000 ) {
	print "reporter reopening connection\n" if( $BigSister::common::dl );
	$self->_end_report();
	unless( $self->_start_report( inet_aton( $self->{"host"} ) ) ) {
	    warn "report_hw: _start_report lost\n";
	    return 0;
	}
    }
    $res = 0;
    eval {
	$SIG{'ALRM'} = sub { die "Timed out\n" };
	Platform::bsalarm($timeout); 
	    
	$msg .= "\n";
	print "reporter reporting: ".$msg if( $BigSister::common::dl>5 );
	unless( defined( $res = send( $self->{"fh"}, $msg, 0 ) ) && $res == length( $msg ) ) {
	    my $err = defined( $res ) ? "wrote $res bytes" : "error: [$!]";
	    $res = 0;
	    $self->_end_report;
	    die "write error: $err\n";
	}
	Platform::bsalarm(0);
	$res = 1;
	# account for it so we know when to nuke the connection
	$Monitor::bb::bsize += length( $msg );
    };
    Platform::bsalarm(0);
    if ($@) {
	my $err = $@;
	chomp($err);
	my $what = join( ' ', (split( ' ', $msg ))[0..2] );
	print "reporter: send '$what ...' lost ($err)\n";
	# and try to recover; caller should resend this packet
	$self->_end_report();
	$self->_start_report( inet_aton( $self->{"host"} ) );
    }
    return $res;
}


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

    return( $self->{"fh"} );
}



sub perfreport {
     my( $self, $alias, $variable, $time, $value ) = @_;

     push( @{$self->{"retries"}}, "perf $time $alias:$variable $value" );
}


sub postevent {
     my( $self, $item, $time, $priority, $text ) = @_;

     $text =~ s/[\r\n]/\|\>/g;
     push( @{$self->{"retries"}}, "event $time $item $priority $text" );
}

1;
