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

package OpenQA::Task::Needle::Save;
use Mojo::Base 'Mojolicious::Plugin', -signatures;

use Cwd 'realpath';
use File::Copy;
use Encode 'encode_utf8';
use OpenQA::Git;
use OpenQA::Jobs::Constants;
use OpenQA::Task::SignalGuard;
use OpenQA::Utils;
use Mojo::File 'path';
use Mojo::JSON 'decode_json';
use Feature::Compat::Try;
use Time::Seconds 'ONE_HOUR';

sub register ($self, $app, $conf = undef) {
    $app->minion->add_task(save_needle => sub { _save_needle($app, @_) });
}

sub _json_validation ($json) {
    my $djson;
    try { $djson = decode_json($json) }
    catch ($e) {
        $e =~ s@at /usr/.*$@@;    # do not print perl module reference
        die "syntax error: $e";
    }
    die 'needle JSON is no object' unless ref $djson eq 'HASH';
    die 'no area defined' unless ref $djson->{area} eq 'ARRAY' && exists $djson->{area}[0];
    die 'no tag defined' unless ref $djson->{tags} eq 'ARRAY' && exists $djson->{tags}[0];

    my @not_ocr_area = grep { ($_->{type} // '') ne 'ocr' } @{$djson->{area}};
    die 'Cannot create a needle with only OCR areas' if scalar(@not_ocr_area) == 0;

    foreach my $area (@{$djson->{area}}) {
        die 'area is no object' unless ref $area eq 'HASH';
        die 'area without xpos' unless exists $area->{xpos};
        die 'area without ypos' unless exists $area->{ypos};
        die 'area without type' unless exists $area->{type};
        die 'area without height' unless exists $area->{height};
        die 'area without width' unless exists $area->{width};
    }
    return $djson;
}

sub _format_git_error ($name, $error) {
    return "<strong>Failed to save $name.</strong><br><pre>$error</pre>";
}

sub _save_needle ($app, $minion_job, $args) {
    my $signal_guard = OpenQA::Task::SignalGuard->new($minion_job);

    my $schema = $app->schema;
    my $openqa_job = $schema->resultset('Jobs')->find($args->{job_id});
    my $user = $schema->resultset('Users')->find($args->{user_id});
    my $needle_json = encode_utf8($args->{needle_json});
    my $imagedir = $args->{imagedir};
    my $imagedistri = $args->{imagedistri};
    my $imagename = $args->{imagename};
    my $imageversion = $args->{imageversion};
    my $needledir = realpath($args->{needledir});
    my $needlename = $args->{needlename};
    my $commit_message = $args->{commit_message};

    # read JSON data
    my $json_data;
    try { $json_data = _json_validation($needle_json); }
    catch ($e) {
        $app->log->error("Error validating needle: $e");
        return $minion_job->finish({error => "<strong>Failed to validate $needlename.</strong><br>$e"});
    }

    # determine imagepath
    my $imagepath;
    if ($imagedir) {
        $imagepath = join '/', $imagedir, $imagename;
    }
    elsif ($imagedistri) {
        $imagepath = join '/', needledir($imagedistri, $imageversion), $imagename;
    }
    else {
        $imagepath = join '/', $openqa_job->result_dir(), $imagename;
    }
    if (!-f $imagepath) {
        my $error = "Image $imagename could not be found!";
        $app->log->error("Failed to save needle: $error");
        return $minion_job->fail({error => "<strong>Failed to save $needlename.</strong><br>$error"});
    }

    # check whether needle directory actually exists
    if (!$needledir || !(-d $needledir)) {
        return $minion_job->fail({error => $needledir ? "$needledir is not a directory" : 'no needle directory'});
    }
    return $minion_job->finish({error => 'Another git task is ongoing. Try again later.'})
      unless my $git_clone_guard = $app->minion->guard("git_clone_${needledir}_task", 2 * ONE_HOUR);

    # ensure needle dir is up-to-date
    my $git = OpenQA::Git->new({app => $app, dir => $needledir, user => $user});
    # avoid retrying if cleanup via Git is not configured (then an admin might need to
    # manually cleanup the repo first)
    $signal_guard->abort(1) if !$git->autocommit_enabled || $git->config->{do_cleanup} ne 'yes';
    if ($git->autocommit_enabled) {
        my $error = $git->set_to_latest_master;
        if ($error) {
            $app->log->error($error);
            return $minion_job->fail({error => _format_git_error($needlename, $error)});
        }
    }

    # do not overwrite the exist needle if disallow to overwrite
    my $baseneedle = "$needledir/$needlename";
    if (-e "$baseneedle.png" && !$args->{overwrite}) {
        #my $returned_data = $self->req->params->to_hash;
        #$returned_data->{requires_overwrite} = 1;
        return $minion_job->finish({requires_overwrite => 1});
    }

    # copy image
    my $error;
    if (!($imagepath eq "$baseneedle.png") && !copy($imagepath, "$baseneedle.png")) {
        $app->log->error("Copy $imagepath -> $baseneedle.png failed: $!");
        $error = $!;
    }
    if (!$error) {
        try {
            path("$baseneedle.json")->spew($needle_json);
        }
        catch ($e) {
            $app->log->error("Writing needle $baseneedle.json failed: $e");
            $error = $e;
        }
    }
    return $minion_job->fail({error => "<strong>Error creating/updating needle:</strong><br>$error."}) if $error;

    try {
        _commit_needle_in_git_repo($app, $git, $needlename, $openqa_job, $commit_message);
    }
    catch ($e) {
        $app->log->error("$e");
        return $minion_job->fail({error => _format_git_error($needlename, "$e")});
    }

    # create/update needle in database
    $schema->resultset('Needles')->update_needle_from_editor($needledir, $needlename, $json_data, $openqa_job);

    # finish minion job with successful result
    my $info = {
        success => "Needle $needlename created/updated",
        json_data => $json_data,
    };
    if ($openqa_job->state eq OpenQA::Jobs::Constants::RUNNING && $openqa_job->developer_session) {
        $info->{developer_session_job_id} = $openqa_job->id;
    }
    if ($openqa_job->can_be_duplicated) {
        $info->{propose_restart} = 1;
    }
    return $minion_job->finish($info);
}

sub _commit_needle_in_git_repo ($app, $git, $needlename, $openqa_job, $commit_message) {
    $git->commit(
        {
            add => ["$needlename.json", "$needlename.png"],
            message => ($commit_message || sprintf '%s for %s', $needlename, $openqa_job->name),
        }) if $git->autocommit_enabled;
}

1;
