#!/usr/bin/env perl

# Copyright 2015-2021 SUSE LLC
# SPDX-License-Identifier: GPL-2.0-or-later

=head1 NAME

dump_templates - dump openQA templates

=head1 SYNOPSIS

dump_templates [OPTIONS] [TABLES...]

=head1 OPTIONS

=over 4

=item B<--host> HOST

connect to specified host, defaults to localhost

=item B<--apibase>

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

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

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

override values from config file

=item B<--group> GROUP

dump only specified job group

=item B<--test> NAME

dump only testsuite with specified name

=item B<--machine> NAME

dump only machine with specified name

=item B<--product> NAME

dump only product with specified name

=item B<--full>

when dumping a specific job group also dump related test suites, products and machines

=item B<--json>

dump as json

=item B<--help, -h>

print help

=back

=head1 TABLES

=over 4

=item B<Machines>

=item B<TestSuites>

=item B<Products>

=item B<JobTemplates>

=item B<JobGroups>

=back

=head1 SYNOPSIS

=over 4

=item dump_templates --host openqa.example.com Products

=item dump_templates --host openqa.example.com --group "openSUSE Tumbleweed" --full

=item dump_templates --host openqa.example.com --test "gnome"

=item dump_templates --host openqa.example.com --machine "64bit"

=back

=head1 DESCRIPTION

Dump openQA database tables for Machines TestSuites Products
JobTemplates e.g. to load them on another instance.

=cut

use FindBin;
use lib "$FindBin::RealBin/../lib";

use Mojo::Base -strict, -signatures;

use Data::Dump 'dd';
use Getopt::Long;
use Mojo::Util qw(decamelize);
use Mojo::URL;
use OpenQA::Client;
use OpenQA::YAML 'load_yaml';

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

my %options;

sub usage ($r) {
    eval "use Pod::Usage; pod2usage($r);";
    die "cannot display help, install perl(Pod::Usage)\n" if $@;
    exit $_[0];
}

GetOptions(
    \%options, "json", "host=s", "apibase=s", "apikey:s", "apisecret:s",
    "group=s@", "name=s@", "test=s@", "product=s@", "machine=s@", "full",
    "help|h",
) or usage(1);

usage(0) if $options{help};

sub tables (@args) {
    my %tables = map { $_ => 1 } qw(Machines TestSuites Products JobTemplates JobGroups);
    return %tables unless @args;
    my %want = map { $_ => 1 } @args;
    # Show an error and refer to usage if a non-existing table name is passed
    for my $t (keys %want) {
        if (!exists $tables{$t}) {
            printf STDERR "Invalid table name $t\n\n";
            usage(1);
        }
    }
    return map { $_ => !!$want{$_} } (keys %tables);
}

my %tables = tables(@ARGV);

if ($options{group}) {
    $options{group} = {map { $_ => 1 } @{$options{group}}};
    $tables{JobTemplates} = 1;
    $tables{JobGroups} = 1;
}
if ($options{test}) {
    $options{test} = {map { $_ => 1 } @{$options{test}}};
    $tables{TestSuites} = 1;
}
if ($options{machine}) {
    $options{machine} = {map { $_ => 1 } @{$options{machine}}};
    $tables{Machines} = 1;
}
if ($options{product}) {
    $options{product} = {map { $_ => 1 } @{$options{product}}};
    $tables{Products} = 1;
}

$options{host} ||= 'localhost';
$options{apibase} ||= '/api/v1';
my $url = OpenQA::Client::url_from_host($options{host});
my $client = OpenQA::Client->new(apikey => $options{apikey}, apisecret => $options{apisecret}, api => $url->host);
my %result;

sub handle_error ($target, $url, $res) {
    my $code = $res->code // 'unknown error code - host ' . $url->host . ' unreachable?';
    my $message = $res->message // 'no error message';
    printf STDERR "ERROR requesting %s via %s: %s - %s\n", $target, $url, $code, $message;
    dd($res->json || $res->body) if $res->body;
    exit(1);
}

sub delete_all_settings ($settings) {
    delete $settings->[$_]->{id} for (0 .. $#{$settings});
}

sub product_key ($r) {
    join('-', map { $r->{$_} } qw(distri version flavor arch));
}

if ($tables{'JobGroups'}) {
    my $group = (keys %{$options{group}})[0];
    $url->path($options{apibase} . '/job_templates_scheduling/' . ($group // ''));
    my $res = $client->get($url)->res;
    handle_error($group // 'all groups', $url, $res) unless $res->code && $res->code == 200;
    if ($group) {
        # This is already the YAML document of a single group
        push @{$result{JobGroups}}, {group_name => $group, template => $res->json};
    }
    else {
        my $yaml = $res->json;
        foreach my $group (sort keys %$yaml) {
            push @{$result{JobGroups}}, {group_name => $group, template => $yaml->{$group}};
        }
    }
}

for my $table (qw(Machines TestSuites Products JobTemplates)) {
    next unless $tables{$table};
    $url->path($options{apibase} . '/' . decamelize($table));
    my $res = $client->get($url)->res;
    handle_error($table, $url, $res) unless $res->code && $res->code == 200;
    my %tmp = (%result, %{$res->json});
    %result = %tmp;
}

# special trick to dump all TestSuites used by specific JobTemplates
if ($tables{JobTemplates} && $options{full}) {
    for my $r (@{$result{JobTemplates}}) {
        next if $options{group} && !$options{group}->{$r->{group_name}};
        $options{test}->{$r->{test_suite}->{name}} = 1;
        $options{machine}->{$r->{machine}->{name}} = 1;
        $options{product}->{product_key($r->{product})} = 1;
    }
}

sub handle_row ($table, $r) {
    if ($table eq 'JobTemplates') {
        return undef if $options{group} && $r->{group_name} && !$options{group}->{$r->{group_name}};
        return undef unless $options{product}->{product_key($r->{product})};
    }
    return undef if $table eq 'TestSuites' && $options{test} && $r->{name} && !$options{test}->{$r->{name}};
    return undef if $table eq 'Machines' && $options{machine} && $r->{name} && !$options{machine}->{$r->{name}};
    return undef if $table eq 'Products' && $options{product} && !$options{product}->{product_key($r)};

    delete $r->{id};
    delete_all_settings($r->{settings}) if $r->{settings};
    delete $r->{product}->{id} if $r->{product};
    delete $r->{machine}->{id} if $r->{machine};
    delete $r->{test_suite}->{id} if $r->{test_suite};
    return $r;
}

sub handle_table ($table) {
    $result{$table} = [grep { defined } map { handle_row($table, $_) } @{$result{$table}}];
}

handle_table($_) for keys %result;

if ($options{json}) {
    use Mojo::JSON;    # booleans
    use Cpanel::JSON::XS;
    print Cpanel::JSON::XS->new->ascii->pretty->encode(\%result);
}
else {
    dd \%result;
}
