#!/usr/bin/perl

# Copyright (C) 2014-2018 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.

=head1 SYNOPSIS

client [OPTIONS] PATH

=head1 OPTIONS

=over 4

=item B<--host> HOST

connect to specified host, defaults to localhost

=item B<--params> FILE

load get/post parameters from a json file. For example

{
   "FLAVOR" : "DVD",
   "BUILD" : "42",
   "ARCH" : "i586",
   "DISTRI" : "opensuse",
   "VERSION" : "26",
}

=item B<--apibase>

Set API base URL component, default: '/api/v1'

=item B<--json-output>

Output JSON instead of perl structures.

=item B<--verbose, -v>

Be verbose in output.

=item B<--apikey> KEY, B<--apisecret> SECRET

Specify api key and secret to use, overrides use of config file ~/.config/openqa/client.conf

=item B<--json-data>

Send JSON data (which is expected by some routes)

For example:
jobs/639172 put --json-data '{"group_id": 1}'

=item B<--help, -h>

print help

=head2 Archive mode

=item B<--archive, -a> DIRECTORY

Archive mode: Download assets and test results from a job to DIRECTORY.

=item B<--with-thumbnails>

Archive mode: Include thumbnails

=item B<--asset-size-limit> LIMIT

Archive mode: Download assets that do not exceed the specified limit in bytes
The default limit is 200 MB.

=head1 SYNOPSIS

Interact with the openQA API by specified route entry points and optionally
operations, defaults to the 'get' operation, i.e. just reading out the data
without changing it. See the help on the openQA instance you want to access
for available API routes.

Common top level entry points: jobs, workers, isos.

=item client --host openqa.example.com jobs

List all jobs. Caution: this will take a very long time or even timeout on big
productive instances.

=item client --host openqa.example.com jobs groupid=135 distri=caasp version=3.0 latest=1

List all jobs matching the specified search criteria.

=item client --host openqa.example.com jobs/overview groupid=135 distri=caasp version=3.0

List the latest jobs for the latest build in the given scenario.
In contrast to the route above, this will limit the results to the latest build in the same
way the test result overview in the web UI does.

=item client --host openqa.example.com jobs/1

Show details of job nr. B<1>.

=item client --host openqa.example.com jobs/1 delete

Delete job nr. B<1> (permissions read from config file).

=item client --host openqa.example.com isos post ISO=bar.iso DISTRI=my-distri FLAVOR=my-flavor ARCH=my-arch VERSION=42 BUILD=1234

Trigger jobs on iso B<bar.iso> matching test suite B<blah>.

=item client --archive /path/to/directory --asset-size-limit 1048576000 --with-thumbnails --host openqa.opensuse.org jobs/42

Download all assets and test logs and images from job B<42> with asset limit of B<1GB> to B</path/to/directory>.

=cut

# Intentionally commented out - so far useful only for openQA debugging
# =item B<--form>
#
# Use HTTP forms instead of appending supplied parameters as URL query
#
# To create nested forms use dotted syntax. For example:
#
# client jobs/1/artefact post file.file=bar file.filename=bar.log

use strict;
use warnings;
use FindBin;
BEGIN { unshift @INC, "$FindBin::RealBin/../lib" }

use Mojo::JSON;    # booleans
use Cpanel::JSON::XS ();
use Data::Dump 'dd';
use Mojo::URL;
use OpenQA::Client;
use Getopt::Long;
use OpenQA::Client::Archive;

Getopt::Long::Configure("no_ignore_case");

my %options;

sub usage($) {
    my $r = shift;
    eval { use Pod::Usage; pod2usage(1); };
    if ($@) {
        die "cannot display help, install perl(Pod::Usage)\n";
    }
}

sub handle_result {
    my $res = shift;

    if ($res->code == 200) {
        my $json = $res->json;
        if ($options{'json-output'}) {
            print Cpanel::JSON::XS->new->pretty->encode($json);
        }
        else {
            dd($json || $res->body);
        }
        return $json;
    }

    printf(STDERR "ERROR: %s - %s\n", $res->code, $res->{error}->{message});
    if ($res->body) {
        if ($options{json}) {
            print Cpanel::JSON::XS->new->pretty->encode($res->json);
        }
        else {
            dd($res->json || $res->body);
        }
    }
    exit(1);
}

# prepend the API-base if the specified path is relative
sub prepend_api_base {
    my $path = shift;

    if ($path !~ m/^\//) {
        $path = join('/', $options{apibase}, $path);
    }
    return $path;
}

GetOptions(
    \%options,            'host=s',      'apibase=s',   'json-output',
    'verbose|v',          'apikey:s',    'apisecret:s', 'params=s',
    'form',               'json-data:s', 'help|h|?',    'archive|a:s',
    'asset-size-limit:i', 'with-thumbnails'
) or usage(1);

usage(1) unless @ARGV;

if ($options{form} && $options{'json-data'}) {
    print STDERR "ERROR: The options --form and --json-data can not be combined.\n";
    exit(2);
}

$options{host}    ||= 'localhost';
$options{apibase} ||= '/api/v1';

# determine operation and path
my $operation = shift @ARGV;
my $path      = prepend_api_base($operation);

my $method = 'get';
my %params;

if ($options{params}) {
    local $/;
    open(my $fh, '<', $options{params});
    my $info = Cpanel::JSON::XS->new->relaxed->decode(<$fh>);
    close $fh;
    %params = %{$info};
}

for my $arg (@ARGV) {
    if ($arg =~ /^(?:get|post|delete|put)$/i) {
        $method = lc $arg;
    }
    elsif ($arg =~ /^([[:alnum:]_\[\]\.]+)=(.+)/) {
        $params{$1} = $2;
    }
}

my $url;

if ($options{host} !~ '/') {
    $url = Mojo::URL->new();
    $url->host($options{host});
    $url->scheme('http');
}
else {
    $url = Mojo::URL->new($options{host});
}

$url->path($path);

if (!$options{form}) {
    $url->query([%params]) if %params;
}
else {
    my %form;
    for (keys %params) {
        if (/(\S+)\.(\S+)/) {
            $form{$1}{$2} = $params{$_};
        }
        else {
            $form{$_} = $params{$_};
        }
    }
    %params = %form;
}

my $client = OpenQA::Client->new(apikey => $options{apikey}, apisecret => $options{apisecret}, api => $url->host);

if ($options{form}) {
    handle_result($client->$method($url, form => \%params)->res);
}
elsif ($options{'json-data'}) {
    handle_result($client->$method($url, {'Content-Type' => 'application/json'} => $options{'json-data'})->res);
}
else {
    # Either the user wants to call a command or wants to interact with
    # the rest api directly.
    if ($options{archive}) {
        my $res;
        $options{path}    = $path;
        $options{url}     = $url;
        $options{params}  = \%params;
        $options{params2} = @ARGV;
        eval { $res = $client->archive->run(\%options) };
        die "ERROR: $@ \n", $@ if $@;
        exit(0);
    }
    elsif ($operation eq 'jobs/overview/restart') {
        $url->path(prepend_api_base('jobs/overview'));
        my $relevant_jobs = handle_result($client->get($url)->res);
        my @job_ids       = map { $_->{id} } @$relevant_jobs;
        $url->path(prepend_api_base('jobs/restart'));
        $url->query(Mojo::Parameters->new);
        $url->query(jobs => \@job_ids);
        print("$url\n");
        handle_result($client->post($url)->res);
    }
    else {
        handle_result($client->$method($url)->res);
    }
}

1;
# vim: set sw=4 sts=4 et:
