The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package App::DuckPAN::Perl;
BEGIN {
  $App::DuckPAN::Perl::AUTHORITY = 'cpan:DDG';
}
# ABSTRACT: Perl related functionality for duckpan
$App::DuckPAN::Perl::VERSION = '0.176';
use Moo;
with 'App::DuckPAN::HasApp';

use Config::INI;
use Dist::Zilla::Util;
use Path::Tiny;
use Config::INI::Reader;
use Config::INI::Writer;
use Data::Dumper;
use LWP::UserAgent;
use List::MoreUtils qw/ uniq /;
use List::Util qw/ first /;
use File::Temp qw/ :POSIX /;
use version;
use Parse::CPAN::Packages::Fast;
use Class::Load ':all';

sub dzil_root { Dist::Zilla::Util->_global_config_root }
sub dzil_config { path(shift->dzil_root,'config.ini') }

sub setup {
	my ( $self, %params ) = @_;
	my $config_root = Dist::Zilla::Util->_global_config_root;
	my $config = $self->get_dzil_config;
	$config = {} unless $config;
	$config->{'%Rights'} = {
		license_class => 'Perl_5',
		copyright_holder => $params{name},
	} unless defined $config->{'%Rights'};
	$config->{'%User'} = {
		email => $params{email},
		name => $params{name},
	} unless defined $config->{'%User'};
	$config->{'%DUKGO'} = {
		username => $params{user},
		password => $params{pass},
	};
	$self->set_dzil_config($config);
}

sub get_local_version {
	my ($self, $module) = @_;
	require Module::Data;
	my $v;
	{
		local $@;

        # ensure $module is installed by trying to load (require) it
		eval {
			my $m = Module::Data->new($module);
			$m->require;
			$v = $m->version;
			1;
		} or return;
	};

    # $module (e.g. DuckPAN, DDG) has loaded, but no $VERSION exists
    # This means we're not working with code that was built by DZIL
    #
    # Example:
    # > ./bin/duckpan -I/lib/ -I../duckduckgo/lib server
	unless (defined $v) {
        if ($module eq 'App::DuckPAN' || $module eq 'DDG'){
            # When executing code in-place, $VERSION will not be defined.
            # Only the installed package will have a defined version
            # thanks to Dist::Zilla::Plugin::PkgVersion
            return '9.999';
        }
        return;
    }
	return version->parse($v) unless ref $v;
	return $v;
}

sub cpanminus_install_error {
	shift->app->emit_and_exit(1,
		"Failure on installation of modules!",
        "There are several possible explanations and fixes for this error:",
        "1. The download from CPAN was unsuccessful - Please restart this installer.",
        "2. Some other error occured - Please read the `build.log` mentioned in the errors and see if you can fix the problem yourself.",
        "If you are unable to solve the problem, please let us know by making a GitHub Issue in the DuckPAN Repo:",
        "https://github.com/duckduckgo/p5-app-duckpan/issues",
        "Make sure to attach the `build.log` file if it exists. Otherwise, copy/paste the output you see."
	);
}

sub duckpan_install {
	my ($self, @modules) = @_;
	my $mirror = $self->app->duckpan;
	my $reinstall;
	if ($modules[0] eq 'reinstall') {
		# We sent in a signal to force reinstallation
		$reinstall = 1;
		shift @modules;
	}
	my $packages = $self->app->duckpan_packages;
	my @to_install;
	for (@modules) {
		my $module = $packages->package($_);
		$self->app->emit_and_exit(1, "Can't find package " . $_ . " on " . $self->app->duckpan) unless $module;

		my $package = $module->package;    # Probably $_, but maybe they'll normalize or something someday.

		# see if we have an env variable for this module
		my $sp = $package;
		$sp =~ s/\:\:/_/g;

		# special case: check for a pinned verison number
		my $pin_version            = $ENV{$sp};
		my $localver               = $self->get_local_version($package);
		my $duckpan_module_version = version->parse($module->version);
		my $duckpan_module_url     = $self->app->duckpan . 'authors/id/' . $module->distribution->pathname;

		$localver ||= 1e-6 if ($pin_version); # a silly, but true, value if missing and we need to compare with pinned.

		my ($install_it, $message);
		if ($reinstall || !$localver) {    # Note the ignored pinning.
			$message = $reinstall ?
                        "Reinstalling $package. Latest version ($duckpan_module_version)" :
                        "You don't have $package installed. Installing latest version ($duckpan_module_version)";
			$install_it = 1;
		} elsif ($pin_version) {
			$self->app->emit_info("$package: $localver installed, $pin_version pin, $duckpan_module_version latest");
			if ($pin_version != $localver) {
				#  We continue here, even if the version is larger than latest released,
				#  on the premise that there might exist unreleased development versions.
				if ($pin_version == $duckpan_module_version || ($duckpan_module_url = $self->find_previous_url($module, $pin_version))) {
					$reinstall  = 1;       # Let us roll back, if necessary. Multiple packages may confuse this, but little harm.
					$install_it = 1;
				} else {
					$message    = 'Could not locate version ' . $pin_version . ' of  ' . $package;
					$install_it = 0;
				}
			}
		} elsif ($localver == $duckpan_module_version) {
			$message = "You already have latest version ($localver) of $package";
		} elsif ($localver > $duckpan_module_version) {
			$message = "You have a newer version ($localver) of $package than duckpan.org ($duckpan_module_version)";
		} else {
			$message = "You have an older version ($localver) of $package than duckpan.org. Installing latest version ($duckpan_module_version)";
			$install_it = 1;
		}
		$self->app->emit_info($message);
		push @to_install, $duckpan_module_url if ($install_it && !(first { $_ eq $duckpan_module_url } @to_install));
	}

	return 0 unless @to_install;
	unshift @to_install, '--reinstall' if ($reinstall);    # cpanm will do the actual forcing.
	return system("cpanm " . join(" ", @to_install));
}

sub find_previous_url {
	my ($self, $module, $desired_version) = @_;

	# Shaky premise #1: the author of our previous version is a current author.
	# Shaky premise #2: the directory structure is always like this.
	my @cpan_dirs = map { join('/', substr($_, 0, 1), substr($_, 0, 2), $_) } uniq map { $_->cpanid } ($self->app->duckpan_packages->distributions);
	# Shaky premise #3: things never change distributions.
	my $dist     = $module->distribution;
	my $filename = $dist->filename;
	# Shaky premise #4: the distribution version will match package version.
	my $version = $dist->version;
	# Shaky premise #5: the version for which they are asking is well-formed.
	$filename =~ s/$version/$desired_version/;
	my @urls = map { $self->app->duckpan . 'authors/id/' . $_ . '/' . $filename } @cpan_dirs;
	$self->app->emit_debug("Checking up to " . scalar @urls . " distributions for pinned version...");

	# Shaky premise #6: our network works well enough to make this a definitive test
	my $ua = LWP::UserAgent->new(
		agent                 => 'DPPF/0.001a',
		requests_redirectable => []);

	return first { $ua->head($_)->is_success } @urls;
}

sub set_dzil_config {
	my ( $self, $config ) = @_;
	$self->dzil_root->mkpath unless -d $self->dzil_root;
	Config::INI::Writer->write_file($config,$self->dzil_config);
}

sub get_dzil_config {
	my ( $self ) = @_;
	return unless -d $self->dzil_root && -f $self->dzil_config;
	Config::INI::Reader->read_file($self->dzil_config);
}

1;

__END__

=pod

=head1 NAME

App::DuckPAN::Perl - Perl related functionality for duckpan

=head1 VERSION

version 0.176

=head1 AUTHOR

Torsten Raudssus <torsten@raudss.us> L<https://raudss.us/>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2013 by DuckDuckGo, Inc. L<https://duckduckgo.com/>.

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

=cut