#!/usr/bin/perl

=head1 DESCRIPTION

Проверяет консистентность релизов: версия соответствует svn и нет хотфиксов, потерянных от более старых релизов

dt-check-release-consistency DIRECT-83559

dt-check-release-consistency DIRECT-83559 DIRECT-83419

=cut

use strict;
use warnings;

use feature qw/state/;

use Getopt::Long;
use XML::LibXML;
use YAML;

use ProjectSpecific qw/svn_url/;

use Startrek::Client::Easy;

use utf8;
use open ':std' => ':utf8';

# Startrek::Client::Easy returns only ids of components
our %COMPONENTS = (
    31053 => 'App: java_api5',
    30820 => 'App: java_intapi',
    31819 => 'App: java_jobs',
    30439 => 'App: java_logviewer',
    34789 => 'App: java_web',
    41082 => 'App: java_user_action_log_writer',
    43064 => 'App: java_binlog_to_yt_sync',
    45572 => 'App: java_binlog_recommendations_tracer',
    46886 => 'App: db_schema',
    53505 => 'App: canvas',
    53425 => 'App: ess_router',
    49723 => 'App: binlogbroker',
    38919 => 'App: dna',
    60208 => 'App: mysql2yt_full',
    72226 => 'App: oneshot',
    110090 => 'App: uac',
    9574  => 'Releases: Direct',
);

our $ARCADIA_URL = 'svn+ssh://arcadia.yandex.ru/arc';
our $apps_conf;
our %component2app;

run() unless caller();

sub run
{

    die "expecting release to check" unless scalar @ARGV > 0;

    $apps_conf = YAML::LoadFile("/etc/yandex-direct/direct-apps.conf.yaml");
    for my $app (keys $apps_conf->{apps}) {
        if (exists $apps_conf->{apps}->{$app}->{'tracker-component'}) {
            $component2app{$apps_conf->{apps}->{$app}->{'tracker-component'}} = $app;
        }
    }

    my $found_errors = 0;
    for my $release (@ARGV){
        print $release."\n";

        my ($rc, $st);
        $rc = issue_info(key => $release, eval_hotfixes => 1);
        die "$release isn't a release" unless $rc->{type} eq 'release';

        if ($rc->{app} eq "dna") {
            print "dna releases are not supported\n";
            next;
        }

        if ($rc->{app} eq "uac") {
            print "uac releases are not supported\n";
            next;
        }

        my $prev_release = find_prev_release($rc);

        $st = issue_info(key => $prev_release, eval_hotfixes => 1);
        die "$prev_release isn't a release" unless $st->{type} eq 'release';

        my $res = release_errors($rc, $st);

        #TODO более человекоориентированный вывод
        print YAML::Dump($res);

        $found_errors = 1 if @{$res->{'hotfixes missed in candidate'}};
    }

    exit(1) if $found_errors;
}

sub find_prev_release
{
    my ($release) = @_;
    my $recent_releases;
   
    my $query = qq(Queue: DIRECT Type: Release Components: "$release->{app_component}" "Sort By": Key desc); 
    $recent_releases = tracker_object()->get(query => $query, limit => 40);
    $recent_releases = [ reverse @$recent_releases ];

    my $prev = $recent_releases->[0]->{key};
    for my $r ( @$recent_releases ){
        if( $r->{key} eq $release->{key}){
            return $prev;
        }
        $prev = $r->{key};
    }

    die "can't find previous release for $release->{key}";
}


sub release_errors
{
    my ($rc, $st) = @_;

    my $rl = $rc->{branches_url};
    my @release_bases = sort { $a <=> $b} 
        map { ($rc->{app_type} eq "svn-perl" && m!^release-(\d+)/?$!) || 
              ($rc->{app_type} eq "arcadia-java" && m!^(\d+)/?$!) ? $1 : die "incorrect release branch name: $_" } 
        split "\n", `svn ls $rl`; 

    my $errors;

    $errors->{candidate} = tracker_cmp_svn($rc, "candidate", \@release_bases );

    $errors->{'hotfixes missed in candidate'} = check_hotfixes($rc, $st);

    return $errors;
}


# Сравнивает состояние тикета и svn для релиза
sub tracker_cmp_svn
{
    my ($r, $name, $branches) = @_;

    my @errors;
    
    # нет бранча, соответствующего версии -- ошибка
    if ($r->{svn_type} eq 'branch' && ! grep { $_ == $r->{base_rev} } @$branches ){
        push @errors, "Tracker: $r->{version}, svn: no $r->{branches_url}";
    }

    # в бранче есть коммиты более новые, чем версия в Джире -- ошибка
    if ( $r->{svn_type} eq 'branch' && grep { $_ == $r->{base_rev} } @$branches ){
        my $xml = `svn info --xml $r->{svn_path}`;
        my $doc = XML::LibXML->new()->parse_string( $xml )->documentElement();
        my $head_rev = ( $doc->findnodes('/info/entry/commit') )[0]->attributes()->getNamedItem('revision')->textContent();
        if ( $head_rev != $r->{last_rev} ) {
            push @errors, "Tracker: $r->{version}, svn: 1.$r->{base_rev}.$head_rev-1";
        }
    }

    return \@errors;
}


# Все хотфиксы из стабильного должны быть и в кандидате тоже
sub check_hotfixes 
{
    my ($rc, $st) = @_;

    my @missed_hotfixes;

    for my $h ( keys %{$st->{hotfixes}} ){
        #$_ <= rc-base || $_ in rc-hotfixes --- ok, else errror
        unless ( $h <= $rc->{base_rev} || $rc->{hotfixes}->{$h} ){
            push @missed_hotfixes, $h;
        }
    }

    return  @missed_hotfixes ? [ join(",", sort {$a <=> $b} @missed_hotfixes) ] : [];
}


sub issue_info
{
    my (%O) = @_;

    die unless $O{key};

    my $issue = tracker_object()->get( key => $O{key} );

    my $info = {
        key      => $issue->{key},
        type     => $issue->{type},
        summary  => $issue->{summary},
        priority => $issue->{priority},
        status   => $issue->{status},
    };

    if ($info->{type} eq 'release') {
        # recognize app by ticket component
        for my $comp (@{$issue->{components}}) {
            if (exists $COMPONENTS{$comp} && exists $component2app{$COMPONENTS{$comp}}) {
                $info->{app} = $component2app{$COMPONENTS{$comp}};
                $info->{app_component} = $COMPONENTS{$comp};
                last;
            }
        }

        die "can't recognize an app for ticket '$issue->{key}'" unless $info->{app};

        $info->{app_type} = $apps_conf->{apps}->{$info->{app}}->{type};

        return $info if $info->{app} eq "dna";

        die "can't get version" unless $issue->{summary} =~ /выложить +(1\.[0-9]+(?:\.[0-9]+)?-[0-9]+)$/;
        my $version = $1;
        die "incorrect base version '$version'" unless $version =~ /^(?:1\.)?(\d+)(?:\.\d+)?(?:-\d)?$/;
        my $base_rev = $1;
        die "incorrect last version '$version'" unless $version =~ /^(?:1\.)?(?:\d+\.)?(\d+)(?:-\d)?$/;
        my $last_rev = $1;

        # branches_url -- каталог, в котором лежат релизные бранчи текущего приложения (напримерб $DRT/releases для perl-Директа) 
        # svn_path -- адрес в svn, из которого была собрана текущая версия (trunk или конкретный релизный бранч)
        my $svn_path;
        if ($info->{app_type} eq "arcadia-java") {
            my $app_full_name = $info->{app};
            if ($base_rev eq $last_rev){
                $svn_path = "$ARCADIA_URL/trunk/arcadia/".$apps_conf->{apps}->{$info->{app}}->{'primary-svn-dir'}
            } else {
                $svn_path = "$ARCADIA_URL/branches/direct/release/$app_full_name/$base_rev/arcadia";
            }
            $info->{branches_url} = "$ARCADIA_URL/branches/direct/release/$app_full_name";
        } else {
            $svn_path = $base_rev eq $last_rev ? svn_url('trunk') : svn_url(release => $base_rev);
            $info->{branches_url} = svn_url('releases');
        }

        $issue->{createdAt} =~ s/^([\d-]*)T.*$/$1/;

        $info->{version} = $version;
        $info->{base_rev} = $base_rev;
        $info->{last_rev} = $last_rev;
        $info->{created} =  $issue->{createdAt};
        $info->{svn_path} = $svn_path;
        $info->{svn_type} = $base_rev eq $last_rev ? 'trunk' : 'branch';
        if ( $base_rev ne $last_rev) {
            my $merged_revisions;
            if ($info->{app_type} eq "arcadia-java") {
                $merged_revisions = qx!svn propget svn:mergeinfo $svn_path |"grep" '^/trunk/arcadia:' --color=never!;
                $merged_revisions =~ s!^/trunk/arcadia:!!;
                # remove unnecessary for checking stars
                $merged_revisions =~ s!\*!!g;
            } else {
                my ($trunk_re) = svn_url('trunk') =~ m!(/trunk.*)!;
                $trunk_re = '^' . quotemeta($trunk_re);
                $merged_revisions = qx!svn propget svn:mergeinfo $svn_path |"grep" '$trunk_re' --color=never!;
                $merged_revisions =~ s!$trunk_re:!!;
            }

            if ($O{eval_hotfixes}) {
                $merged_revisions =~ s/-/../g;
                $info->{hotfixes} = { map { $_ => 1 } eval($merged_revisions) };
                die if $@;
            } else {
                $info->{hotfixes} = $merged_revisions;
            }
        }
    }

    return $info;
}


sub tracker_object
{
    state $tracker;
    $tracker //= Startrek::Client::Easy->new();
    return $tracker;
}

