package NGCP::Prometheus::HTTP;

use strict;
use warnings;

use Carp;
use HTTP::Tiny;
use JSON::XS;

our $VERSION = '0.01';

sub new
{
    my ($this, %opts) = @_;
    my $class = ref $this || $this;

    my $self = {
        addr => undef,
    };

    my %http_attr = (
        agent => "NGCP-PromQL-HTTP/$VERSION",
        timeout => $opts{timeout} // 180,
    );
    my $http;
    my $host = $opts{host} // 'localhost';
    my $port = $opts{port} // 9090;
    $host = lc $host;
    $http = HTTP::Tiny->new(%http_attr);
    $self->{addr} = "http://$host:$port";
    $self->{http} = $http;

    bless $self, $class;

    return $self;
}

sub get_http
{
    my $self = shift;

    return $self->{http};
}

sub _get_http_api_uri
{
    my ($self, $endpoint, $query_data) = @_;

    my $uri = "$self->{addr}/api/v1/$endpoint";
    if (defined $query_data) {
        my $uri_params = $self->{http}->www_form_urlencode($query_data);
        $uri .= "?$uri_params";
    }

    return $uri;
}

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

    my $res_error;
    eval {
        my $data = decode_json($res->{content});

        if ($data->{status} eq 'success') {
            return {
                retval => 1,
            };
        }
        $res_error = $data->{error};
    } or do {
        $res_error = "$res->{status} $res->{reason}";
    };

    return {
        retval => 0,
        error => $res_error,
        http_code => $res->{status},
        http_error => $res->{reason},
    };
}

sub _validate_duration
{
    my $duration = shift;

    return $duration =~ m/[0-9]+[smhdwy]/;
}

# RFC3339 or seconds-since-epoch
sub _validate_time
{
    my $time = shift;

    # TODO

    return 1;
}

my %BOOL = (
    false => 0,
    true => 1
);

sub _validate_bool
{
    my $bool = shift;

    return exists $BOOL{$bool};
}

##
## Management API
##

sub _mgmt_api
{
    my ($self, $endpoint) = @_;
    my $uri = $self->_get_api_uri("-/$endpoint");
    my $res = $self->{http}->head($uri);

    if (! $res->{success}) {
        return $self->_get_response_error($res);
    }

    return {
        retval => 1,
    };
}

sub healthy
{
    my $self = shift;

    return $self->_mgmt_api('healthy');
}

sub ready
{
    my $self = shift;

    return $self->_mgmt_api('ready');
}

sub reload
{
    my $self = shift;

    return $self->_mgmt_api('reload');
}

sub quit
{
    my $self = shift;

    return $self->_mgmt_api('quit');
}

##
## Querying API
##

sub _query_api
{
    my ($self, $endpoint, $uri_query) = @_;

    my $uri = $self->_get_http_api_uri($endpoint, $uri_query);

    my $res = $self->{http}->get($uri);

    if (! $res->{success}) {
        return $self->_get_response_error($res);
    }

    my $data = decode_json($res->{content});

    return {
        retval      => 1,
        data        => $data,
        result_type => $data->{data}{resultType},
        result      => $data->{data}{result},
        json        => $res->{content},
    };
}

sub query
{
    my ($self, $query, %opts) = @_;

    if (not defined $query) {
        croak('missing query argument');
    }

    my $uri_query = { query => $query };
    if (defined $opts{time}) {
        if (not _validate_time($opts{time})) {
            croak('invalid time argument');
        }
        $uri_query->{time} = $opts{time};
    }
    if (defined $opts{timeout}) {
        if (not _validate_duration($opts{timeout})) {
            croak('invalid timeout argument');
        }
        $uri_query->{timeout} = $opts{timeout};
    }

    return $self->_query_api('query', $uri_query);
}

sub query_range
{
    my ($self, $query, %opts) = @_;

    if (not defined $query) {
        croak('missing query argument');
    }

    my $uri_query = { query => $query };
    foreach my $time (qw(start end)) {
        if (defined $opts{$time}) {
            if (not _validate_time($opts{$time})) {
                croak("invalid $time argument");
            }
            $uri_query->{$time} = $opts{$time};
        }
    }
    foreach my $delta (qw(step timeout)) {
        if (defined $opts{$delta}) {
            if (not _validate_duration($opts{$delta})) {
                croak("invalid $delta argument");
            }
            $uri_query->{$delta} = $opts{$delta};
        }
    }

    return $self->_query_api('query_range', $uri_query);
}

##
## Querying Metadata API
##

sub _query_meta_api
{
    my ($self, $endpoint, $uri_query) = @_;

    my $uri = $self->_get_http_api_uri($endpoint, $uri_query);

    my $res = $self->{http}->get($uri);

    if (! $res->{success}) {
        return $self->_get_response_error($res);
    }

    my $data = decode_json($res->{content});

    return {
        retval      => 1,
        data        => $data,
        json        => $res->{content},
    };
}

sub series
{
    my ($self, $matches, %opts) = @_;

    if (not defined $matches) {
        croak('missing matches argument');
    }

    my $uri_query = { 'match[]' => $matches };
    foreach my $time (qw(start end)) {
        if (defined $opts{$time}) {
            if (not _validate_time($opts{$time})) {
                croak("invalid $time argument");
            }
            $uri_query->{$time} = $opts{$time};
        }
    }

    return $self->_query_meta_api('series', $uri_query);
}

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

    return $self->_query_meta_api('labels');
}

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

    return $self->_query_meta_api("label/$label/values");
}

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

    return $self->_query_meta_api('targets');
}

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

    return $self->_query_meta_api('rules');
}

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

    return $self->_query_meta_api('alerts');
}

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

    return $self->_query_meta_api('alertmanagers');
}

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

    return $self->_query_meta_api("status/$item");
}

1;

__END__

=head1 NAME

NGCP::Prometheus::HTTP - query Prometheus instances

=head1 VERSION

Version 0.01

=head1 DESCRIPTION

NGCP::Prometheus::HTTP makes it possible to query the Prometheus HTTP API.
The module tries to provide one method per Prometheus HTTP API endpoint,
as of Prometheus 2.7.

    use NGCP::Prometheus::HTTP;

    my $prom = NGCP::Prometheus::HTTP->new();

    my $health = $prom->healthy();
    print "$health->{retval}\n";

    my $query = $prom->query(
        'http_requests_total',
        timeout => '5m',
    );

    print "$query->{json}\n";


=head1 RETURN VALUES AND ERROR HANDLING

The results from methods are returned as a HASHREF. The key C<retval>
stores a boolean denoting whether the sub failed or succeeded. On error
the C<error> key stores the error message.

    my $health = $prom->healthy();
    print $health->{version} unless $health->{retval};

=head1 METHODS

=head2 $prom = NGCP::Prometheus->new(host => 'localhost', port => 9090)

Passing C<host> and/or C<port> is optional, defaulting to the Prometheus
defaults.

Returns an instance of NGCP::Prometheus::HTTP.

=head2 $prom->get_http()

Returns the internally used HTTP::Tiny instance for possible modifications
(e.g. to configure an HTTP proxy).

=head2 $prom->healthy()

Checks the Prometheus instance configured in the constructor (i.e. by C<host>
and C<port>) for health.

Returns a HASHREF containing a C<retval> key which evaluates to true or false
depending on whether the health check was successful or not.

=head2 $prom->ready()

Checks the Prometheus instance configured in the constructor (i.e. by C<host>
and C<port>) for readiness.

Returns a HASHREF containing a C<retval> key which evaluates to true or false
depending on whether the readiness check was successful or not.

=head2 $prom->reload()

Requests the Prometheus instance configured in the constructor (i.e. by C<host>
and C<port>) to reload itself.

Returns a HASHREF containing a C<retval> key which evaluates to true or false
depending on whether the readiness check was successful or not.

=head2 $prom->quit()

Requests the Prometheus instance configured in the constructor (i.e. by C<host>
and C<port>) to quit itself.

Returns a HASHREF containing a C<retval> key which evaluates to true or false
depending on whether the readiness check was successful or not.

=head2 $prom->query($query, %opts)

Evaluates an instant query at a single point in time.

The required $query parameter is a string containing a PromQL query.
The %opts hash accepts C<time> (RFC3339) and C<timeout> (duration) options.

If the returned HASHREF C<retval> key evaluates to true, indicating that the
query was successful, then the returned object's C<data> key contains the
entire response from Prometheus as Perl hash, the C<resultType> key contains
the data type for the result (B<vector>), and C<result> contains the specific
query result. The C<json> key contains the raw JSON response.

=head2 $prom->query_range($query, %opts)

Evaluates an expression query over a range of time.

The required $query parameter is a string containing a PromQL query.
The %opts hash accepts C<start> (RFC3339) and C<end> (RFC3339),
C<step> (duration or float) and C<timeout> (duration) options.

If the returned HASHREF C<retval> key evaluates to true, indicating that the
query was successful, then the returned object's C<data> key contains the
entire response from Prometheus as Perl hash, the C<resultType> key contains
the data type for the result (B<matrix>), and C<result> contains the specific
query result. The C<json> key contains the raw JSON response.

=head2 $prom->series($matches, %opts)

Returns the list of time series that matches a certain label set.

The $matches parameter is an array ref containing series selectors.
The %opts hash accepts C<start> (RFC3339) and C<end> (RFC3339) options.

=head2 $prom->labels()

Returns the list of labels.

=head2 $prom->label($label)

Returns the values for $label.

=head2 $prom->targets()

Returns an overview of the current state of the Prometheus target discovery.

=head2 $prom->rules()

Returns a list of alerting and recording rules that are currently loaded.
In addition it returns the currently active alerts fired by the Prometheus
instance of each alerting rule.

=head2 $prom->alerts()

Returns a list of all active alerts.

=head2 $prom->alertmanagers()

Returns an overview of the current state of the Prometheus alertmanager
discovery.

=head2 $prom->status($item)

Exposes current Prometheus configuration items.
Supported $item are B<config> and B<flags>.

=head1 AUTHOR

Guillem Jover, C<< <gjover@sipwise.com> >>

=head1 BUGS

Please report any bugs to Sipwise GmbH.

=head1 LICENSE

Copyright (c) 2016-2020 Sipwise GmbH, Austria.

GPL-3+, Sipwise GmbH, Austria.

=cut
