#!/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/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 tests=blah

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 JSON;
use Data::Dump;
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";
    }
}

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';

my $path = shift @ARGV;
# Relative paths are routed to v1
if ($path !~ m/^\//) {
    $path = join('/', $options{apibase}, $path);
}

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

if ($options{params}) {
    local $/;
    open(my $fh, '<', $options{params});
    my $info = JSON->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);

my $res;
if ($options{form}) {
    $res = $client->$method($url, form => \%params)->res;
}
elsif ($options{'json-data'}) {
    $res = $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}) {
        $options{path}    = $path;
        $options{url}     = $url;
        $options{params}  = \%params;
        $options{params2} = @ARGV;
        eval { $res = $client->archive->run(\%options) };
        die "ERROR: $@ \n", $@ if $@;
        exit(0);
    }
    else {
        $res = $client->$method($url)->res;
        #exit(0);
    }
}
if ($res->code == 200) {
    if ($options{'json-output'}) {
        print JSON->new->pretty->encode($res->json);
    }
    else {
        dd($res->json || $res->body);
    }
}
else {
    printf STDERR "ERROR: %s - %s\n", $res->code, $res->{error}->{message};
    if ($res->body) {
        if ($options{json}) {
            print JSON->new->pretty->encode($res->json);
        }
        else {
            dd($res->json || $res->body);
        }
    }
    exit(1);
}

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