package Opsview::REST;
{
  $Opsview::REST::VERSION = '0.013';
}

use Moo;
use Carp;
use Opsview::REST::Config;
use Opsview::REST::Exception;

with 'Opsview::REST::APICaller';

has [qw/ user base_url /] => (
    is       => 'ro',
    required => 1,
);

has [qw/ pass auth_tkt /] => (
    is  => 'ro',
);

{
    # install methods in the namespace for configurable objects
    my @config_objects = qw/
        contact host role servicecheck hosttemplate attribute timeperiod
        hostgroup servicegroup notificationmethod hostcheckcommand keyword
        monitoringserver
    /;

    for my $obj_type (@config_objects) {
        no strict 'refs';

        my $general_url = Opsview::REST::Config->new($obj_type);

        # Single object get (get_contact, get_host, ...)
        # URL: /rest/config/{object_type}/{id}
        # GET - get object details
        *{__PACKAGE__ . "::get_$obj_type"} = sub {
            my $self = shift;
            my $id   = shift;
            croak "Required id" unless defined $id;

            my $uri = Opsview::REST::Config->new($obj_type, $id);
            return $self->get($uri->as_string);
        };

        # Multiple object get (get_contacts, get_hosts, ...)
        # URL: /rest/config/{object_type}
        # GET - list object type. Can pass in search attributes
        *{__PACKAGE__ . '::get_' . $obj_type . 's'} = sub {
            my $self = shift;
            require JSON;
            my $uri = Opsview::REST::Config->new(
                $obj_type,
                json_filter => JSON::encode_json({@_}),
            );
            return $self->get($uri->as_string);

        };

        # Create object
        # URL: /rest/config/{object_type}
        # POST - add a new object or a list of object type
        *{__PACKAGE__ . "::create_$obj_type"} = sub {
            my $self = shift;
            my $uri  = Opsview::REST::Config->new($obj_type);
            my $to_post;
            if (ref $_[0] && ref $_[0] eq 'ARRAY') {
                $to_post = { list => shift };
            } else {
                $to_post = { @_ };
            }
            return $self->post($uri->as_string, $to_post);
        };

        # Alias to call last method in plural
        *{__PACKAGE__ . "::create_${obj_type}s"} =
            *{__PACKAGE__ . "::create_$obj_type"};

        # Clone object
        # URL: /rest/config/{object_type}/{id}
        # POST - clone this object with merged incoming data to create
        # new object
        *{__PACKAGE__ . "::clone_$obj_type"} = sub {
            my $self = shift;
            my $id   = shift;
            croak "Required id" unless defined $id;

            my $uri = Opsview::REST::Config->new($obj_type, $id);
            return $self->post($uri->as_string, { @_ });
        };

        # Create or update
        # URL: /rest/config/{object_type}
        # PUT - create or update (based on unique keys) object or a list
        # of objects
        *{__PACKAGE__ . "::create_or_update_$obj_type"} = sub {
            my $self = shift;
            my $uri  = Opsview::REST::Config->new($obj_type);
            my $to_post;
            if (ref $_[0] && ref $_[0] eq 'ARRAY') {
                $to_post = { list => shift };
            } else {
                $to_post = { @_ };
            }
            return $self->put($uri->as_string, $to_post);
        };
        # Alias to call last method in plural
        *{__PACKAGE__ . "::create_or_update_${obj_type}s"} =
            *{__PACKAGE__ . "::create_or_update_$obj_type"};


        # Update
        # URL: /rest/config/{object_type}/{id}
        # PUT - update this object's details
        *{__PACKAGE__ . "::update_$obj_type"} = sub {
            my $self = shift;
            my $id   = shift;
            croak "Required id" unless defined $id;

            my $uri = Opsview::REST::Config->new($obj_type, $id);
            return $self->put($uri->as_string, { @_ });
        };

        # Delete
        # URL: /rest/config/{object_type}/{id}
        # DELETE - delete object
        *{__PACKAGE__ . "::delete_$obj_type"} = sub {
            my $self = shift;
            my $id   = shift;
            croak "Required id" unless defined $id;

            my $uri = Opsview::REST::Config->new($obj_type, $id);
            return $self->delete($uri->as_string, { @_ });
        };
    }
}

sub BUILD {
    my ($self) = @_;
    
    my $r;
    if (defined $self->pass) {
        $r = $self->post('/login', {
            username => $self->user,
            password => $self->pass,
        });

    } elsif (defined $self->auth_tkt) {
        $self->headers->{'Cookie'} = 'auth_tkt=' . $self->auth_tkt . ';';
        $r = $self->post('/login_tkt', { username => $self->user });

        # Clean the cookie as this is not required anymore
        delete $self->headers->{'Cookie'};

    } else {
        croak "Need either a pass or an auth_tkt";
    }

    $self->headers->{'X-Opsview-Username'} = $self->user;
    $self->headers->{'X-Opsview-Token'}    = $r->{token};

}

# Status
sub status {
    my $self = shift;

    require Opsview::REST::Status;
    my $uri = Opsview::REST::Status->new(@_);

    return $self->get($uri->as_string);
}

# Event
sub events {
    my $self = shift;

    require Opsview::REST::Event;
    my $uri = Opsview::REST::Event->new(@_);

    return $self->get($uri->as_string);
}

# Downtime
sub _downtime {
    my $self = shift;

    require Opsview::REST::Downtime;
    my $uri = Opsview::REST::Downtime->new(@_);

    return $uri->as_string;
}

sub downtimes {
    my $self = shift;
    return $self->get($self->_downtime(@_));
}

sub create_downtime {
    my $self = shift;
    return $self->post($self->_downtime(@_));
}

sub delete_downtime {
    my $self = shift;
    return $self->delete($self->_downtime(@_));
}

# Reload
sub reload {
    my $self = shift;
    return $self->post('/reload');
}

sub reload_info {
    my $self = shift;
    return $self->get('/reload');
}

# Acknowledge
sub _ack {
    my $self = shift;

    require Opsview::REST::Acknowledge;
    my $uri = Opsview::REST::Acknowledge->new(@_);

    return $uri->as_string;
}

sub acknowledge_list {
    my $self = shift;
    return $self->get($self->_ack(@_));
}

sub acknowledge {
    my $self = shift;
    return $self->post($self->_ack(@_));
}

# Recheck
sub recheck {
    my $self = shift;

    require Opsview::REST::Recheck;
    my $uri = Opsview::REST::Recheck->new(@_);

    return $self->post($uri->as_string);
}

__PACKAGE__->meta->make_immutable;

1;
__END__

=pod

=head1 NAME
    
Opsview::REST - Interface to the Opsview REST API

=head1 SYNOPSIS

    use Opsview::REST;

    my $ops = Opsview::REST->new(
        base_url => 'http://opsview.example.com/rest',
        user     => 'username',
        pass     => 'password',
    );

    # Check status
    my $status = $ops->status(
        'hostgroup',
        'hostgroupid' => [1, 2],
        'filter'      => 'unhandled',
    );

    # Configuration methods
    my $host1 = $ops->create_host(
        ip                  => '192.168.0.1',
        name                => 'monitoring-slave',
        hostgroup           => { name => 'Monitoring Servers' },
        notification_period => { name => '24x7' },
    );

    $ops->clone_host(
        $host1->{object}->{id},
        name => 'another-host',
        ip   => '192.168.0.2',
    );

    # Search methods support complex SQL::Abstract queries
    my $hosts = $ops->get_hosts(
        -or => [
            name => { -like => '%.example.com' },
            ip   => { -like => '10.25.%' },
        ],
    );

    # Update several objects at once
    map { $_->{check_attempts} = 4 } @{ $hosts->{list} };
    my $response = $ops->create_or_update_hosts($hosts->{list});

    # ... or only one
    my $response = $ops->create_or_update_host(
        name         => 'host1.example.com',
        snmp_version => '2c',
    );

    # Reload after make changes in config
    $ops->reload;

=head1 DESCRIPTION

Opsview::REST is a set of modules to access the Opsview REST API, which is the
recommended method for scripting configuration changes or any other form of
integration since version 3.9.0

=head1 METHODS

=head2 new

Return an instance of the Opsview::REST.

=head3 Required Arguments

=over 4

=item base_url

Base url where the REST API resides. By default it is under C</rest>.

=item user

Username to login as.

=back

=head3 Other Arguments

=over 4

=item pass

=item auth_tkt

Either the pass or the auth_tkt MUST be passed. It will die horribly if none
of these are found.

=item ua

A user agent object can be provided here. It should be an L<HTTP::Tiny>
subclass.

=back

=head2 get($url)

Makes a "GET" request to the API. The response is properly deserialized and
returned as a Perl data structure.

=head2 status( $endpoint, [ %args ] )

Convenience method to request the "status" part of the API. C<$endpoint> is
the endpoint to send the query to. C<%args> is a hash which will get properly
translated to URL arguments.

More info: L<http://docs.opsview.com/doku.php?id=opsview-core:restapi:status>

=head2 downtimes

=head2 create_downtime( %args )

=head2 delete_downtime( [ %args ] )

Downtime related methods.

More info: L<http://docs.opsview.com/doku.php?id=opsview-core:restapi:downtimes>

=head2 events( [ %args ] )

Get events. An event is considered to be either:

=over 4

=item *

a host or service changing state

=item *

a host or service result during soft failures

=item *

a host or service in a failure state where 'alert every failure' is enabled

=back

More info: L<http://docs.opsview.com/doku.php?id=opsview-core:restapi:event>

=head2 acknowledge( [ %args ] )

Acknowledge problems.

More info: L<http://docs.opsview.com/doku.php?id=opsview-core:restapi:acknowledge>

=head2 acknowledge_list

Lists the problems which the current logged in user has permission to
acknowledge.

=head2 reload

Initiates a synchronous reload. Be careful: if your opsview reload takes more
than 60 seconds to run, this call will time out. The returned data contains
the info of the reload.

More info: L<http://docs.opsview.com/doku.php?id=opsview-core:restapi#initiating_an_opsview_reload>

=head2 reload_info

Get status of reload.

More info: L<http://docs.opsview.com/doku.php?id=opsview-core:restapi#initiating_an_opsview_reload>

=head2 recheck( [ %args ] )

Recheck services or hosts asynchronously. It returns info about the number of
hosts and services that will be rechecked.

More info: L<http://docs.opsview.com/doku.php?id=opsview-core:restapi:recheck>

=head2 Config methods for single objects

=head3 get_*

=head3 create_*

=head3 clone_*

=head3 create_or_update_*

=head3 delete_*

This methods will be generated for the following types of objects: C<contact>,
C<role>, C<servicecheck>, C<hosttemplate>, C<attribute>, C<timeperiod>,
C<hostgroup>, C<servicegroup>, C<notificationmethod>, C<hostcheckcommand>,
C<keyword>, C<monitoringserver>.

They all except C<create>, require the object's id. Additionally, C<create>,
C<clone> and C<create_or_update> accept a list of key-value pairs:

    my $host1 = $ops->create_host(
        name => 'host1',
        ip   => '192.168.10.27',
    );

    $ops->clone_host(
        $host1->{object}->{id},
        name => 'host2',
        ip   => '192.168.10.28',
    );

    $host->delete($id);

=head2 Config methods for multiple objects

=head3 get_*

=head3 create_*

=head3 create_or_update_*

This methods will be generated for the following types of objects: C<contacts>,
C<roles>, C<servicechecks>, C<hosttemplates>, C<attributes>, C<timeperiods>,
C<hostgroups>, C<servicegroups>, C<notificationmethods>, C<hostcheckcommands>,
C<keywords>, C<monitoringservers>.

C<get> accepts complex queries in L<SQL::Abstract> format.

C<create_or_update> is specially useful when you want to update several objects
with a single call:

    # First get a list of objects you want to modify
    my $dbhosts = $ops->get_hosts(
        name    => { -like => 'db%' },
    );

    # $dbhosts = {
    #   summary => { ... },
    #   list => [ { name => 'db1.example.com , ... }, ... ],
    # };

    # Modify them as you need
    map { $_->{check_attempts} = 4 } @{ $dbhosts->{list} };

    # Make the call
    $ops->create_or_update($dbhosts->{list});


To know which fields are accepted for each type of object, the format of the
responses, and additional info:

L<http://docs.opsview.com/doku.php?id=opsview-core:restapi:config>

=head1 SEE ALSO

=over 4

=item *

L<http://www.opsview.org/>

=item *

L<Opsview REST API Documentation|http://docs.opsview.com/doku.php?id=opsview-core:restapi>

=back

=head1 AUTHOR

=over 4

=item *

Miquel Ruiz <mruiz@cpan.org>

=back

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Miquel Ruiz.

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