#!/usr/bin/perl
use strict;
use warnings;
use Carp;
use Config::Tiny;
use Getopt::Long;
use Fedora::Rebuild::Set::Package;
use Fedora::Rebuild::Package;
use Fedora::Rebuild::Scheduler;
use Fedora::Rebuild::Package::ListLog;
use Fcntl;
use File::Spec;
use File::Temp;

=encoding utf8

=head1 NAME

rebuildreset - Reset package for rebuild

=head1 SYNOPIS

    rebuilreset [--config FILE] [--changed] < PACKAGE_LIST

=head1 DESCRIPTION

This tool resets packages listed on standard input or in a file named in
positional arguments, so that next L<rebuildperl(1)> will consider the
packages as not yet rebuilt.

This is a helper for L<rebuildperl(1)> and it assumes the packages have
already been populated by rebuildperl. It will reset the local repository and
removes all stage locks except I<.clone>. It will also edit I<all>, I<done>,
and I<failed> package lists (see the configuration) accordingly. (It will
remove changed packaged from done and failed list.)

With other options, the set of packages to reset can be reduced more.

=cut

my $cfgfile = File::Spec->catfile($ENV{HOME}, '.rebuildperlrc');
my %config = (
    done => undef,
    failed => undef,
    failedtemp => undef,
    workdir => undef,
    repodir => undef,
    dist => undef,
    loadthreads => 1,
    target => '',
    message => '',
);
my $changed_only = 0;

=head1 OPTIONS

=head2 --config I<FILE>

Read configuration from I<FILE>, or F<~/.rebuildperlrc> if not specified.

=head2 --changed

Reset only packages whose remote repository has changed (e.g. there is a newer
version). It compares local copy of I<origin> GIT repository with repository
on the remote server and if it finds the HEADs are different it will reset the
package. Otherwise the package will not be reset.

If I<failedtemp> configuration option is defined and there is an error while
checking the remote repository, the faulting package will be logged into
the I<failedtemp> file. This is because talking to a GIT server can result
into intermittent network error that can disappear on next retry.

=head1 FILES

=head2 F<~/.rebuildperlrc>

Configuration is in L<Config::Tiny> format. Following options are needed:

    done = done
    failed = failed
    failedtemp = failedtemp
    workdir = workdir
    dist = rawhide
    loadthreads = 4

=cut

# Copy file permissions and ownership from source file handle to target file
# handle. Croaks on error.
sub copy_file_attributes {
    my ($source_handle, $source_name, $target_handle, $target_name) = @_;
    my @source_meta = stat($source_handle) or
        croak "Could not retrieve `$source_name' meta-data: $!";
    if (defined $source_meta[2]) {
        chmod($source_meta[2], $target_handle) or
            croak "Could not change `$target_name' permissions: $!";
    }
    if (defined $source_meta[4] and defined $source_meta[5]) {
        chown($source_meta[4], $source_meta[5], $target_name) or
            croak "Could not change `$target_name' ownership: $!";
    }
}

# Remove listed packages from file.
sub edit_file {
    my ($file_name, $packages) = @_;
    my $file;
    print "Strippig packages from file `$file_name'...\n";

    # Open input file.
    # Write mode for exclusive lock
    open($file, '+<', $file_name) or
        croak "Could not open `$file_name': $!";
    flock($file, Fcntl::LOCK_EX) or
        croak "Could not lock `$file_name' file: $!";

    # Create output file
    my ($new_file, $new_file_name) =
        File::Temp::tempfile($file_name . 'XXXXXX', UNLINK => 0, EXLOCK => 1);
    copy_file_attributes($file, $file_name, $new_file, $new_file_name);

    # Filter input file into output file
    while (local $_ = <$file>) {
        chomp;
        if (m/^\s*$/) { next; }
        if ($packages->contains($_)) { next; }
        print $new_file "$_\n" or
            croak "Could not write into temporary file `$new_file_name' :$!";
    }
    if ($!) {
        croak "Could not read list of package names fromfile `$file_name' :$!";
    }
    
    # Finish writing and replace the the files
    $new_file->flush && $new_file->sync or
        croak "Could not flush temporary file `$new_file_name': $!";
    rename $new_file_name, $file_name or
        croak "Could not replace file `$file_name' with stripped " .
            "`$new_file_name': $!";
    close $new_file or
        croak "Could not close `$file_name': $!";

    # This unlocks input file.
    close $file;

    print "Strippig packages from file `$file_name' finished successfully.\n";
}


GetOptions(
    'config=s' => \$cfgfile,
    'changed' => \$changed_only
) or die "Could not parse program options\n"; 

# Load configuration
if (-f $cfgfile) {
    my $cfg = Config::Tiny->new->read($cfgfile);
    if (! defined $cfg) {
        print STDERR "Could not parse `" . $cfgfile .
            "' configuration file: " . $Config::Tiny::errstr . "\n";
        exit 1;
    }
    foreach (keys %{$cfg->{_}}) {
        $config{$_} = $cfg->{_}->{$_};
        $config{$_} = eval $config{$_} if $_ eq 'buildrequiresfilter';
    }
}

# Load list of packages to synchronize
my $packages = Fedora::Rebuild::Set::Package->new();
print "Loading list of package names...\n";
while (local $_ = <>) {
    chomp;
    if (m/^\s*$/) { next; }
    if ($packages->contains($_)) { next; }
    my $package = Fedora::Rebuild::Package->new(name => $_,
        workdir => $config{workdir}, dist => $config{dist},
        pkgdist => $config{pkgdist}, target => $config{target},
        message => $config{message});
    $packages->insert($package);
}
if ($!) {
    croak "Could not read list of package names: $!"; 
}
print "Number of packages: " . $packages->size() . "\n";


my $changed_packages = Fedora::Rebuild::Set::Package->new();
my $temporary_error = 0;

# Process packages
my $scheduler = Fedora::Rebuild::Scheduler->new(
    limit => $config{loadthreads},
    name => ($changed_only) ?
        'Checking remote repository' : 'Reseting repository',
    total => $packages->size
);
my %jobs = ();
my $i = 0;

for my $package ($packages->packages) {
    my $job = $scheduler->schedule($package->can(
            ($changed_only) ? 'reset_remotly_updated' : 'reset'
        ), $package);
    if (! defined $job) {
        next;
    }
    $jobs{$job} = $package;
    my %finished = $scheduler->finish(++$i < $packages->size);

    while (my ($job, $status) = each %finished) {
        my $package = $jobs{$job};

        if ($changed_only) {
            if (!$$status[0]) {
                print "Could not check remote repository for package `",
                     $package->name, "': $$status[1].\n";
                $temporary_error++;
                if (defined $config{failedtemp}) {
                    Fedora::Rebuild::Package::ListLog->new(
                        file => $config{failedtemp})->log($package);
                } else {
                    print "Waiting for finishing scheduled jobs...\n";
                    $scheduler->finish(1);
                    print "All jobs have finished.\n";
                    croak "Could not check all packages for remote changes.\n";
                }
            } elsif (2 == $$status[0]) {
                print "`", $package->name,
                    "' has been remotely changed and locally reset.\n";
                $changed_packages->insert($package);
            }
        } else {
            if (!$$status[0]) {
                print "Could not reset package `", $package->name,
                    "': $$status[1].\n";
                print "Waiting for finishing scheduled jobs...\n";
                $scheduler->finish(1);
                print "All jobs have finished.\n";
                croak "Could not reset all specified packages.\n";
            }
            $changed_packages->insert($package);
        }
    }
}

if ($changed_packages->size > 0) {
    print "Following packages have been locally reset (",
        $changed_packages->size, "): ", $changed_packages->string, "\n";
    edit_file($config{done}, $changed_packages);  
    edit_file($config{failed}, $changed_packages);  
} else {
    print "None package has been reset.\n";
}

if ($temporary_error) {
    exit 1;
}
exit 0;

