package Zabbix::Sender;
{
  $Zabbix::Sender::VERSION = '0.03';
}
# ABSTRACT: A pure-perl implementation of zabbix-sender.

use Moose;
use namespace::autoclean;

use JSON;
use IO::Socket;
use IO::Select;
use Net::Domain;


has 'server' => (
    'is'       => 'rw',
    'isa'      => 'Str',
    'required' => 1,
);

has 'port' => (
    'is'      => 'rw',
    'isa'     => 'Int',
    'default' => 10051,
);

has 'timeout' => (
    'is'      => 'rw',
    'isa'     => 'Int',
    'default' => 30,
);

has 'hostname' => (
    'is'      => 'rw',
    'isa'     => 'Str',
    'lazy'    => 1,
    'builder' => '_init_hostname',
);

has 'interval' => (
    'is'      => 'rw',
    'isa'     => 'Int',
    'default' => 1,
);

has 'retries' => (
    'is'      => 'rw',
    'isa'     => 'Int',
    'default' => 3,
);

has 'keepalive' => (
    'is'    => 'rw',
    'isa'   => 'Bool',
    'default' => 0,
);

has '_json' => (
    'is'      => 'rw',
    'isa'     => 'JSON',
    'lazy'    => 1,
    'builder' => '_init_json',
);

has '_last_sent' => (
    'is'      => 'rw',
    'isa'     => 'Int',
    'default' => 0,
);

has '_socket' => (
    'is'    => 'rw',
    'isa'   => 'Maybe[IO::Socket]',
);


sub _init_json {
    my $self = shift;

    my $JSON = JSON::->new->utf8();

    return $JSON;
}


sub _init_hostname {
    my $self = shift;

    return Net::Domain::hostname() . '.' . Net::Domain::hostdomain();
}


has 'zabbix_template_1_8' => (
    'is'      => 'ro',
    'isa'     => 'Str',
    'default' => "a4 b c4 c4 a*",
);


sub _encode_request {
    my $self  = shift;
    my $item  = shift;
    my $value = shift;
    my $clock = shift;

    my $data_ref = {
        'host'  => $self->hostname(),
        'key'   => $item,
        'value' => $value,
    };
    $data_ref->{'clock'} = $clock if defined($clock);

    my $data = {
        'request' => 'sender data',
        'data'    => [$data_ref],
    };

    my $output = '';
    my $json   = $self->_json()->encode($data);

    # turn on byte semantics to get the real length of the string
    use bytes;
    my $length = length($json);
    no bytes;

    ## no critic (ProhibitBitwiseOperators)
    $output = pack(
        $self->zabbix_template_1_8(),
        "ZBXD", 0x01,
        ( $length & 0xFF ),
        ( $length & 0x00FF ) >> 8,
        ( $length & 0x0000FF ) >> 16,
        ( $length & 0x000000FF ) >> 24,
        0x00, 0x00, 0x00, 0x00, $json
    );
    ## use critic

    return $output;
}


sub _decode_answer {
    my $self = shift;
    my $data = shift;

    my ( $ident, $answer );
    $ident = substr( $data, 0, 4 ) if length($data) > 3;
    $answer = substr( $data, 13 ) if length($data) > 12;

    if ( $ident && $answer ) {
        if ( $ident eq 'ZBXD' ) {
            my $ref = $self->_json()->decode($answer);
            if ( $ref->{'response'} eq 'success' ) {
                return 1;
            }
        }
    }
    return;
}


# DGR: Anything but send just doesn't makes sense here. And since this is a pure-OO module
# and if the implementor avoids indirect object notation you should be fine.
## no critic (ProhibitBuiltinHomonyms)
sub send {
## use critic
    my $self  = shift;
    my $item  = shift;
    my $value = shift;
    my $clock = shift;

    my $status = 0;
    foreach my $i ( 1 .. $self->retries() ) {
        if ( $self->_send( $item, $value, $clock ) ) {
            $status = 1;
            last;
        }
    }

    if ($status) {
        return 1;
    }
    else {
        return;
    }

}

sub _send {
    my $self  = shift;
    my $item  = shift;
    my $value = shift;
    my $clock = shift;

    if ( time() - $self->_last_sent() < $self->interval() ) {
        my $sleep = $self->interval() - ( time() - $self->_last_sent() );
        $sleep ||= 0;
        sleep $sleep;
    }

    $self->_connect() unless $self->_socket();
    $self->_socket()->send( $self->_encode_request( $item, $value, $clock ) );
    my $Select  = IO::Select::->new($self->_socket());
    my @Handles = $Select->can_read( $self->timeout() );

    my $status = 0;
    if ( scalar(@Handles) > 0 ) {
        my $result;
        $self->_socket()->recv( $result, 1024 );
        if ( $self->_decode_answer($result) ) {
            $status = 1;
        }
    }
    $self->_disconnect() unless $self->keepalive();
    if ($status) {
        return $status;
    }
    else {
        return;
    }
}

sub _connect {
    my $self = shift;

    my $Socket = IO::Socket::INET::->new(
        PeerAddr => $self->server(),
        PeerPort => $self->port(),
        Proto    => 'tcp',
        Timeout  => $self->timeout(),
    ) or die("Could not create socket: $!");

    $self->_socket($Socket);

    return 1;
}

sub _disconnect {
    my $self = shift;

    if(!$self->_socket()) {
        return;
    }

    $self->_socket()->close();
    $self->_socket(undef);

    return 1;
}


sub DEMOLISH {
    my $self = shift;

    $self->_disconnect();

    return 1;
}

no Moose;
__PACKAGE__->meta->make_immutable;


1;    # End of Zabbix::Sender

__END__

=pod

=head1 NAME

Zabbix::Sender - A pure-perl implementation of zabbix-sender.

=head1 VERSION

version 0.03

=head1 SYNOPSIS

This code snippet shows how to send the value "OK" for the item "my.zabbix.item"
to the zabbix server/proxy at "my.zabbix.server.example" on port "10055".

    use Zabbix::Sender;

    my $Sender = Zabbix::Sender->new({
    	'server' => 'my.zabbix.server.example',
    	'port' => 10055,
    });
    $Sender->send('my.zabbix.item','OK');

=head1 NAME

Zabbix::Sender - A pure-perl implementation of zabbix-sender.

=head1 SUBROUTINES/METHODS

=head2 _init_json

Zabbix 1.8 uses a JSON encoded payload after a custom Zabbix header.
So this initializes the JSON object.

=head2 _init_hostname

The hostname of the sending instance may be given in the constructor.

If not it is detected here.

=head2 zabbix_template_1_8

ZABBIX 1.8 TEMPLATE

a4 - ZBXD
b  - 0x01
c4 - Length of Request in Bytes (64-bit integer), aligned left, padded with 0x00
c4 - dito
a* - JSON encoded request

This may be changed to a HashRef if future version of zabbix change the header template.

=head2 _encode_request

This method encodes the item and value as a json string and creates
the required header acording to the template defined above.

=head2 _decode_answer

This method tries to decode the answer received from the server.

=head2 send

Send the given item with the given value to the server.

Takes two arguments: item and value. Both should be scalars.

=head2 DEMOLISH

Disconnects any open sockets on destruction.

=head1 AUTHOR

"Dominik Schulz", C<< <"lkml at ds.gauner.org"> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-zabbix-sender at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Zabbix-Sender>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Zabbix::Sender

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Zabbix-Sender>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/Zabbix-Sender>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/Zabbix-Sender>

=item * Search CPAN

L<http://search.cpan.org/dist/Zabbix-Sender/>

=back

=head1 ACKNOWLEDGEMENTS

This code is based on the documentation and sample code found at:

=over 4

=item http://www.zabbix.com/wiki/doc/tech/proto/zabbixsenderprotocol

=item http://www.zabbix.com/documentation/1.8/protocols

=back

=head1 LICENSE AND COPYRIGHT

Copyright 2011 Dominik Schulz.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=head1 AUTHOR

Dominik Schulz <dominik.schulz@gauner.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Dominik Schulz.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
