#!/usr/bin/env perl
#***************************************************************************
#*   Copyright (C) 2008-2009 by Eugene V. Lyubimkin                        *
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the GNU General Public License                  *
#*   (version 3 or above) as published by the Free Software Foundation.    *
#*                                                                         *
#*   This program is distributed in the hope that it will be useful,       *
#*   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
#*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
#*   GNU General Public License for more details.                          *
#*                                                                         *
#*   You should have received a copy of the GNU GPL                        *
#*   along with this program; if not, write to the                         *
#*   Free Software Foundation, Inc.,                                       *
#*   51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA               *
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the Artistic License, which comes with Perl     *
#***************************************************************************

package main;

use 5.10.0;
use warnings;
use strict;

BEGIN {
	unshift @INC, '.';
}

INIT {
	## no critic (RequireLocalizedPunctuationVars)
	require Carp;
	$SIG{__WARN__} = \&Carp::confess;
	$SIG{__DIE__} = \&Carp::confess;
}

use List::Util qw(sum reduce);
use List::MoreUtils qw(uniq none);
use Getopt::Long;
Getopt::Long::Configure('pass_through', 'no_auto_abbrev', 'no_ignore_case');
use File::Temp qw(tempfile);
use File::stat;

use Cupt::Core;
use Cupt::Config;
use Cupt::Cache;
use Cupt::Cache::Relation qw(stringify_relation_expressions stringify_relation_expression
		parse_relation_line);
use Cupt::Cache::ArchitecturedRelation qw(unarchitecture_relation_expressions
		parse_architectured_relation_line);

our $config;
our $cache;
my $download_progress_class_name = 'Cupt::Download::Progresses::Console';

sub build_config {
	eval {
		$config = Cupt::Config->new();
	};
	if (mycatch()) {
		myerr('error while loading config');
		exit 1;
	}
}

sub new_download_progress {
	eval "require $download_progress_class_name"; die $@ if $@; ## no critic (StringyEval)
	return $download_progress_class_name->new();
}

my @original_arguments = @ARGV;

# build config at start
build_config();

process_options();

my $command;
my $o_shell_mode = 0;
sub main {
	# for repeated invokes
	undef $command;

	if (scalar @ARGV == 2 and $ARGV[0] eq 'get' and $ARGV[1] eq 'ride') {
		say __("You've got ride.");
		return 0;
	}

	if ($config->get_string('quiet')) {
		close(STDOUT) or mydie('unable to close standard output: %s', $!);
		open(STDOUT, '>>', '/dev/null') or mydie('unable to turn standard output to /dev/null: %s', $!);
		$download_progress_class_name = 'Cupt::Download::Progress';
	}

	foreach my $idx (0..$#ARGV) {
		if ($ARGV[$idx] eq '-h' or $ARGV[$idx] eq '--help') {
			$ARGV[$idx] = 'help'; ## no critic (LocalizedPunctuationVars)
		}
		if ($ARGV[$idx] eq '-v' or $ARGV[$idx] eq '--version') {
			$ARGV[$idx] = 'version'; ## no critic (LocalizedPunctuationVars)
		}
		if ($ARGV[$idx] !~ m/^-/) {
			# this is not a option, then it's a command
			$command = $ARGV[$idx];
			splice @ARGV, $idx, 1;
			last;
		}
	}

	$command //= 'help';

	my %command_handlers = (
		'version' => \&version,
		'help' => \&help,
		'config-dump' => \&config_dump,
		'show' => \&show_binary_package_versions,
		'showsrc' => \&show_source_package_versions,
		'search' => \&search,
		'depends' => sub { show_package_relations('normal') },
		'rdepends' => sub { show_package_relations('reverse') },
		'why' => \&why,
		'policy' => sub { policy('binary') },
		'policysrc' => sub { policy('source') },
		'pkgnames' => \&pkgnames,
		'changelog' => sub { show_changelog_or_copyright('changelog') },
		'copyright' => sub { show_changelog_or_copyright('copyright') },
		'screenshots' => \&screenshots,
		'update' => \&update_release_data,
		'install' => sub { manage_packages('install') },
		'reinstall' => sub { manage_packages('reinstall') },
		'remove' => sub { manage_packages('remove') },
		'purge' => sub { manage_packages('purge') },
		'satisfy' => sub { manage_packages('satisfy') },
		'safe-upgrade' => sub { manage_packages('safe-upgrade') },
		'full-upgrade' => sub { manage_packages('full-upgrade') },
		'dist-upgrade' => \&dist_upgrade,
		'build-dep' => sub { manage_packages('build-depends') },
		'source' => \&acquire_source_package,
		'clean' => sub { clean_archives(0) },
		'autoclean' => sub { clean_archives(1) },
		'markauto' => sub { mark_autoinstalled('markauto') },
		'unmarkauto' => sub { mark_autoinstalled('unmarkauto') },
		'shell' => \&shell,
		'snapshot' => \&snapshot,
	);

	if (defined($command_handlers{$command})) {
		eval {
			return ($command_handlers{$command}->() // 0);
		};
		if (mycatch()) {
			myerr("error performing command '%s'", $command);
			return 3;
		}
	} else {
		myerr("unrecognized command '%s'", $command);
		undef $command;
		return 2;
	}
}

# launch!
my $main_result = main();
exit $main_result;

sub build_cache {
	eval {
		# propagate any parameters passed to Cupt::Cache::&new
		$cache //= Cupt::Cache->new($config, @_);
	};
	if (mycatch()) {
		myerr('error while creating package cache');
		exit 1;
	}
}

sub check_package_name ($) {
	if (! ($_[0] =~ m/^$package_name_regex$/)) {
		mydie("bad package name '%s'", $_[0]);
	}
	return;
}

sub check_version_string ($) {
	if (! ($_[0] =~ m/^$version_string_regex$/)) {
		mydie("bad version string '%s'", $_[0]);
	}
	return;
}

sub check_no_extra_args () {
	if (scalar @ARGV > 0) {
		mywarn("extra arguments '%s' are not processed", "@ARGV");
	}
	return;
}

sub check_no_extra_options () {
	foreach (@ARGV) {
		m/^-/ and mydie("unknown option '%s'", $_);
	}
	return;
}

sub get_binary_package ($$) {
	my ($package_name, $o_fatal) = @_;
	my $package = $cache->get_binary_package($package_name);
	if (not defined $package and $o_fatal) {
		mydie("cannot find binary package '%s'", $package_name);
	}
	return $package;
}

sub get_source_package ($$) {
	my ($package_name, $o_fatal) = @_;
	my $package = $cache->get_source_package($package_name);
	if (not defined $package and $o_fatal) {
		mydie("cannot find source package '%s'", $package_name);
	}
	return $package;
}

sub select_package_version ($$$) { ## no critic (RequireFinalReturn)
	my ($package_expression, $sub_package_getter, $o_fatal) = @_;

	if ($package_expression =~ m/^(.*?)=(.*)/) {
		# selecting by strict version string
		# example: "nlkt=0.3.2.1-1"
		my $package_name = $1;
		check_package_name($package_name);
		my $version_string = $2;
		check_version_string($version_string);
		my $package = $sub_package_getter->($package_name, $o_fatal);
		return undef if not defined $package;
		my $version = $package->get_specific_version($version_string);
		# not found
		if (defined $version) {
			return $version;
		} else {
			if ($o_fatal) {
				mydie("cannot find version '%s' for package '%s'", $version_string, $package_name);
			} else {
				return undef;
			}
		}
	} elsif ($package_expression =~ m/^(.*?)\/(.*)/) {
		# selecting by release distibution
		my $package_name = $1;
		check_package_name($package_name);
		my $distribution_expression = $2;
		if ($distribution_expression !~ m/^[a-z-]+$/) {
			if ($o_fatal) {
				mydie("bad distribution '%s' requested, use archive or codename", $distribution_expression);
			}
		}
		my $package = $sub_package_getter->($package_name, $o_fatal);
		return undef if not defined $package;

		# example: "nlkt/sid" or "nlkt/unstable"
		foreach my $version (@{$package->get_versions()}) {
			my @available_as = @{$version->available_as};

			foreach (@available_as) {
				if ($_->{release}->{archive} eq $distribution_expression ||
					$_->{release}->{codename} eq $distribution_expression)
				{
					# found such a version
					return $version;
				}
			}
		}
		# not found
		if ($o_fatal) {
			mydie("cannot find distribution '%s' for package '%s'", $distribution_expression, $package_name);
		} else {
			return undef;
		}
	} else {
		my $package = $sub_package_getter->($package_expression, $o_fatal);
		return undef if not defined $package;
		my $result_version = $cache->get_policy_version($package);
		if (not defined $result_version and $o_fatal) {
			mydie("no versions available for package '%s'", $package_expression);
		} else {
			return $result_version;
		}
	}
}

sub select_binary_package_version ($$) {
	my ($package_name, $o_fatal) = @_;
	return select_package_version($package_name, \&get_binary_package, $o_fatal);
}

sub select_source_package_version ($$) { ## no critic (RequireFinalReturn)
	my ($package_expression, $o_fatal) = @_;

	# there we should gracefully accept both source and binary packages names,
	# let's try...

	my $version = select_package_version($package_expression, \&get_source_package, 0);
	# is there a source package with that name?
	return $version if (defined $version);

	# try binary
	$version = select_package_version($package_expression, \&get_binary_package, 0);

	if (defined $version) {
		my $binary_version = $version;
		my $source_package = get_source_package($binary_version->source_package_name, 1);
		my $source_version_string = $binary_version->source_version_string;
		my $source_version = $source_package->get_specific_version($source_version_string);
		if (not defined $source_version) {
			mydie("cannot find version '%s' for package '%s'",
					$source_version_string, $binary_version->source_package_name);
		}
		return $source_version;
	} else {
		if ($o_fatal) {
			mydie("cannot find appropriate source or binary version for '%s'", $package_expression);
		} else {
			return undef;
		}
	}
}

# returns array of versions
sub select_package_versions_wildcarded ($$;$) {
	my ($expression, $source_or_binary, $o_fatal) = @_;
	$o_fatal //= 1;

	my ($package_name, $remainder) = ($expression =~ m{([^=/]+)((?:=|/).*)?});
	$remainder //= '';

	my $sub_selector = $source_or_binary eq 'source' ?
			\&select_source_package_version : \&select_binary_package_version;

	if ($package_name !~ m/(?:\*|\?)/) {
		# there were no wildcards
		my $version = $sub_selector->($expression, $o_fatal);
		if (defined $version) {
			return ( $version );
		} else {
			return ();
		}
	} else {
		# handling wildcards
		my @result;
		glob_to_regex(my $package_regex = $package_name);

		my $method_name = "get_${source_or_binary}_package_names";
		foreach my $package_name ($cache->$method_name()) {
			if ($package_name =~ m/^$package_regex$/) {
				my $version = $sub_selector->("$package_name$remainder", 0);
				if (defined $version) {
					push @result, $version;
				}
			}
		}

		if (not scalar @result and $o_fatal) {
			mydie("no appropriate versions available for wildcarded version expression '%s'", $expression);
		}
		return @result;
	}
}

sub version () {
	check_no_extra_options();
	check_no_extra_args();
	build_cache(-source => 0, -binary => 0, -installed => 1);
	my @packages = qw(cupt libcupt-perl);
	foreach my $package (@packages) {
		say "$package: ", $cache->get_system_state()->get_installed_version_string($package);
	}
	return 0;
}

sub help () {
	check_no_extra_options();
	check_no_extra_args();
	# we don't need the cache :)
	my %action_descriptions = (
		'help' => __('prints a short help'),
		'version' => __("prints versions of packages 'cupt' and 'libcupt-perl'"),
		'config-dump' => __('prints values of configuration variables'),
		'show' => __('prints info about binary package(s)'),
		'showsrc' => __('prints info about source packages(s)'),
		'search' => __('searches for packages using regular expression(s)'),
		'depends' => __('prints dependencies of binary package(s)'),
		'rdepends' => __('print reverse-dependencies of binary package(s)'),
		'why' => __('finds a dependency path between system/package(s) and a package'),
		'policy' => __('prints pin info for the binary package(s)'),
		'policysrc' => __('prints pin info for the source package(s)'),
		'pkgnames' => __('prints available package names'),
		'changelog' => __('views Debian changelog(s) of binary package(s)'),
		'copyright' => __('views Debian copyright info of binary package(s)'),
		'screenshots' => __('views Debian screenshot web pages for the binary package(s)'),
		'update' => __('updates repository metadata'),
		'install' => __('installs/upgrades/downgrades binary package(s)'),
		'reinstall' => __('reinstalls binary packages(s)'),
		'remove' => __('removes binary package(s)'),
		'purge' => __('removes binary package(s) along with their configuration files'),
		'satisfy' => __('performs actions to make relation expressions satisfied'),
		'safe-upgrade' => __('upgrades the system without removing packages'),
		'full-upgrade' => __('upgrades the system with possible removal of some packages'),
		'dist-upgrade' => __('does a two-stage full upgrade'),
		'build-dep' => __('satisfies build dependencies for source package(s)'),
		'source' => __('fetches and unpacks source package(s)'),
		'clean' => __('cleans the whole binary package cache'),
		'autoclean' => __('cleans unavailable from repositories archives from binary package cache'),
		'markauto' => __('marks binary package(s) as automatically installed'),
		'unmarkauto' => __('marks binary package(s) as manually installed'),
		'shell' => __('starts an interactive package manager shell'),
		'snapshot' => __('works with system snapshots'),
	);

	say __('Usage: cupt <action> [<parameters>]');
	say '';
	say __('Actions:');
	foreach my $action (sort keys %action_descriptions) {
		say "  $action: ", $action_descriptions{$action};
	}
	return 0;
}

sub config_dump () {
	check_no_extra_options();

	my $particular_key = shift @ARGV;

	check_no_extra_args();

	if (defined $particular_key) {
		my $value;
		$value = $config->get_string($particular_key);
		$value //= $config->get_list($particular_key); # if it's the list option
		$value //= '';
		say $value;
	} else {
		foreach my $key (sort $config->get_scalar_option_names()) {
			my $value = $config->get_string($key);
			defined $value or next;
			say sprintf q/%s "%s";/, $key, $value;
		}

		foreach my $key (sort $config->get_list_option_names()) {
			foreach my $element ($config->get_list($key)) {
				say qq/$key { "$element"; };/;
			}
		}
	}
	return 0;
}

sub show_binary_package_versions () {
	my $o_with_release_info = 0;
	my $o_installed_only = 0;
	GetOptions(
		'with-release-info' => \$o_with_release_info,
		'installed-only' => \$o_installed_only,
	);

	check_no_extra_options();

	my $sub_get_reverse_provides = sub {
		my ($package_name) = @_;

		my $virtual_relation = Cupt::Cache::Relation->new("$package_name");
		my $ref_satisfying_versions = $cache->get_satisfying_versions($virtual_relation);
		my @relations;
		foreach (@$ref_satisfying_versions) {
			# we don't need versions of the same package
			my $new_package_name = $_->package_name;
			$new_package_name ne $package_name or next;

			my $new_relation_version_string = $_->version_string;
			my $new_relation = Cupt::Cache::Relation->new("$new_package_name (= $new_relation_version_string)");
			push @relations, $new_relation;
		}
		return \@relations;
	};

	my @package_expressions = @ARGV;
	scalar @package_expressions or
			mydie('no package expressions specified');

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	foreach my $package_expression (@package_expressions) {
		my $ref_versions;
		my $p = sub { print shift, ': ', shift, "\n" };

		if ($config->get_bool('apt::cache::allversions')) {
			$ref_versions = get_binary_package($package_expression, 1)->get_versions();
		} else {
			if (not defined get_binary_package($package_expression, 0)
				and $package_expression =~ /^$package_name_regex$/) {
				# there is no such binary binary, maybe it's virtual?
				my $ref_reverse_provides = $sub_get_reverse_provides->($package_expression);
				if (scalar @$ref_reverse_provides) {
					$p->(__('Virtual package, provided by'), stringify_relation_expressions($ref_reverse_provides));
					$ref_versions = [];
				}
			}
			if (!defined $ref_versions) {
				my @versions = select_package_versions_wildcarded($package_expression, 'binary');
				$ref_versions = \@versions;
			}
		}

		foreach my $version (@$ref_versions) {
			my $package_name = $version->package_name;
			$p->(__('Package'), $package_name);
			$p->(__('Version'), $version->version_string);
			my $installed_status = $cache->get_system_state()->get_status_for_version($version);
			$p->(__('Status'), defined($installed_status) ? $installed_status->{'status'} : 'not installed');
			if ($version->is_installed()) {
				my $is_auto_installed = $cache->is_automatically_installed($package_name);
				$p->(__('Automatically installed'), $is_auto_installed ? __('yes') : __('no'));
			}
			$p->(__('Source'), $version->source_package_name);
			if ($version->source_version_string ne $version->version_string) {
				$p->(__('Source version'), $version->source_version_string);
			}
			$p->(__('Essential'), $version->essential) if $version->essential;
			$p->(__('Priority'), $version->priority);
			$p->(__('Section'), $version->section) if defined $version->section;
			$p->(__('Size'), human_readable_size_string($version->size)) if defined $version->size;
			$p->(__('Uncompressed size'), human_readable_size_string($version->installed_size));
			$p->(__('Maintainer'), $version->maintainer);
			$p->(__('Architecture'), $version->architecture);
			if ($o_with_release_info) {
				foreach (@{$version->available_as}) {
					next if !defined($_->{release}->{description}); # no release description available
					$p->(__('Release'), $_->{release}->{description});
				}
			}
			$p->(__('Pre-Depends'), stringify_relation_expressions($version->pre_depends)) if @{$version->pre_depends};
			$p->(__('Depends'), stringify_relation_expressions($version->depends)) if @{$version->depends};
			$p->(__('Recommends'), stringify_relation_expressions($version->recommends)) if @{$version->recommends};
			$p->(__('Suggests'), stringify_relation_expressions($version->suggests)) if @{$version->suggests};
			$p->(__('Conflicts'), stringify_relation_expressions($version->conflicts)) if @{$version->conflicts};
			$p->(__('Breaks'), stringify_relation_expressions($version->breaks)) if @{$version->breaks};
			$p->(__('Replaces'), stringify_relation_expressions($version->replaces)) if @{$version->replaces};
			$p->(__('Provides'), join(', ', @{$version->provides})) if @{$version->provides};
			$p->(__('Enhances'), stringify_relation_expressions($version->enhances)) if @{$version->enhances};
			my $ref_reverse_provides = $sub_get_reverse_provides->($package_name);
			if (scalar @$ref_reverse_provides) {
				$p->(__('Provided by'), stringify_relation_expressions($ref_reverse_provides));
			}
			foreach (map { $_->{'download_uri'} } $version->uris()) {
				$p->('URI', $_);
			}
			$p->('MD5', $version->md5sum) if defined $version->md5sum;
			$p->('SHA1', $version->sha1sum) if defined $version->sha1sum;
			$p->('SHA256', $version->sha256sum) if defined $version->sha256sum;
			if (defined $version->short_description) {
				$p->(__('Description'), $version->short_description);
				print $version->long_description if defined $version->long_description;
			}
			$p->(__('Task'), $version->task) if defined($version->task);
			$p->(__('Tags'), $version->tags) if defined($version->tags);
			foreach my $other_field (sort keys %{$version->others}) {
				$p->($other_field, $version->others->{$other_field});
			}
			print "\n";
		}
	}
	return 0;
}

sub show_source_package_versions () {
	my $o_with_release_info = 0;
	GetOptions(
		'with-release-info' => \$o_with_release_info,
	);

	check_no_extra_options();

	my @package_expressions = @ARGV;
	scalar @package_expressions or
			mydie('no package expressions specified');

	build_cache(-source => 1, -binary => 1, -installed => 1);

	foreach my $package_expression (@package_expressions) {
		my $ref_versions;
		my $p = sub { print shift, ': ', shift, "\n" };

		if ($config->get_bool('apt::cache::allversions')) {
			$ref_versions = get_source_package($package_expression, 1)->get_versions();
		} else {
			my @versions = select_package_versions_wildcarded($package_expression, 'source');
			$ref_versions = \@versions;
		}

		foreach my $version (@$ref_versions) {
			my $package_name = $version->package_name;
			$p->(__('Package'), $package_name);
			$p->(__('Binary'), join(', ', @{$version->binary_package_names}));
			$p->(__('Version'), $version->version_string);
			$p->(__('Priority'), $version->priority);
			$p->(__('Section'), $version->section) if defined $version->section;
			$p->(__('Maintainer'), $version->maintainer);
			$p->(__('Uploaders'), join(', ', @{$version->uploaders})) if scalar @{$version->uploaders};
			$p->(__('Architecture'), $version->architecture);
			if ($o_with_release_info) {
				foreach (@{$version->available_as}) {
					next if !defined($_->{release}->{description}); # no release description available
					$p->(__('Release'), $_->{release}->{description});
				}
			}
			$p->(__('Build-Depends'), stringify_relation_expressions($version->build_depends));
			if (scalar @{$version->build_depends_indep}) {
				$p->(__('Build-Depends-Indep'), stringify_relation_expressions($version->build_depends_indep));
			}
			if (scalar @{$version->build_conflicts}) {
				$p->(__('Build-Conflicts'), stringify_relation_expressions($version->build_conflicts));
			}
			if (scalar @{$version->build_conflicts_indep}) {
				$p->(__('Build-Conflicts-Indep'), stringify_relation_expressions($version->build_conflicts_indep));
			}
			{
				my $ref_uris = $version->uris();
				foreach my $part ('tarball', 'diff', 'dsc') {
					next if ($part eq 'diff' and not defined $version->diff);
					$p->($part, '');
					$p->('  ' . __('Size'), human_readable_size_string($version->$part->{'size'}));
					$p->('  MD5', $version->$part->{'md5sum'}) if defined $version->$part->{'md5sum'};
					$p->('  SHA1', $version->$part->{'sha1sum'}) if defined $version->$part->{'sha1sum'};
					$p->('  SHA256', $version->$part->{'sha256sum'}) if defined $version->$part->{'sha256sum'};
					foreach (map { $_->{'download_uri'} } @{$ref_uris->{$part}}) {
						$p->('  URI', $_);
					}
				}
			}
			foreach my $other_field (sort keys %{$version->others}) {
				$p->($other_field, $version->others->{$other_field});
			}
			print "\n";
		}
	}
	return 0;
}

sub search () {
	# turn off relations parsing, we don't need them
	unless ($o_shell_mode) {
		$Cupt::Cache::BinaryVersion::o_no_parse_relations = 1;
	}

	my $o_names_only = 0;
	my $o_installed_only = 0;
	my $o_case_sensitive = 0;
	GetOptions(
		'names-only|n' => sub { $config->set_scalar('apt::cache::namesonly', 1) },
		'case-sensitive' => \$o_case_sensitive,
		'installed-only' => \$o_installed_only,
	);

	check_no_extra_options();
	my @patterns = @ARGV;
	scalar @patterns or
			mydie('no search patterns specified');

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	my @regexes;
	foreach my $pattern (@patterns) {
		eval {
			push @regexes, ($o_case_sensitive ? qr/$pattern/ : qr/$pattern/i);
		};
		if ($@) {
			# something was bad with regex compilation
			mydie("regular expression '%s' is not valid", $pattern);
		}
	}

	if ($config->get_bool('apt::cache::namesonly')) {
		# search only in package names
		foreach my $package_name ($cache->get_binary_package_names()) {
			my $matched = 1;
			foreach my $regex (@regexes) {
				if ($package_name !~ m/$regex/) {
					$matched = 0;
					last;
				}
			}

			say $package_name if $matched;
		}
	} else {
		foreach my $package_name ($cache->get_binary_package_names()) {
			my $package = $cache->get_binary_package($package_name);
			my $matched = 1;
			my $version;
			REGEX:
			foreach my $regex (@regexes) {
				if ($package_name =~ m/$regex/) {
					next REGEX;
				} else {
					foreach (@{$package->get_versions()}) {
						if ((defined $_->short_description and $_->short_description =~ m/$regex/) or
							(defined $_->long_description and $_->long_description =~ m/$regex/))
						{
							$version = $_;
							next REGEX;
						}
					}
				}
				$matched = 0;
				last REGEX;
			}
			next if !$matched;

			if (defined $version) {
				say $package_name . ' - ' . $version->short_description if $matched;
			} else {
				say $package_name;
			}
		}
	}
	return;
}

sub show_package_relations ($) {
	my ($mode) = @_;

	# turn off info parsing, we don't need it, only relations :)
	unless ($o_shell_mode) {
		$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;
	}

	my $o_with_suggests = 0;
	my $o_installed_only = 0;
	GetOptions(
		'with-suggests' => \$o_with_suggests,
		'installed-only' => \$o_installed_only,
	);

	check_no_extra_options();

	scalar @ARGV or
			mydie('no binary package expressions specified');

	if ($mode eq 'reverse') {
		$Cupt::Cache::Package::o_memoize = 1;
	}

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	my @versions = map { select_package_versions_wildcarded($_, 'binary') } @ARGV;

	my @relation_groups = (
		[ 'pre_depends', __('Pre-Depends') ],
		[ 'depends', __('Depends') ],
	);
	if (!$config->get_bool('apt::cache::important')) {
		push @relation_groups, [ 'recommends', __('Recommends') ];

		if ($o_with_suggests) {
			push @relation_groups, [ 'suggests', __('Suggests') ];
		}
	}

	# don't output the same version more than one time
	my %processed_entries;

	# used only by rdepends
	my %reverse_depends_index;
	if ($mode eq 'reverse') {
		my @relation_group_names = map { $_->[0] } @relation_groups;
		foreach my $package_name ($cache->get_binary_package_names()) {
			my $package_candidate = $cache->get_binary_package($package_name);
			foreach my $reverse_version_candidate (@{$package_candidate->get_versions()}) {
				foreach my $relation_group_name (@relation_group_names) {
					foreach (@{$reverse_version_candidate->$relation_group_name}) {
						foreach my $version_candidate (@{$cache->get_satisfying_versions($_)}) {
							push @{$reverse_depends_index{$version_candidate->package_name}},
									$package_name;
						}
					}
				}
			}
		}
		foreach my $ref_index_entry (values %reverse_depends_index) {
			@$ref_index_entry = sort(uniq(@$ref_index_entry));
		}
	}

	while (scalar @versions) {
		my $version = shift @versions;

		my $package_name = $version->package_name;
		my $version_string = $version->version_string;

		next if exists $processed_entries{$package_name,$version_string};
		$processed_entries{$package_name,$version_string} = 1;

		say "$package_name $version_string:";
		foreach my $item (@relation_groups) {
			my $relation_group_name = $item->[0];
			my $caption = $item->[1];

			# fill relations with actual relations
			if ($mode eq 'normal') {
				# just plain normal dependencies
				foreach my $relation_expression (@{$version->$relation_group_name}) {
					say "  $caption: ", stringify_relation_expression($relation_expression);
					if ($config->get_bool('apt::cache::recursedepends')) {
						# insert recursive depends into queue
						my @satisfying_versions = @{$cache->get_satisfying_versions($relation_expression)};
						if ($config->get_bool('apt::cache::allversions')) {
							push @versions, @satisfying_versions;
						} else {
							# push the version with the maximum pin
							push @versions, reduce {
										$cache->get_pin($a) >= $cache->get_pin($b) ? $a : $b
									} @satisfying_versions;
						}
					}
				}
			} else {
				# we have to check all reverse dependencies for this version
				foreach my $package_candidate_name (@{$reverse_depends_index{$package_name} // []}) {
					my $package_candidate = $cache->get_binary_package($package_candidate_name);
					CANDIDATE:
					foreach my $reverse_version_candidate (@{$package_candidate->get_versions()}) {
						foreach (@{$reverse_version_candidate->$relation_group_name}) {
							foreach my $version_candidate (@{$cache->get_satisfying_versions($_)}) {
								if ($version_candidate->package_name eq $package_name
									&& $version_candidate->version_string eq $version_string)
								{
									# positive result
									my $label = $reverse_version_candidate->package_name .
											' ' . $reverse_version_candidate->version_string;
									if ($config->get_bool('apt::cache::recursedepends')) {
										push @versions, $reverse_version_candidate;
									}
									print '  ' . __('Reverse-') . "$caption: $label: ";
									say stringify_relation_expression($_);

									next CANDIDATE;
								}
							}
						}
					}
				}
			}
		}
	}
	return 0;
}

sub why ($) {
	# turn off info parsing, we don't need it, only relations :)
	unless ($o_shell_mode) {
		$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;
	}

	my $o_installed_only = 0;
	GetOptions(
		'installed-only' => \$o_installed_only,
	);

	check_no_extra_options();

	scalar @ARGV or
			mydie('no binary packages specified');


	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	my $leaf_package_expression = pop @ARGV;
	my $leaf_version = select_binary_package_version($leaf_package_expression, 1);

	my @versions;
	if (scalar @ARGV) {
		# selected packages
		@versions = map { select_package_versions_wildcarded($_, 'binary') } @ARGV;
	} else {
		# the whole system
		@versions = @{$cache->get_system_state()->export_installed_versions()};
		@versions = grep { not $cache->is_automatically_installed($_->package_name) } @versions;
	}

	my @relation_groups = (
		[ 'pre_depends', __('Pre-Depends') ],
		[ 'depends', __('Depends') ],
	);
	if ($config->get_bool('cupt::resolver::keep-recommends')) {
		push @relation_groups,
				[ 'recommends', __('Recommends') ];
	}
	if ($config->get_bool('cupt::resolver::keep-suggests')) {
		push @relation_groups,
				[ 'suggests', __('Suggests') ];
	}

	# don't output the same version more than one time
	my %processed_entries;

	my %previouses;

	$previouses{$_->package_name,$_->version_string} = undef foreach @versions;

	while (scalar @versions) {
		my $version = shift @versions;
		my $package_name = $version->package_name;
		my $version_string = $version->version_string;

		next if $processed_entries{$package_name,$version_string}++;

		if ($package_name eq $leaf_version->package_name and
			$version_string eq $leaf_version->version_string)
		{
			# we found a path, re-walk it
			my @path;
			my $current_package_name = $package_name;
			my $current_version_string = $version_string;

			while (defined $previouses{$current_package_name,$current_version_string}) {
				$_ = $previouses{$current_package_name,$current_version_string};
				my $current_version = $_->[0];
				$current_package_name = $current_version->package_name;
				$current_version_string = $current_version->version_string;
				unshift @path, $_;
			}
			foreach my $ref_path_entry (@path) {
				my ($path_version, $caption, $relation_expression) = @$ref_path_entry;
				say sprintf '%s %s: %s: %s',
						$path_version->package_name,
						$path_version->version_string,
						$caption,
						stringify_relation_expression($relation_expression);
			}
			last;
		}

		foreach my $item (@relation_groups) {
			my $relation_group_name = $item->[0];
			my $caption = $item->[1];

			foreach my $relation_expression (@{$version->$relation_group_name}) {
				# insert recursive depends into queue
				foreach my $new_version (@{$cache->get_satisfying_versions($relation_expression)}) {
					my $new_package_name = $new_version->package_name;
					my $new_version_string = $new_version->version_string;
					push @versions, $new_version;
					if (not exists $previouses{$new_package_name,$new_version_string}) {
						$previouses{$new_package_name,$new_version_string} =
								[ $version, $caption, $relation_expression ];
					}
				}
			}
		}
	}
	return 0;
}

sub policy ($) {
	my ($flavour) = @_;
	# turn off info and relations parsing, we don't need it
	unless ($o_shell_mode) {
		$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;
		$Cupt::Cache::BinaryVersion::o_no_parse_relations = 1;
		$Cupt::Cache::SourceVersion::o_no_parse_info_onlys = 1;
		$Cupt::Cache::SourceVersion::o_no_parse_relations = 1;
	}

	check_no_extra_options();

	my @packages = @ARGV;

	if (scalar @packages) {
		# print release info for supplied package names

		build_cache(-source => $flavour eq 'source',
				-binary => $flavour eq 'binary', -installed => $flavour eq 'binary');

		for my $package_name (@packages) {
			my $package = $flavour eq 'binary' ?
					get_binary_package($package_name, 1) : get_source_package($package_name, 1);
			my $policy_version = $cache->get_policy_version($package);
			defined $policy_version or
					mydie("no versions available for package '%s'", $package_name);

			my $installed_version_string = $flavour eq 'source' ? undef :
					$cache->get_system_state()->get_installed_version_string($package_name);
			say "$package_name:";
			if ($flavour eq 'binary') {
				say '  ' . __('Installed') . ': ' .
						($installed_version_string // ('(' . __('none') . ')'));
			}

			say '  ' . __('Candidate') . ': ' . $policy_version->version_string;
			say '  ' . __('Version table') . ':';

			my $ref_versions = $cache->get_sorted_pinned_versions($package);

			foreach my $entry (@$ref_versions) {
				my $version = $entry->{'version'};
				my $pin = $entry->{'pin'};

				if (defined $installed_version_string &&
					$version->version_string eq $installed_version_string)
				{
					print ' *** ';
				} else {
					print '     ';
				}

				say $version->version_string . ' ' . $pin;

				my @available_as = @{$version->available_as};
				foreach (@available_as) {
					print (' ' x 8);
					my $origin = $_->{release}->{base_uri} || $config->get_string('dir::state::status');
					print $origin . ' ';
					print $_->{release}->{archive} . '/'. $_->{release}->{component} . ' ';
					say '(' . ($_->{release}->{signed} ? __('signed') : __('unsigned')) . ')';
				}
			}
		}
	} else {
		# print overall release data
		build_cache(-source => 1, -binary => 1, -installed => 1);

		my $sub_say_release_info = sub {
			my ($ref_release_info) = @_;
			my $origin = $ref_release_info->{base_uri} || $config->get_string('dir::state::status');
			my $archive = $ref_release_info->{archive};
			my $component = $ref_release_info->{component};
			print "  $origin $archive/$component: ";
			print 'o=', $ref_release_info->{vendor};
			print ',a=', $archive;
			print ',l=', $ref_release_info->{label};
			print ',c=', $component;
			print ',v=', $ref_release_info->{version};
			print ',n=', $ref_release_info->{codename};
			say '';
		};

		say 'Package files:';
		my $ref_binary_release_data = $cache->get_binary_release_data();
		foreach my $ref_release_info (@$ref_binary_release_data) {
			$sub_say_release_info->($ref_release_info);
		}

		say 'Source files:';
		my $ref_source_release_data = $cache->get_source_release_data();
		foreach my $ref_release_info (@$ref_source_release_data) {
			$sub_say_release_info->($ref_release_info);
		}
	}
	return;
}

sub mark_autoinstalled ($) {
	require Cupt::System::Worker;
	my ($mark_action) = @_;

	check_no_extra_options();

	build_cache(-source => 0, -binary => 0, -installed => 0);

	my $worker = Cupt::System::Worker->new($config, $cache);
	my $markauto = $mark_action eq 'markauto' ? 1 : 0;
	$worker->mark_as_automatically_installed($markauto, @ARGV);

	return;
}

sub pkgnames () {
	my $o_installed_only = 0;
	GetOptions(
		'installed-only' => \$o_installed_only,
	);

	check_no_extra_options();

	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	my $prefix = '';
	if (scalar @ARGV) {
		$prefix = quotemeta(shift @ARGV);
	}

	check_no_extra_args();

	foreach my $package_name ($cache->get_binary_package_names()) {
		# check package name for pattern and output it
		$package_name =~ m/^$prefix/o and say $package_name;
	}

	return;
}

sub generate_management_prompt ($$$) {
	my ($worker, $show_versions, $show_size_changes) = @_;

	my $show_reasons = $config->get_bool('cupt::resolver::track-reasons');

	my $sub_prompt = sub {
		my $ref_desired_packages = $_[0];
		$worker->set_desired_state($ref_desired_packages);

		my $ref_actions_preview = $worker->get_actions_preview();
		my $ref_unpacked_sizes_preview = $worker->get_unpacked_sizes_preview($ref_actions_preview);

		my $action_count = 0;
		# print planned actions
		do {
			my %action_names = (
				'install' => __('INSTALLED'),
				'remove' => __('REMOVED'),
				'upgrade' => __('UPGRADED'),
				'purge' => __('PURGED'),
				'downgrade' => __('DOWNGRADED'),
				'configure' => __('CONFIGURED'),
				'deconfigure' => __('DECONFIGURED'),
			);
			say '';
			foreach my $action (qw(install upgrade remove purge downgrade configure deconfigure)) {
				my $ref_package_entries = $ref_actions_preview->{$action};

				# sort by package name
				@$ref_package_entries = sort { $a->{'package_name'} cmp $b->{'package_name'} } @$ref_package_entries;

				scalar @$ref_package_entries or next; # don't print empty lists
				my $action_name = $action_names{$action};
				say sprintf __('The following %u packages will be %s:'), scalar @$ref_package_entries, $action_name;
				say '';
				foreach my $ref_package_entry (@$ref_package_entries) {
					++$action_count;
					my $package_name = $ref_package_entry->{'package_name'};
					print "$package_name";
					if ($show_versions) {
						my $old_version_string =
								$cache->get_system_state()->get_installed_version_string($package_name);
						my $new_version = $ref_package_entry->{'version'};
						my $new_version_string = defined $new_version ?
								$new_version->version_string : undef;
						if (defined $old_version_string and defined $new_version_string) {
							print " [$old_version_string -> $new_version_string]";
						} elsif (defined $old_version_string) {
							print " [$old_version_string]";
						} elsif (defined $new_version_string) {
							print " [$new_version_string]";
						}
					}
					if ($show_size_changes) {
						my $size_change = $ref_unpacked_sizes_preview->{$package_name};
						if ($size_change != 0) {
							my $size_change_string = ($size_change >= 0) ?
									'+' . human_readable_size_string($size_change) :
									'-' . human_readable_size_string(-$size_change);
							print " <$size_change_string>";
						}
					}

					if ($show_versions || $show_size_changes || $show_reasons) {
						# put newline
						say '';
					} else {
						# put a space between package names
						print ' ';
					}

					if ($show_reasons) {
						my $sub_say_reason = sub {
							say '  ' . __('reason: ') . $_[0];
						};
						foreach my $ref_reason (@{$ref_package_entry->{'reasons'}}) {
							my @reason_parts = @$ref_reason;
							my $reason_type = shift @reason_parts;
							given ($reason_type) {
								when ('relation expression') {
									my %reason_dependency_name_translations = (
										'pre_depends' => __('pre-depends on'),
										'depends' => __('depends on'),
										'recommends' => __('recommends'),
										'suggests' => __('suggests'),
										'conflicts' => __('conflicts with'),
										'breaks' => __('breaks'),
									);
									my ($reason_version, $reason_dependency_name, $reason_relation_expression) = @reason_parts;

									my $reason_package_name = $reason_version->package_name;
									my $reason_version_string = $reason_version->version_string;

									$sub_say_reason->(sprintf("%s %s %s '%s'",
											$reason_package_name, $reason_version_string,
											$reason_dependency_name_translations{$reason_dependency_name},
											stringify_relation_expression($reason_relation_expression)
											));
								}
								when ('sync') {
									my ($reason_package_name) = @reason_parts;
									$sub_say_reason->(sprintf __("synchronized with package '%s'"), $reason_package_name);
								}
								when ('user') {
									$sub_say_reason->(__('user request'));
								}
								when ('auto-remove') {
									$sub_say_reason->(__('auto-removal'));
								}
								default {
									myinternaldie("bad reason type '%s'", $reason_type);
								}
							}
						}
						say '';
					}
				}
				say '' if !$show_versions && !$show_size_changes && !$show_reasons;
				say '';
			}
		};

		# nothing to do maybe?
		if ($action_count == 0) {
			return 1;
		}

		# prepare list of soft dependencies that are not satisfied
		do {
			my @unsatisfied_soft_dependencies;
			my @versions_to_remove;
			foreach my $action (qw(remove purge)) {
				my $ref_package_entries = $ref_actions_preview->{$action};
				foreach my $ref_package_entry (@$ref_package_entries) {
					my $package = get_binary_package($ref_package_entry->{'package_name'}, 0);
					# package or version may be undefined for packages in 'config-files' state
					defined $package or next;
					my $version = $package->get_installed_version();
					defined $version or next;
					push @versions_to_remove, $version;
				}
			}
			foreach my $package_name (keys %$ref_desired_packages) {
				my $ref_package_entry = $ref_desired_packages->{$package_name};
				next if not defined $ref_package_entry->{'version'};

				foreach my $dependency_name (qw(recommends suggests)) {
					next if not $config->get_bool("cupt::resolver::keep-$dependency_name");
					foreach my $relation_expression (@{$ref_package_entry->{'version'}->$dependency_name}) {
						my $ref_satisfying_versions = $cache->get_satisfying_versions($relation_expression);

						my $soft_breakage_possible = 0;
						RELATION_ENTRY:
						foreach my $satisfying_version (@$ref_satisfying_versions) {
							foreach my $version_to_remove (@versions_to_remove) {
								next if $satisfying_version->package_name ne $version_to_remove->package_name;
								next if $satisfying_version->version_string ne $version_to_remove->version_string;
								$soft_breakage_possible = 1;
								last RELATION_ENTRY;
							}
						}

						if ($soft_breakage_possible) {
							my $is_broken = 1;
							foreach my $satisfying_version (@$ref_satisfying_versions) {
								my $satisfying_package_name = $satisfying_version->package_name;
								next if not defined $ref_desired_packages->{$satisfying_package_name};
								my $desired_version = $ref_desired_packages->{$satisfying_package_name}->{'version'};
								next if not defined $desired_version;
								next if $desired_version->version_string ne $satisfying_version->version_string;
								$is_broken = 0;
								last;
							}
							if ($is_broken) {
								my $version_string = $ref_package_entry->{version}->version_string;
								push @unsatisfied_soft_dependencies,
										sprintf __('%s %s %s %s'), $package_name, $version_string,
										$dependency_name eq 'recommends' ? __('recommends') : __('suggests'),
										stringify_relation_expression($relation_expression);
							}
						}
					}
				}
			}
			if (scalar @unsatisfied_soft_dependencies) {
				say sprintf __('Leave the following dependencies unresolved:');
				say '';
				say $_ for @unsatisfied_soft_dependencies;
				say '';
			}
		};

		my $is_dangerous_action = 0;

		if (!$config->get_bool('cupt::console::allow-untrusted')) {
			my @untrusted_package_names;
			# generate loud warning for unsigned versions
			foreach my $action (qw(install upgrade downgrade)) {
				my $ref_package_entries = $ref_actions_preview->{$action};
				foreach my $version (map { $_->{version} } @$ref_package_entries) {
					push @untrusted_package_names, $version->package_name if !$version->is_signed();
				}
			}
			if (scalar @untrusted_package_names) {
				$is_dangerous_action = 1;
				say sprintf __('WARNING! The untrusted versions of the following packages will be used:');
				say '';
				say $_ for @untrusted_package_names;
				say '';
			}
		}

		do { # generate loud warnings for removing essential packages
			my @essential_package_names;
			# generate loud warning for unsigned versions
			foreach my $action (qw(remove purge)) {
				my $ref_package_entries = $ref_actions_preview->{$action};
				foreach my $package_name (map { $_->{'package_name'} } @$ref_package_entries) {
					my $package = get_binary_package($package_name, 0);
					next if not defined $package;
					my $version = $package->get_installed_version();
					next if not defined $version; # purge of config-files package when candidates available
					push @essential_package_names, $version->package_name if $version->essential;
				}
			}
			if (scalar @essential_package_names) {
				$is_dangerous_action = 1;
				say sprintf __('WARNING! The following essential packages will be REMOVED:');
				say '';
				say $_ for @essential_package_names;
				say '';
			}
		};

		do { # print size estimations
			my ($total_bytes, $need_bytes) = $worker->get_download_sizes_preview($ref_actions_preview);
			print sprintf __('Need to get %s/%s of archives. '),
					human_readable_size_string($need_bytes),
					human_readable_size_string($total_bytes);

			my $total_unpacked_size_change =
					sum(values %$ref_unpacked_sizes_preview) // 0;
			if ($total_unpacked_size_change >= 0) {
				say sprintf __('After unpacking %s will be used.'),
						human_readable_size_string($total_unpacked_size_change);
			} else {
				say sprintf __('After unpacking %s will be freed.'),
						human_readable_size_string(-$total_unpacked_size_change);
			}
		};

		my $confirmation_for_dangerous_action = __('Yes, do as I say!');
		my $question = $is_dangerous_action ?
				sprintf(__("Dangerous actions selected. Type '%s' if you want to continue, 'q' to exit, anything else to discard this solution:\n"), $confirmation_for_dangerous_action) :
				__('Do you want to continue? [y/N/q] ');
		my $positive_answer = $is_dangerous_action ? $confirmation_for_dangerous_action : 'y';

		print $question;
		my $answer;
		my $interactive;

		if ($config->get_bool('cupt::console::assume-yes')) {
			$interactive = 0;
			$answer = 'y';
		} else {
			$interactive = 1;
			$answer = <STDIN>; ## no critic (ExplicitStdin)
		}
		return undef if not defined $answer;
		chomp $answer;
		$answer = lc($answer) if !$is_dangerous_action;
		if ($answer eq $positive_answer) {
			# solution has been accepted
			return 1;
		} elsif ($answer eq 'q') {
			# user is willing to abandon all further tryings
			return undef;
		} elsif ($interactive) {
			# user haven't chosen this solution, try next one
			print __('Resolving further... ');
			return 0;
		} else {
			# non-interactive, abandon immediately
			return undef;
		}
	};

	return $sub_prompt;
}

sub acquire_source_package {
	my @parts_to_download = qw(tarball diff dsc);
	# turn off info and relations parsing, we don't need it
	unless ($o_shell_mode) {
		$Cupt::Cache::SourceVersion::o_no_parse_info_onlys = 1;
		$Cupt::Cache::SourceVersion::o_no_parse_relations = 1;
	}

	my $o_download_only = 0;
	GetOptions(
		'download-only|d' => \$o_download_only,
		'tar-only' => sub { @parts_to_download = ('tarball') },
		'diff-only' => sub { @parts_to_download = ('diff') },
		'dsc-only' => sub { @parts_to_download = ('dsc') },
	);

	check_no_extra_options();

	scalar @ARGV or
			mydie('no package expressions specified');

	build_cache(-source => 1, -binary => 1, -installed => 1);

	my @versions = map { select_package_versions_wildcarded($_, 'source') } @ARGV;

	my @download_entries;

	my $download_progress = new_download_progress();

	foreach my $version (@versions) {
		my $package_name = $version->package_name;
		my $version_string = $version->version_string;
		my $ref_release = $version->available_as->[0]->{'release'};
		my $codename = $ref_release->{'codename'};
		my $component = $ref_release->{'component'};

		my $ref_uris = $version->uris();
		foreach my $part (@parts_to_download) {
			next if $part eq 'diff' and not defined $version->diff;
			my $filename = $version->$part->{filename};
			my @download_uri_entries;
			foreach my $ref_uri_entry (@{$ref_uris->{$part}}) {
				my $download_uri = $ref_uri_entry->{'download_uri'};
				my $base_uri = $ref_uri_entry->{'base_uri'};
				my $long_alias = "$base_uri $codename/$component $package_name $version_string $part";
				push @download_uri_entries, {
					'uri' => $download_uri,
					'short-alias' => "$package_name $part",
					'long-alias' => $long_alias,
				};
			}
			push @download_entries, {
				'uri-entries' => \@download_uri_entries,
				'filename' => $filename,
				'size' => $version->$part->{'size'},
				'post-action' => sub {
					Cupt::Cache::verify_hash_sums($version->$part, $filename) or
							do {
								unlink $filename or
										mywarn("unable to delete file '%s': %s", $filename, $!);
								return sprintf __('%s: hash sums mismatch'), $filename;
							};
					return 0;
				}
			}
		}
	}

	require Cupt::Download::Manager;

	my $download_result;
	do {
		my $download_manager = Cupt::Download::Manager->new($config, $download_progress);
		$download_result = $download_manager->download(@download_entries);
	}; # make sure that download manager is already destroyed at this point

	# fail and exit if it was something bad with downloading
	mydie($download_result) if $download_result;

	if (not $o_download_only and scalar @parts_to_download == 3) {
		# unpack downloaded sources
		foreach my $version (@versions) {
			my $dsc_filename = $version->dsc->{'filename'};
			system("dpkg-source -x $dsc_filename") == 0 or
					mydie('dpkg-source failed: %s', $?);
		}
	}

	# all's good
	return 0;
}

sub manage_packages ($) {
	require Cupt::System::Resolver;
	require Cupt::System::Worker;

	my $o_show_versions = 0;
	my $o_show_size_changes = 0;
	my $o_simulate;
	GetOptions(
		'resolver=s' => sub { $config->set_scalar('cupt::resolver::type', $_[1]) },
		'show-deps|show-reasons|D' => sub { $config->set_scalar('cupt::resolver::track-reasons', 1) },
		'max-solution-count=s' => sub { $config->set_scalar('cupt::resolver::max-solution-count', $_[1]) },
		'no-install-recommends|R' => sub { $config->set_scalar('apt::install-recommends' => 0) },
		'no-remove' => sub { $config->set_scalar('cupt::resolver::no-remove' => 1) },
		'no-auto-remove' => sub { $config->set_scalar('cupt::resolver::auto-remove', 0) },
		'download-only|d' => sub { $config->set_scalar('cupt::worker::download-only', 1) },
		'show-versions|V' => \$o_show_versions,
		'show-size-changes|Z' => \$o_show_size_changes,
		'yes|assume-yes|y' => sub { $config->set_scalar('cupt::console::assume-yes' => 1) },
	);
	# action can be 'install', 'remove', 'purge', 'safe-upgrade', 'full-upgrade'
	my ($action) = @_;

	# turn off info parsing, we don't need it
	unless ($o_shell_mode) {
		$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;
		$Cupt::Cache::SourceVersion::o_no_parse_info_onlys = 1;
	}

	$Cupt::Cache::Package::o_memoize = 1;

	check_no_extra_options();

	local @ARGV = map { ## no critic (ProhibitComplexMappings)
		if (m/^@/) {
			my $file = $_;
			$file =~ s/^@//;
			# reading package expressions from file
			open(my $fd, '<', $file) or
					mydie("unable to open file '%s': %s", $file, $!);
			my @expression_list = <$fd>;
			chomp foreach @expression_list;
			close($fd) or
					mydie("unable to close file '%s': %s", $file, $!);
			@expression_list;
		} else {
			# straight package expression
			$_;
		}
	} @ARGV;

	my $snapshot_name;
	if ($action eq 'load-snapshot') {
		require Cupt::System::Snapshots;
		scalar @ARGV == 1 or
				mydie('exactly one argument (the snapshot name) should be specified');
		$snapshot_name = shift @ARGV;
		Cupt::System::Snapshots->new($config)->setup_config_for_snapshot($snapshot_name);
		$action = 'install';
	}

	local $| = 1;
	do {
		my @package_expressions_to_reinstall;
		if ($action eq 'reinstall') {
			$action = 'install';
			@package_expressions_to_reinstall = @ARGV;
		}

		# source packages give dramatic speed-up for synchronizing source versions
		my $o_build_source = ($action eq 'build-depends' or
				$config->get_string('cupt::resolver::synchronize-source-versions') ne 'none');

		print __('Building the package cache... ');
		build_cache(
				'-source' => $o_build_source,
				'-binary' => 1,
				'-installed' => 1,
				'-allow-reinstall' => \@package_expressions_to_reinstall
		);
		say __('[done]');
	};

	print __('Initializing package resolver and worker... ');
	my $resolver;
	do {
		my $resolver_module_name = defined $config->get_string('cupt::resolver::external-command') ?
				'Cupt::System::Resolvers::External' : 'Cupt::System::Resolvers::Native';
		eval "require $resolver_module_name;"; die $@ if $@; ## no critic (StringyEval)
		$resolver = $resolver_module_name->new($config, $cache);
	};
	$resolver->import_installed_versions($cache->get_system_state()->export_installed_versions());
	if (defined $snapshot_name) {
		Cupt::System::Snapshots->new($config)->setup_resolver_for_snapshot($snapshot_name, $resolver);
	}
	my $worker = Cupt::System::Worker->new($config, $cache);
	say __('[done]');

	print __('Scheduling requested actions... ');
	if ($action eq 'safe-upgrade' || $action eq 'full-upgrade') {
		if ($action eq 'safe-upgrade') {
			$config->set_scalar('cupt::resolver::no-remove' => 1);
		}
		$resolver->upgrade();

		# despite the main action is {safe,full}-upgrade, allow package
		# modifiers in the command line just as with the install command
		$action = 'install';
	}

	my $o_purge = 0;
	if ($action eq 'purge') {
		$action = 'remove';
		$config->set_scalar('cupt::worker::purge', 1)
	}

	my @package_expressions = @ARGV;
	foreach my $package_expression (@package_expressions) {
		if ($action eq 'satisfy') {
			$config->set_scalar('apt::install-recommends' => 0);
			$config->set_scalar('apt::install-suggests' => 0);

			my $method_name = 'satisfy_relation_expression';

			if ($package_expression =~ m/-$/) {
				$method_name = "un$method_name";
				$package_expression =~ s/-$//;
			}

			my $ref_relation_expressions = parse_architectured_relation_line($package_expression);
			$ref_relation_expressions = unarchitecture_relation_expressions(
					$ref_relation_expressions, $config->get_string('apt::architecture'));
			$resolver->$method_name($_) for @$ref_relation_expressions;
		} elsif ($action eq 'build-depends') {
			my $architecture = $config->get_string('apt::architecture');

			$config->set_scalar('apt::install-recommends' => 0);
			$config->set_scalar('apt::install-suggests' => 0);
			my $version = select_source_package_version($package_expression, 1);
			my @relation_expressions;

			push @relation_expressions, @{unarchitecture_relation_expressions(
					$version->build_depends, $architecture)};
			push @relation_expressions, @{unarchitecture_relation_expressions(
					$version->build_depends_indep, $architecture)};
			$resolver->satisfy_relation_expression($_) for @relation_expressions;

			@relation_expressions = ();
			push @relation_expressions, @{unarchitecture_relation_expressions(
					$version->build_conflicts, $architecture)};
			push @relation_expressions, @{unarchitecture_relation_expressions(
					$version->build_conflicts_indep, $architecture)};
			$resolver->unsatisfy_relation_expression($_) for @relation_expressions;

			$resolver->satisfy_relation_expression(Cupt::Cache::Relation->new('build-essential'));
		} else {
			my $action = $action; ## no critic (ReusedNames)
			my $actual_package_expression = $package_expression;
			my @versions = select_package_versions_wildcarded($actual_package_expression, 'binary', 0);
			if (not scalar @versions) {
				# we have a funny situation with package names like 'g++',
				# where one don't know is there simple package name or '+'/'-'
				# modifier at the end of package name, so we enter here only if
				# it seems that there is no such binary package

				# "localizing" action to make it modifiable by package modifiers
				my $last_letter = substr($package_expression, -1, 1);
				if ($last_letter eq '+') {
					$action = 'install';
					$package_expression = substr($package_expression, 0, length($package_expression)-1);
				} elsif ($last_letter eq '-') {
					$action = 'remove';
					$package_expression = substr($package_expression, 0, length($package_expression)-1);
				}
				$actual_package_expression = $package_expression;
			}

			if ($action eq 'install') {
				if (not scalar @versions) {
					@versions = select_package_versions_wildcarded($actual_package_expression, 'binary');
				}
				$resolver->install_version($_) foreach @versions;
			} else { # 'remove'
				if (not scalar @versions) {
					# retry, still non-fatal, to deal with packages in 'config-files' state
					@versions = select_package_versions_wildcarded($actual_package_expression, 'binary', 0);
				}

				if (scalar @versions) {
					for my $version (@versions) {
						$resolver->remove_package($version->package_name);
					}
				} else {
					check_package_name($package_expression);
					if (not defined $cache->get_system_state()->get_installed_info($package_expression) and
						not defined get_binary_package($package_expression, 0))
					{
						mydie("unable to find binary package/expression '%s'", $package_expression);
					}

					$resolver->remove_package($package_expression);
				}
			}
		}
	}

	say __('[done]');

	print __('Resolving possible unmet dependencies... ');
	my $resolve_result = $resolver->resolve(
			generate_management_prompt($worker, $o_show_versions, $o_show_size_changes)
	);
	say '';

	# at this stage resolver has done its work, so to does not consume the RAM
	undef $resolver;

	if (!defined $resolve_result) {
		# tryings were abandoned
		return 1;
	} elsif ($resolve_result) {
		# if some solution was found and user has accepted it
		my $download_progress = new_download_progress();
		say __('Performing requested actions:');
		my $overall_result = $worker->change_system($download_progress);
		mydie('unable to do requested actions') unless $overall_result;

		return 0;
	} else {
		say __('no more solutions.');
		return 1;
	}
	return;
}

sub dist_upgrade {
	do { # 1st stage: upgrading of package management tools
		say '[ upgrading package management tools ]';
		say '';
		local @ARGV = @ARGV;
		unshift @ARGV, 'dpkg', 'libcupt-perl', 'cupt';
		manage_packages('install') == 0 or
				mydie('upgrading of the package management tools failed');
	};

	do { # 2nd stage: full upgrade
		say '';
		say '[ upgrading the system ]';
		say '';
		exec($0, 'full-upgrade', grep { $_ ne 'dist-upgrade' } @original_arguments);
	};
}

sub update_release_data () {
	require Cupt::System::Worker;

	check_no_extra_args();
	check_no_extra_options();

	build_cache(-source => 0, -binary => 0, -installed => 0);

	my $download_progress = new_download_progress();
	my $worker = Cupt::System::Worker->new($config, $cache);

	return !$worker->update_release_and_index_data($download_progress);
}

sub show_changelog_or_copyright ($) {
	my ($target) = @_;
	# turn off info and relations parsing, we don't need it
	unless ($o_shell_mode) {
		$Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;
		$Cupt::Cache::BinaryVersion::o_no_parse_relations = 1;
	}

	my $o_installed_only = 0;
	GetOptions(
		'installed-only' => \$o_installed_only,
	);

	check_no_extra_options();

	scalar @ARGV or
			mydie('no package expressions specified');

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => !$o_installed_only, -installed => 1);

	my @versions = map { select_package_versions_wildcarded($_, 'binary') } @ARGV;

	foreach my $version (@versions) {
		my $local_target_path;
		if ($target eq 'changelog') {
			$local_target_path = Cupt::Cache::get_path_of_debian_changelog($version);
		} else {
			$local_target_path = Cupt::Cache::get_path_of_debian_copyright($version);
		}

		if (defined $local_target_path) {
			# there is a local changelog, display it
			my $prepare_command = $target eq 'changelog' ? 'zcat' : 'cat';
			my $result = system("$prepare_command $local_target_path | sensible-pager");
			# return non-zero code in case of some error
			return $result if $result;
		} else {
			my %base_uris_by_vendor = (
				'Debian' => 'http://packages.debian.org/changelogs/pool',
				'Ubuntu' => 'http://changelogs.ubuntu.com/changelogs/pool',
				# yes, 'changelogs' even for copyrights :)
			);

			my @matched_avail_entries;
			foreach my $ref_avail_entry (@{$version->available_as}) {
				if ($ref_avail_entry->{release}->{vendor} eq 'Debian') {
					# this is probably a package from Debian archive
					push @matched_avail_entries, $ref_avail_entry;
					last;
				}
				if ($ref_avail_entry->{release}->{vendor} eq 'Ubuntu') {
					# this is probably a package from Debian archive
					push @matched_avail_entries, $ref_avail_entry;
					last;
				}
			}

			my @target_uris;
			foreach my $ref_matched_avail_entry (@matched_avail_entries) {
				# all of above are only good guessings
				my $version_string = $version->source_version_string;
				my $source_package_name = $version->source_package_name;
				my $short_prefix = ($source_package_name =~ m/^lib/) ?
						substr($source_package_name, 0, 4) : substr($source_package_name, 0, 1);

				my $component = $ref_matched_avail_entry->{release}->{component};
				my $remote_target_base_uri = $base_uris_by_vendor{$ref_matched_avail_entry->{release}->{vendor}};
				my $uri_appendage = "$component/$short_prefix/$source_package_name/${source_package_name}_$version_string/$target";
				push @target_uris, "$remote_target_base_uri/$uri_appendage";
			}

			do { # temporary (I hope) hack to work around dropping epoch on Debian/Ubuntu web links
				my @fallback_uris;
				foreach my $target_uri (@target_uris) {
					(my $fallback_uri = $target_uri) =~ s/\d+://;
					if ($fallback_uri ne $target_uri) {
						push @fallback_uris, $fallback_uri;
					}
				}
				push @target_uris, @fallback_uris;
			};

			if (scalar @target_uris) {
				require Cupt::Download::Manager;

				my (undef, $temp_target_path) = tempfile();

				my $download_progress = new_download_progress();

				my $download_result;
				do {
					my $download_manager = Cupt::Download::Manager->new($config, $download_progress);
					$download_result = $download_manager->download(
							{
								'uri-entries' => [ map { { 'uri' => $_ } } @target_uris ],
								'filename' => $temp_target_path,
							}
					);
				}; # make sure that download manager is already destroyed at this point

				# fail and exit if it was something bad with downloading
				mydie($download_result) if $download_result;

				my $result = system("sensible-pager $temp_target_path");
				# remove the file
				unlink $temp_target_path or
						mydie("unable to delete file '%s': %s", $temp_target_path, $!);
				# return non-zero code in case of some error
				return $result if $result;
			} else {
				mydie("no info where to acquire %s for version '%s' of package '%s'",
						$target, $version->version_string, $version->package_name);
			}
		}
	}

	return 0;
}

sub screenshots ($) {
	# turn off info and relations parsing, we don't need it
	unless ($o_shell_mode) {
		local $Cupt::Cache::BinaryVersion::o_no_parse_info_onlys = 1;
		local $Cupt::Cache::BinaryVersion::o_no_parse_relations = 1;
	}

	check_no_extra_options();

	my @package_names = @ARGV;
	scalar @package_names or
			mydie('no package expressions specified');

	# we need to build binary-only cache for this operation
	build_cache(-source => 0, -binary => 1, -installed => 1);

	foreach my $package_name (@package_names) {
		# check for existence
		get_binary_package($package_name, 1);

		if (my $result = system("sensible-browser http://screenshots.debian.net/package/$package_name &")) {
			return $result;
		}
	}

	return 0;
}

sub clean_archives ($) {
	require Cupt::System::Worker;
	my ($leave_available) = @_;

	check_no_extra_options();
	check_no_extra_args();

	build_cache(-source => 0, -binary => 1, -installed => 0);

	my $worker = Cupt::System::Worker->new($config, $cache);
	my $size_deleted = 0;

	my $sub_callback = sub {
		my $path = $_[0];
		my $size = lstat($path)->size;
		$size_deleted += $size;
		say sprintf __("Deleting '%s' <%s>."), $path, human_readable_size_string($size);
	};
	$worker->clean_archives($sub_callback, $leave_available);
	say sprintf __('Freed %s of disk space.'), human_readable_size_string($size_deleted);

	return 0;
}

sub snapshot {
	my $action = shift @ARGV or
			mydie('the action is not specified');

	if ($action eq 'load') {
		return manage_packages('load-snapshot');
	} else {
		check_no_extra_options();
		require Cupt::System::Snapshots;
		my $snapshots = Cupt::System::Snapshots->new($config);

		given ($action) {
			when ('list') {
				check_no_extra_args();
				say join("\n", $snapshots->get_snapshot_names());
			}
			when ('remove') {
				my $snapshot_name = shift @ARGV or
						mydie('no snapshot name specified');
				check_no_extra_args();
				require Cupt::System::Worker;
				build_cache('-source' => 0, '-binary' => 0, '-installed' => 0);
				my $worker = Cupt::System::Worker->new($config, $cache);
				$worker->remove_system_snapshot($snapshots, $snapshot_name);
			}
			when ('rename') {
				my $previous_snapshot_name = shift @ARGV or
						mydie('no previous snapshot name specified');
				my $new_snapshot_name = shift @ARGV or
						mydie('no new snapshot name specified');
				check_no_extra_args();
				require Cupt::System::Worker;
				build_cache('-source' => 0, '-binary' => 0, '-installed' => 0);
				my $worker = Cupt::System::Worker->new($config, $cache);
				$worker->rename_system_snapshot($snapshots, $previous_snapshot_name => $new_snapshot_name);
			}
			when ('save') {
				my $snapshot_name = shift @ARGV or
						mydie('no snapshot name specified');
				check_no_extra_args();
				require Cupt::System::Worker;
				build_cache('-source' => 0, '-binary' => 0, '-installed' => 1);
				my $worker = Cupt::System::Worker->new($config, $cache);
				$worker->save_system_snapshot($snapshots, $snapshot_name);
			}
			default {
				mydie("unknown action for the 'snapshot' subcommand");
			}
		}
		return 0;
	}
}

sub process_options {
	# strip options out (for now)
	my @direct_options;
	eval {
		GetOptions(
			'option|o=s@' => \@direct_options,
			'important|i' => sub { $config->set_scalar('apt::cache::important', 1) },
			'all-versions|a' => sub { $config->set_scalar('apt::cache::allversions', 1) },
			'no-all-versions' => sub { $config->set_scalar('apt::cache::allversions', 0) },
			'target-release|default-release|t=s' => sub { $config->set_scalar('apt::default-release', $_[1]) },
			'recurse' => sub { $config->set_scalar('apt::cache::recursedepends', 1) },
			'simulate|s' => sub { $config->set_scalar('cupt::worker::simulate' => 1) },
			'purge' => sub { $config->set_scalar('cupt::worker::purge' => 1) },
			'quiet|q' => sub { $config->set_scalar('quiet' => 1) },
		);
		foreach (@direct_options) {
			if (m/(.*?)=(.*)/) {
				my ($key, $value) = ($1, $2);
				if ($key =~ m/^(.*?)::$/) {
					# this is list option
					$config->set_list($1, $value);
				} else {
					# regular option
					$config->set_scalar($key, $value);
				}
			} else {
				mydie("incorrect option syntax (right is '<option>=<value>')");
			}
		}
	};
	if (mycatch()) {
		myerr('error while processing command-line options');
		exit 2;
	}
}

sub shell {
	require Text::ParseWords;
	Text::ParseWords->import('shellwords');
	$o_shell_mode = 1;
	$Cupt::Cache::Package::o_memoize = 1;

	check_no_extra_options();
	check_no_extra_args();

	say __('This is an interactive shell of the cupt package manager.');

	my $sub_rebuild_cache = sub {
		local $| = 1;
		print __('Building the package cache... ');
		$cache = Cupt::Cache->new($config);
		say __('[done]');
	};

	$sub_rebuild_cache->();

	require Term::ReadLine;
	my $term = Term::ReadLine->new('cupt shell');
	my $prompt = 'cupt> ';
	while ( defined (my $line = $term->readline($prompt)) ) {
		chomp($line);
		$term->addhistory($line);
		last if $line =~ m/^(exit|quit|:q|q)$/; ## no critic (FixedStringMatches)

		if ($line =~ m/^\?+$/) { # if line consists only of '?' chars, shell behaves wierd...
			$line = 'help';
		}
		local @ARGV = shellwords($line) or
				do {
					mywarn('bad shell syntax');
					next;
				};

		my $o_simulate;
		do {
			local $config = $config->clone(); ## no critic (LocalVars)
			$cache->set_config($config);

			process_options();
			$o_simulate = $config->get_bool('cupt::worker::simulate');
			main();
		};

		my @safe_commands = qw(config-dump show showsrc search depends rdepends
				policy policysrc pkgnames changelog copyright screenshots
				source clean autoclean);
		if (defined $command and not $o_simulate and none { $_ eq $command } @safe_commands) {
			# the system could be modified, need to rebuild all
			build_config();
			$sub_rebuild_cache->();
		}
	}

	return 0;
}

