package Youri::Repository::Mageia;

=head1 NAME

Youri::Repository::Mageia - Mageia repository implementation

=head1 DESCRIPTION

This module implements Mageia repository

=cut

use warnings;
use strict;
use Carp;
use Memoize;
use File::Find 'find';
use base qw/Youri::Repository/;
use MDV::Distribconf::Build;
use SVN::Client;
use Sys::Hostname;

use constant {
    PACKAGE_CLASS   => 'Youri::Package::RPM::URPM',
    PACKAGE_CHARSET => 'utf8'
};

memoize('_get_media_config');

my %translate_arch = (
    i386 => 'i586',
    sparc64 => 'sparcv9',
);

sub _init {
    my $self   = shift;
    my %options = (
        noarch => 'i586', # noarch packages policy
	src => 'i586',
	install_root => '',
        test          => 0,  # test mode
        verbose       => 0,  # verbose mode
	queue	      => '',
	rejected      => '',
        @_
    );
    foreach my $var ('upload_state') {
	$self->{"_$var"} = [];
	foreach my $value (split ' ', $options{$var}) {
	    push @{$self->{"_$var"}}, $value
	}
    }
    print "Initializing repository\n";
    foreach my $v ('rejected', 'svn', 'queue', 'noarch', 'install_root', 'upload_root', 'verbose') {
        $self->{"_$v"}  = $options{$v}
    }
    foreach my $target (@{$options{targets}}) {
	$self->{$target} = [];
	print "Adding $target ($options{$target}{arch})\n" if $self->{_verbose};
	foreach my $value (split ' ', $options{$target}{arch}) {
	    push @{$self->{_arch}{$target}}, $value;
	    push @{$self->{_extra_arches}}, $value
	}
    }
    $self
}

sub get_group_id {
    my ($user) = @_;
    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time);
    $year+=1900;
    $mon++;
    my ($host) = hostname =~ /([^.]*)/;
    sprintf "$year%02d%02d%02d%02d%02d.$user.$host.${$}_", $mon, $mday, $hour, $min, $sec;
}

sub get_target_arch {
    my ($self, $target) = $_;
    return $self->{_arch}{$target}
}

sub set_arch_changed {
    my ($self, $target, $arch) = @_;
    if ($arch eq 'noarch') {
	    $self->{_arch_changed}{$_} = 1 foreach @{$self->{_arch}{$target}}
    } elsif ($arch eq 'src') {
	    $self->{_arch_changed} = $self->{_src}
    } else {
	$self->{_arch_changed}{$arch} = 1
    }
}

sub get_arch_changed {
    my ($self, $target) = @_;
    return [ keys %{$self->{_arch_changed}} ]
}

sub set_install_dir_changed {
    my ($self, $install_dir) = @_;
    $self->{_install_dir_changed}{$install_dir} = 1;
}

sub get_install_dir_changed {
    my ($self) = @_;
    return [ keys %{$self->{_install_dir_changed}} ];
}

sub _get_media_config {
    my ($self, $target) = @_;
    my %media;
    my $real_target = $target;
    $real_target =~ s/_force//;
    foreach my $arch (@{$self->{_arch}{$target}}) {
	my $root = "$self->{_install_root}/$real_target/$arch";
	my $distrib = MDV::Distribconf::Build->new($root);
	print "Getting media config from $root\n" if $self->{_verbose};
	$self->{distrib}{$arch} = $distrib;
	$distrib->loadtree or die "$root does not seem to be a distribution tree\n";
	$distrib->parse_mediacfg;
	foreach my $media ($distrib->listmedia) {
	    my $rpms = $distrib->getvalue($media, 'rpms');
	    my $debug_for = $distrib->getvalue($media, 'debug_for');
	    my $srpms = $distrib->getvalue($media, 'srpms');
	    my $path = $distrib->getfullpath($media, 'path');
	    if (!$rpms) {
		if (-d $path) {
		    print "MEDIA defining $media in $path\n" if $self->{_verbose} > 1;
		    $media{$arch}{$media} = $path
		} else {
		    print "ERROR $path does not exist for media $media on $arch\n"
		}
	    } else {
		my ($media) = split ' ', $rpms;
		if (-d $path) {
		    print "MEDIA defining SOURCE media for $media in $path\n" if $self->{_verbose} > 1;
		    $media{src}{$media} = $path
		} else {
		    print "ERROR $path does not exist for source media $media on $arch\n"
		}
	    }
	}
    }
    \%media
}

sub get_package_class {
    return PACKAGE_CLASS;
}

sub get_package_charset {
    return PACKAGE_CHARSET;
}

sub get_upload_dir {
    my ($self, $package, $target, $user_context, $app_context) = @_;
    croak "Not a class method" unless ref $self;
    my $arch = $package->get_arch();
    return
        $self->{_upload_root} .
        "/$self->{_queue}/$target/" .
        _get_section($self, $package, $target, $user_context, $app_context) .
	'/' . 
	($user_context->{prefix} ? '' : get_group_id($user_context->{user}))
}

sub get_install_path {
    my ($self, $package, $target, $user_context, $app_context) = @_;

    return $self->_get_path($package, $target, $user_context, $app_context);
}


sub get_distribution_paths {
    my ($self, $package, $target) = @_;

    return $self->_get_distribution_paths($package, $target);
}

=head2 get_distribution_roots()

Returns distribution roots (ie install_root + target + arch)
(it returns a list in case of noarch)

=cut

sub get_distribution_roots {
    my ($self, $package, $target) = @_;
    croak "Not a class method" unless ref $self;

    map { 
	$self->_get_dir($self->{_install_root}, $_);
    } $self->_get_distribution_paths($package, $target);
}

sub get_archive_path {
    my ($self, $package, $target, $user_context, $app_context) = @_;

    return $self->_get_path($package, $target, $user_context, $app_context);
}

sub get_reject_path {
    my ($self, $package, $target, $user_context, $app_context) = @_;

    return $self->{_rejected};
}


sub _get_path {
    my ($self, $package, $target, $user_context, $app_context) = @_;

    my $section = $self->_get_section($package, $target, $user_context, $app_context);
    my $arch = $app_context->{arch} || $package->get_arch();
    $arch = $translate_arch{$arch} || $arch;
    if ($arch eq 'noarch') {
	$arch = $self->{_noarch} 
    } elsif ($arch eq 'src') {
	return "$target/SRPMS/$section"
    }
    "$target/$arch/media/$section"
}

sub _get_distribution_paths {
    my ($self, $package, $target) = @_;

    my $arch = $package->get_arch();
    $arch = $translate_arch{$arch} || $arch;
    if ($arch eq 'noarch') {
	map { "$target/$_" } $self->get_target_arches($target);
    } elsif ($arch eq 'src') {
	die "no way to get distribution path using a $arch package";
    } else {
	"$target/$arch";
    }
}

sub get_arch {
    my ($self, $package, $target, $user_context, $app_context) = @_;
    my $arch = $package->get_arch();
    $arch = $translate_arch{$arch} || $arch;
    if ($arch eq 'noarch') {
	$arch = $self->{_noarch} 
    }
    $arch
}

sub get_version_path {
    my ($self, $package, $target, $user_context, $app_context) = @_;

    my $section = $self->_get_section($package, $target, $user_context, $app_context);

    return "$self->{_module}/$section";
}

=head2 get_replaced_packages($package, $target, $user_context, $app_context)

Overrides parent method to add libified packages.

=cut

sub get_replaced_packages {
    my ($self, $package, $target, $user_context, $app_context) = @_;
    croak "Not a class method" unless ref $self;

    my @replaced_packages = 
        $self->SUPER::get_replaced_packages($package, $target, $user_context, $app_context);

    my $name = $package->get_name();

    # kernel packages have the version in the name
    # binary dkms built for old kernels have to be removed too
    if ($name =~ /^kernel-([^\d]*-)?([\d.]*)-(.*)$/) { # "desktop", "2.6.28", "2mnb"
        push(@replaced_packages,
            map { PACKAGE_CLASS->new(file => $_) }
            $self->get_files(
                $self->{_install_root},
                $self->get_install_path($package, $target, $user_context, $app_context),
                PACKAGE_CLASS->get_pattern(
                    '(kernel-' . $1 . '\d.*|.*-kernel-[\d.]*-' . $1 . '\d.*)',
                    undef,
                    undef,
                    $package->get_arch()
                ),
            )
        );
    }

    return @replaced_packages;
       
}

sub _get_main_section {
    my ($self, $package, $target, $user_context, $app_context) = @_;

    my $section = $self->_get_section($package, $target, $user_context, $app_context);
    my ($main_section) = $section =~ m,^([^/]+),;
    $main_section
}

sub _get_section {
    my ($self, $package, $target, $user_context, $app_context) = @_;

    my $name = $package->get_name();
    my $cname = $package->get_canonical_name();
    my $version = $package->get_version();
    my $release = $package->get_release();
    my $section = $user_context->{section};
    my $media = $self->_get_media_config($target);
    my $arch = $package->get_arch();
    my $file = $package->as_file();
    $file =~ s,/+,/,g; # unneeded?
    # FIXME: use $self->get_arch()
    $arch = $self->{_noarch} if $arch eq 'noarch';
    $arch = $translate_arch{$arch} || $arch;

    if (!$section) {
        $section = $self->{packages}{$file}{section};
        print "Section undefined, repository says it is '$section' for '$file'\n" if $self->{_verbose};
    }
    # FIXME: use debug_for info
    if ($section && $section !~ m|debug/| && $package->is_debug()) {
	$section = "debug/$section"
    }

    # if have section already, check if it exists, and may return immediately
    if ($section) {
	print "Using requested section $section\n" if $self->{_verbose};
	if ($media->{$arch}{$section}) { 
	    return $section
	} else {
	    die "FATAL youri: unknown section $section for target $target for arch $arch\n"
	}
    }
    # else, try to find section automatically

    # pattern for search of src package with specific version-release,
    # should be searched first, because we prefer to find the precise
    # section a package is already in
    my $specific_source_pattern = PACKAGE_CLASS->get_pattern(
        $cname,
        $version,
        $release,
        'src'
    );

    my $source_pattern = PACKAGE_CLASS->get_pattern(
        $cname,
        undef,
        undef,
        'src'
    );

    # if a media has no source media configured, or if it is a debug
    # package, we search in binary media

    # pattern for search when a binary media has no src media configured
    my $specific_binary_pattern = PACKAGE_CLASS->get_pattern(
          $name,
          $version,
          $release,
          $arch
    );

    # last resort pattern: previous existing binary packages
    my $binary_pattern = PACKAGE_CLASS->get_pattern(
          $name,
          undef,
          undef,
          $arch
    );

    # first try to find section for the specific version, as it is possibly already there;
    # this is the case for when called in Youri::Submit::Action::Archive, to find the
    # section the package got installed
    print "Looking for package $name with version $version-$release\n" if $self->{_verbose};
    foreach my $m (keys %{$media->{$arch}}) {
        print " .. section '$m' path '".$media->{$arch}{$m}."'\n" if $self->{_verbose};
        # - prefer source for non-debug packages, use binary if there is no source media configured
        # - debug packages must be searched in binary medias, due to their
        #   src section != binary section; NOTE: should/need we search in
        #   src medias and add the 'debug_' prefix?
        if (!$package->is_debug() && $media->{src}{$m}) {
            next unless $self->get_files('', $media->{src}{$m}, $specific_source_pattern);
        } else {
            next unless $self->get_files('', $media->{$arch}{$m}, $specific_binary_pattern);
        }
        $section = $m;
        last;
    }

    # if still not found, try finding any version of the package in a
    # /release subsection (safe default: /release is default for cooker,
    # should be locked for released distros, and we don't risk wrongly
    # choosing /backports, /testing, or /updates);
    # this is the case for when called at submit, to find the section where
    # the package already resides
    if (!$section) {
        # debug packages should be found by previous specific version search
        # NOTE: as above, should/need we search here and add the 'debug_' prefix?
        # ... probably... as at least mga-youri-submit-force will process debug packages
        if ($package->is_debug() && $self->{_verbose}) {
            print "Warning: debug package $name with version $version-$release not found.\n";
        }

        print "Warning: Looking for any section with a package $name of any version\n";
        foreach my $m (keys %{$media->{$arch}}) {
            print " .. section '$m' path '".$media->{$arch}{$m}."'\n" if $self->{_verbose};
            # NOTE: !$package->is_debug() test is here to prevent when above FATAL error is removed
            next if $m !~ /release/ || ($m =~ /debug/ && !$package->is_debug());
            # - prefer source
            if ($media->{src}{$m}) {
                next unless $self->get_files('', $media->{src}{$m}, $source_pattern);
            } else {
                next unless $self->get_files('', $media->{$arch}{$m}, $binary_pattern);
            }
            $section = $m;
            last;
        }
    }

    # FIXME: doing this here is wrong; this way the caller can never know if
    # a section was actually found or not; should return undef and let the
    # caller set a default (Note: IIRC PLF|Zarb has this right, see there) -spuk
    print STDERR "Warning: Can't guess destination: section missing, defaulting to core/release\n" unless $section;
    $section ||= 'core/release';

    # next time we don't need to search everything again
    $self->{packages}{$file}{section} = $section;

    print "Section is '$section'.\n" if $self->{_verbose};

    return $section;
}

sub get_upload_newer_revisions {
    my ($self, $package, $target, $user_context, $app_context) = @_;
    croak "Not a class method" unless ref $self;
    my $arch = $package->get_arch();
    my $name = $package->as_string();
    $name =~ s/^\@\d+://;
    my $pattern = $self->get_package_class()->get_pattern($package->get_name(), undef, undef, $arch);
    my $media = $self->_get_media_config($target);
    my @packages;
    foreach my $state (@{$self->{_upload_state}}) {
	foreach my $m (keys %{$media->{$arch}}) {
	    next if defined($user_context->{section}) and $user_context->{section} ne $m;
	    my $path = "$self->{_upload_root}/$state/$target/$m";
	    print "Looking for package $package revisions for $target in $path (pattern $pattern)\n" if $self->{_verbose};
	    find(
		sub { 
		    s/\d{14}\.[^.]*\.[^.]*\.\d+_//; 
		    s/^\@\d+://;
		    return if ! /^$pattern/; 
		    return if /\.info$/; 
		    print "Find $_\n" if $self->{_verbose} > 1; 
		    push @packages, $File::Find::name if $package->check_ranges_compatibility("== $name", "< $_")
		}, $path);
	}
    }
    return
        @packages;
}

sub package_in_svn {
    my ($self, $srpm_name) = @_;
    my $ctx = new SVN::Client(
	auth => [SVN::Client::get_simple_provider(),
	SVN::Client::get_simple_prompt_provider(\&simple_prompt,2),
	SVN::Client::get_username_provider()]
    );

    my $svn_entry = $ctx->ls("$self->{_svn}/$srpm_name", 'HEAD', 0);
    if ($svn_entry) {
	print "Package $srpm_name is in the SVN\n" if $self->{_verbose};
	return 1
    }
}

sub get_svn_url {
    my ($self) = @_;
    $self->{_svn}
}

sub reject {
    my ($self, $package, $target, $user_context, $app_context) = @_;
    croak "Not a class method" unless ref $self;


}

sub get_archive_dir {
    my ($self, $package, $target, $user_context, $app_context) = @_;
    croak "Not a class method" unless ref $self;

    return
        $self->{_archive_root}
}


=head1 COPYRIGHT AND LICENSE

Copyright (C) 2002-2006, YOURI project
Copyright (C) 2006,2007,2009  Mandriva
Copyright (C) 2011      Nicolas Vigier, Michael Scherer, Pascal Terjan

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut

1;