#!/usr/bin/perl
# Copyright (c) 2013 SUSE LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

=head1 openqa-clone-job

openqa-clone-job - clone job from local or remote openQA instances

=head1 SYNOPSIS

  openqa-clone-job [OPTIONS] JOBREF [KEY=[VALUE] ...]

  openqa-clone-job https://openqa.opensuse.org/t42

  openqa-clone-job --from https://openqa.opensuse.org/tests/42

  openqa-clone-job --from https://openqa.opensuse.org 42

  openqa-clone-job --from https://openqa.opensuse.org --host openqa.example.com 42

  openqa-clone-job --from localhost --host localhost 42 MAKETESTSNAPSHOTS=1 FOOBAR=


=head1 OPTIONS

=over 4

=item B<--host> HOST

connect to specified host

=item B<--from> HOST

get job from specified host

=item B<--dir> DIR

specify directory where test assets are stored (default /var/lib/openqa/factory)

=item B<--skip-deps>

do not clone parent jobs.

=item B<--skip-chained-deps>

do not clone parent jobs of type chained. This makes the job use the downloaded hdd image instead of running the generator job again.

=item B<--skip-download>

do not try any download. You need to ensure all required assets are provided yourself.

=item B<--within-instance> HOST

a shortcut for C<--skip-download --from HOST --host HOST> to clone a job on a remote instance.

=item B<--show-progress>

display a progress bar of downloading asset

=item B<--parental-inheritance>

provide parental job with variables from command line (they go to child job by default).

=item B<--apikey> <value>

specify the public key needed for API authentication

=item B<--apisecret> <value>

specify the secret key needed for API authentication

=item B<--verbose, -v>

increase verbosity

=item B<--help, -h>

print help

=back

=head1 SYNOPSIS

Clone job from another instance. Downloads all assets associated
with the job. Optionally settings can be modified.

openqa-clone-job https://openqa.opensuse.org/t42

openqa-clone-job --from https://openqa.opensuse.org 42

openqa-clone-job --from https://openqa.opensuse.org --host openqa.example.com 42

openqa-clone-job --from localhost --host localhost 42 MAKETESTSNAPSHOTS=1 FOOBAR=

Call with either a full URL pointing to a test job to clone from or one of
both parameters C<--from> or C<--within-instance>. The job ID can be specified
as part of the URL or as its own parameter.

Any parent jobs (chained or parallel) are also cloned unless C<--skip-deps> or
C<--skip-chained-deps> is specified. If C<--skip-chained-deps> is specified
published assets generated by parent jobs are downloaded to be directly used
instead of generated. Keep in mind that any additional parameters are not
added to the also cloned parent jobs.

=cut

use strict;
use warnings;
use Data::Dump 'pp';
use Getopt::Long;
use LWP::UserAgent;
Getopt::Long::Configure("no_ignore_case");
use Mojo::File 'path';
use Mojo::URL;
use Mojo::JSON;    # booleans
use Cpanel::JSON::XS;
use FindBin;
use lib "$FindBin::RealBin/../lib";
use OpenQA::Client;
use OpenQA::Script::CloneJob;

my %options;
my $jobid;
my $ua;
my $local;
my $local_url;
my $remote;
my $remote_url;

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

sub split_jobid {
    my ($url_string) = @_;
    my $url = Mojo::URL->new($url_string);

    # handle scheme being omitted and support specifying only a domain (e.g. 'openqa.opensuse.org')
    $url->scheme('http') unless $url->scheme;
    $url->host($url->path->parts->[0]) unless $url->host;

    my $host_url = Mojo::URL->new->scheme($url->scheme)->host($url->host)->port($url->port)->to_string;
    (my $jobid) = $url->path =~ /([0-9]+)/;
    return ($host_url, $jobid);
}

sub parse_options {
    GetOptions(
        \%options,           "from=s",        "host=s",               "dir=s",
        "apikey:s",          "apisecret:s",   "verbose|v",            "skip-deps",
        "skip-chained-deps", "skip-download", "parental-inheritance", "help|h",
        "show-progress",     "within-instance|w=s",
    ) or usage(1);
    usage(0) if $options{help};

    usage(1) if $options{help} || ($options{'within-instance'} && $options{from});
    if ($options{'within-instance'}) {
        ($options{'within-instance'}, $jobid) = split_jobid($options{'within-instance'});
        $options{'skip-download'} = 1;
        $options{'from'}          = $options{'within-instance'};
        $options{'host'}          = $options{'within-instance'};
    }
    elsif ($options{'from'}) {
        ($options{'from'}, $jobid) = split_jobid($options{'from'});
    }
    $jobid = shift @ARGV unless $jobid;
    die "missing job reference, see --help for usage\n" unless $jobid;
    if (!$options{'from'}) {
        ($options{'from'}, $jobid) = split_jobid($jobid);
    }
    usage(1) unless ($jobid && $options{'from'});
    $options{'dir'}  ||= '/var/lib/openqa/factory';
    $options{'host'} ||= 'localhost';
    return $jobid;
}

sub create_url_handler {
    $ua = LWP::UserAgent->new;
    $ua->timeout(10);
    $ua->env_proxy;
    $ua->show_progress(1) if ($options{'show-progress'});

    if ($options{'host'} !~ '/') {
        $local_url = Mojo::URL->new();
        $local_url->host($options{'host'});
        $local_url->scheme('http');
    }
    else {
        $local_url = Mojo::URL->new($options{'host'});
    }
    $local_url->path('/api/v1/jobs');
    $local = OpenQA::Client->new(
        api       => $local_url->host,
        apikey    => $options{'apikey'},
        apisecret => $options{'apisecret'});

    if ($options{'from'} !~ '/') {
        $remote_url = Mojo::URL->new();
        $remote_url->host($options{'from'});
        $remote_url->scheme('http');
    }
    else {
        $remote_url = Mojo::URL->new($options{'from'});
    }
    $remote_url->path('/api/v1/jobs');
}

sub openqa_baseurl {
    my ($local_url) = @_;
    my $port = '';
    if (
        $local_url->port
        && (   ($local_url->scheme eq 'http' && $local_url->port != 80)
            || ($local_url->scheme eq 'https' && $local_url->port != 443)))
    {
        $port = ':' . $local_url->port;
    }
    return $local_url->scheme . '://' . $local_url->host . $port;
}

sub clone_job {
    my ($jobid, $clone_map, $depth) = @_;
    $clone_map //= {};
    $depth     //= 0;
    return $clone_map->{$jobid} if defined $clone_map->{$jobid};

    my $job = clone_job_get_job($jobid, $remote, $remote_url, \%options);
    if ($job->{parents}) {
        my ($chained, $directly_chained, $parallel);
        unless ($options{'skip-deps'}) {
            unless ($options{'skip-chained-deps'}) {
                $chained          = $job->{parents}->{Chained};
                $directly_chained = $job->{parents}->{'Directly chained'};
            }
            $parallel = $job->{parents}->{Parallel};
        }
        $chained          //= [];
        $directly_chained //= [];
        $parallel         //= [];

        print "Cloning dependencies of $job->{name}\n" if (@$chained || @$directly_chained || @$parallel);
        for my $dependencies ($chained, $directly_chained, $parallel) {
            clone_job($_, $clone_map, $depth + 1) for @$dependencies;
        }

        my @new_chained          = map { $clone_map->{$_} } @$chained;
        my @new_directly_chained = map { $clone_map->{$_} } @$directly_chained;
        my @new_parallel         = map { $clone_map->{$_} } @$parallel;

        $job->{settings}->{_PARALLEL_JOBS}             = join(',', @new_parallel)         if @new_parallel;
        $job->{settings}->{_START_AFTER_JOBS}          = join(',', @new_chained)          if @new_chained;
        $job->{settings}->{_START_DIRECTLY_AFTER_JOBS} = join(',', @new_directly_chained) if @new_directly_chained;
    }

    clone_job_download_assets($jobid, $job, $remote, $remote_url, $ua, \%options)
      unless $options{'skip-download'};

    my $url      = $local_url->clone;
    my %settings = %{$job->{settings}};
    if (my $group_id = $job->{group_id}) {
        $settings{_GROUP_ID} = $group_id;
    }
    clone_job_apply_settings(\@ARGV, $depth, \%settings, \%options);

    print Cpanel::JSON::XS->new->pretty->encode(\%settings) if ($options{verbose});
    $url->query(%settings);
    my $tx = $local->max_redirects(3)->post($url);
    if (!$tx->error) {
        my $r = $tx->res->json->{id};
        if ($r) {
            my $url = openqa_baseurl($local_url) . '/t' . $r;
            print "Created job #$r: $job->{name} -> $url\n";
            $clone_map->{$jobid} = $r;
            return $r;
        }
        else {
            die "job not created. duplicate? ", pp($tx->res->body);
        }
    }
    else {
        die "Failed to create job, empty response. Make sure your HTTP proxy is running, e.g. apache, nginx, etc."
          unless $tx->res->body;
        die "Failed to create job: ", pp($tx->res->body);
    }
}

sub main {
    my ($jobid, $host) = parse_options();
    create_url_handler();
    $remote = OpenQA::Client->new(api => $host);
    return unless $jobid;
    clone_job($jobid);
}

main;

1;
