The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package App::DuckPAN;
BEGIN {
  $App::DuckPAN::AUTHORITY = 'cpan:DDG';
}
# ABSTRACT: The DuckDuckGo DuckPAN client
$App::DuckPAN::VERSION = '0.165';
use feature 'state';

use Moo;
use MooX::Cmd;
use MooX::Options;
use App::DuckPAN::Config;
use File::Which;
use Class::Load ':all';
use HTTP::Request::Common qw( GET POST );
use HTTP::Status;
use List::Util qw( first max );
use LWP::UserAgent;
use LWP::Simple;
use Parse::CPAN::Packages::Fast;
use File::Temp qw/ :POSIX /;
use Term::ANSIColor;
use Term::UI;
use Term::ReadLine;
use Carp;
use Encode;
use Perl::Version;
use Path::Tiny;
use App::DuckPAN::Cmd::Help;

our $VERSION ||= '9.999';

option dukgo_login => (
	is => 'ro',
	lazy => 1,
	default => sub { 'https://duck.co/my/login' },
	doc => 'URI to log into community platform. defaults to "https://duck.co/my/login"',
);

option check => (
	is          => 'ro',
	lazy        => 1,
	negativable => 1,
	default     => sub { 1 },
	doc         => 'perform requirements checks. turn off with --no-check',
);

option 'empty' => (
	is          => 'ro',
	lazy        => 1,
	short       => 'e',
	default     => sub { 0 },
	doc         => 'empty duckpan cache at start-up',
);

has cachesec => (
	is      => 'ro',
	lazy    => 1,
	default => sub { 60 * 60 * 4 },    # 4 hours by default
);

option colors => (
	is          => 'ro',
	lazy        => 1,
	negativable => 1,
	default     => sub { 1 },
	doc         => 'use color output. turn off with --no-colors',
);

option verbose => (
	is      => 'ro',
	lazy    => 1,
	short   => 'v',
	default => sub { 0 },
	doc     => 'provide expanded output during operation',
);

has duckpan_packages => (
	is => 'ro',
	lazy => 1,
	builder => 1,
);

sub _build_duckpan_packages {
	my $self = shift;

	my $gz         = '02packages.details.txt.gz';
	my $package_url = join('/', $self->duckpan, 'modules', $gz);
	my $mirror_to   = $self->cfg->cache_path->child($gz);

	if (is_error(mirror($package_url, $mirror_to))) {
		$self->emit_and_exit(-1, "Cannot download $package_url");
	}

	return Parse::CPAN::Packages::Fast->new($mirror_to->stringify);
}

option duckpan => (
	is => 'ro',
	lazy => 1,
	default => sub { 'http://duckpan.org/' },
	doc => 'URI for the duckpan package server. defaults to "https://duckpan.org/"',
);

sub _ua_string {
  my ($self) = @_;
  my $class   = ref $self || $self;
  my $version = $class->VERSION;
  return "$class/$version";
}

option http_proxy => (
	is => 'ro',
	predicate => 1,
	doc => 'proxy to use for outbound HTTP requests',
);

option config => (
	is => 'ro',
	predicate => 1,
	doc => 'path to config directory. defaults to "~/.duckpan/config"',
);

option cache => (
	is => 'ro',
	predicate => 1,
	doc => 'path to cache directory. defaults to "~/.duckpan/cache"',
);

has term => (
	is => 'ro',
	lazy => 1,
	builder => '_build_term',
);

sub _build_term { Term::ReadLine->new('duckpan') }

has ia_types => (
	is => 'ro',
	lazy => 1,
	builder => '_build_ia_types',
);

sub _build_ia_types {
    my $ddg_path = path('lib', 'DDG');
    my $t_dir = path('t');
    return [{
            name      => 'Goodie',
            dir       => $ddg_path->child('Goodie'),
            supported => 1,
            templates => {
                code => {
                    in  => path('template', 'lib', 'DDG', 'Goodie', 'Example.pm'),
                    out => $ddg_path->child('Goodie')
                },
                test => {
                    in  => path('template', 't', 'Example.t'),
                    out => $t_dir
                },
            },
        },
        {
            name      => 'Spice',
            dir       => $ddg_path->child('Spice'),
            supported => 1,
            templates => {
                code => {
                    in  => path('template', 'lib', 'DDG', 'Spice', 'Example.pm'),
                    out => $ddg_path->child('Spice')
                },
                test => {
                    in  => path('template', 't', 'Example.t'),
                    out => $t_dir
                },
                handlebars => {
                    in  => path('template', 'share', 'spice', 'example', 'example.handlebars'),
                    out => path('share',    'spice')
                },
                js => {
                    in  => path('template', 'share', 'spice', 'example', 'example.js'),
                    out => path('share',    'spice')
                },
            },
        },
        {
            name      => 'Fathead',
            dir       => $ddg_path->child('Fathead'),
            supported => 0
        },
        {
            name      => 'Longtail',
            dir       => $ddg_path->child('Longtail'),
            supported => 0
        },
    ];
}

sub get_reply {
	my ( $self, $prompt, %params ) = @_;
	my $return = $self->term->get_reply( prompt => $prompt, %params );
	Encode::_utf8_on($return);
	return $return;
}

has http => (
	is => 'ro',
	builder => '_build_http',
	lazy => 1,
);

sub _build_http {
	my ( $self ) = @_;
	my $agent = LWP::UserAgent->new;
	$agent->agent($self->_ua_string);
	$agent->env_proxy;
	$agent->proxy( http => $self->http_proxy ) if $self->has_http_proxy;
	return $agent;
}

has server_hostname => (
	is => 'ro',
	lazy => 1,
	builder => 1,
);

sub _build_server_hostname { defined $ENV{APP_DUCKPAN_SERVER_HOSTNAME} ? $ENV{APP_DUCKPAN_SERVER_HOSTNAME} : 'duckduckgo.com' }

has cfg => (
	is => 'ro',
	lazy => 1,
	builder => 1,
	handles => [qw(
		config_path
		config_file
		set_config
		get_config
	)]
);

sub _build_cfg {
	my ( $self ) = @_;
	App::DuckPAN::Config->new(
		$self->has_config ? ( config_path => $self->config ) : (),
		$self->has_cache ? ( cache_path => $self->cache ) : (),
	);
}

has perl => (
	is => 'ro',
	builder => '_build_perl',
	lazy => 1,
);

sub _build_perl {
	load_class('App::DuckPAN::Perl');
	App::DuckPAN::Perl->new( app => shift );
}

has ddg => (
	is => 'ro',
	builder => '_build_ddg',
	lazy => 1,
);

sub _build_ddg {
	load_class('App::DuckPAN::DDG');
	App::DuckPAN::DDG->new( app => shift );
}

sub execute {
	my ( $self, $args, $chain ) = @_;
	my @arr_args = grep { $_ !~ /^-/} @{$args}; # Command line switches make it here, so we try to remove
	App::DuckPAN::Cmd::Help->run(1) if scalar @arr_args == 0;
	if (@arr_args) {
		my @modules;
		my @left_args;
		for (@arr_args) {
			if ($_ =~ /^www/i ||
				$_ =~ /^dist/i ||
				$_ =~ /^ddg/i ||
				$_ =~ /^app/i) {
				push @modules, $_;
			} elsif ($_ =~ m/^(duckpan|upgrade|update|reinstall)$/i) {
				$self->empty_cache unless $self->empty;
				push @modules, 'App::DuckPAN';
				push @modules, 'DDG' if $_ =~ /^(?:upgrade|reinstall)$/i;
				unshift @modules, 'reinstall' if lc($_) eq 'reinstall';
			} else {
				push @left_args, $_;
			}
		}
		exit $self->perl->duckpan_install(@modules) unless @left_args;
	}
	$self->emit_and_exit(0, "Unknown command. Use `duckpan help` to see the list of available DuckPAN commands.");
}

has standard_prefix_width => (
	is      => 'ro',
	default => sub { 9 },
);

sub _output_prefix {
	my ($self, $word, $color) = @_;

	my $extra_spaces = max(0, $self->standard_prefix_width - length($word) - 2 ); # 2 []s to be added.

	my $full_prefix = '[' . uc $word . ']' . (' ' x $extra_spaces);

	return ($self->colors) ? colored($full_prefix, $color) : $full_prefix;
}

sub emit_info {
	my ($self, @msg) = @_;

	$self->_print_msg(*STDOUT, '', @msg);
}

sub emit_error {
	my ($self, @msg) = @_;

	state $prefix = $self->_output_prefix('ERROR', 'red bold');

	$self->_print_msg(*STDERR, $prefix, @msg);
}

sub emit_and_exit {
	my ($self, $exit_code, @msg) = @_;

	state $prefix = $self->_output_prefix('FATAL', 'bright_red bold');

	if ($exit_code == 0) {      # This is just an info message.
		$self->emit_info(@msg);
	} else {                    # But if it's an unhappy exit
		$self->_print_msg(*STDERR, $prefix, @msg);
	}

	exit $exit_code;
}

sub emit_debug {
	my ($self, @msg) = @_;

	return unless $self->verbose;    # only show messages in verbose mode.

	return $self->_print_msg(*STDOUT, '', @msg);
}

sub emit_notice {
	my ($self, @msg) = @_;

	state $prefix = $self->_output_prefix('NOTICE', 'yellow bold');

	$self->_print_msg(*STDOUT, $prefix, @msg);
}

sub _print_msg {
	my ($self, $fh, $prefix, @lines) = @_;

	foreach my $line (map { $prefix . $_ } grep { $_ } @lines) {
		print $fh $line . "\n";
	}
}

sub camel_to_underscore {
 my ($self, $name) = @_;
	# Replace first capital by lowercase
	# if followed my lowercase.
	$name =~ s/^([A-Z])([a-z])/lc($1).$2/ge;
	# Substitute camelCase to camel_case
	$name =~ s/([a-z])([A-Z])/$1.'_'.lc($2)/ge;
	return lc $name;
}

sub phrase_to_camel {
 my ($self, $phrase) = @_;
	# Other things imply camelCase, we're going with CamelCase, kinda.
	return join('', map { ucfirst $_; } (split /\s+/, $phrase));
}

sub check_requirements {
	my ($self) = @_;

	if (!$self->check) {
		$self->emit_notice("Requirements checking was disabled...");
		return 1;
	}
	my $signal_file = $self->cfg->cache_path->child('perl_checked');
	my $last_checked_perl = ($signal_file->exists) ? $signal_file->stat->mtime : 0;
	if ((time - $last_checked_perl) <= $self->cachesec) {
		$self->emit_debug("Perl module versions recently checked, skipping requirements check...");
	} else {
		$self->emit_info("Checking for DuckPAN requirements...");

		$self->emit_and_exit(1, 'Requirements check failed')
		  unless ($self->check_perl && $self->check_app_duckpan && $self->check_ddg && $self->check_ssh && $self->check_git);
	}
	$signal_file->touch;

	return 1;
}

sub get_local_ddg_version {
	my ( $self ) = @_;
	return $self->perl->get_local_version('DDG');
}

sub get_local_app_duckpan_version {
	my ( $self ) = @_;
	return $self->perl->get_local_version('App::DuckPAN');
}


sub check_git {
	my ( $self ) = @_;
	my $ok = 0;
	$self->emit_info("Checking for git...");
	if (my $git = which('git')) {
		my $version_string = `$git --version`;
		if ($version_string =~ m/git version (\d+)\.(\d+)/) {
			if ($1 <= 1 && $2 < 7) {
				$self->emit_error("require minimum git 1.7");
			} else {
				$self->emit_debug($git);
				$ok = 1;
			}
		} else {
			$self->emit_error("Unknown git version!");
		}
	} else {
		$self->emit_error("git not found");
	}
	return $ok;
}

sub check_ssh {
	my ( $self ) = @_;
	my $ok = 0;
	$self->emit_info("Checking for ssh...");
	if (my $ssh = which('ssh')) {
		$self->emit_debug($ssh);
		$ok = 1;
	} else {
		$self->emit_error('ssh not found');
	}
	return $ok;
}

my %perl_versions = (
    required    => Perl::Version->new('v5.14'),
    recommended => Perl::Version->new('v5.16'),
);

sub check_perl {
	my ($self) = @_;

	$self->emit_info("Checking perl version... ");
	my $installed_version = Perl::Version->new($]);

	my $ok = 1;
	if ($installed_version->vcmp($perl_versions{required}) < 0) {
		$self->emit_error('perl ' . $perl_versions{required}->normal . ' or higher is required. ', $installed_version->normal . ' is installed.');
		$ok = 0;
	} elsif ($installed_version->vcmp($perl_versions{recommended}) < 0) {
		$self->emit_notice('perl ' . $perl_versions{recommended}->normal . ' or higher is recommended. ',
			$installed_version->normal . " is installed.");
	} else {
		$self->emit_debug($installed_version->normal);
	}

	return $ok;
}

sub check_app_duckpan {
	my ($self) = @_;
	my $ok                = 1;
	my $installed_version = $self->get_local_app_duckpan_version;
	return $ok if $installed_version && $installed_version == '9.999';
	$self->emit_info("Checking for latest App::DuckPAN... ");
	my $packages = $self->duckpan_packages;
	my $module   = $packages->package('App::DuckPAN');
	my $latest   = $self->duckpan . 'authors/id/' . $module->distribution->pathname;
	if ($installed_version && version->parse($installed_version) >= version->parse($module->version)) {
		my $msg = "App::DuckPAN version: $installed_version";
		$msg .= " (duckpan has " . $module->version . ")" if $installed_version ne $module->version;
		$self->emit_debug($msg);
	} else {
		my @msg = ("Please install the latest App::DuckPAN package with: duckpan upgrade");
		unshift @msg, "You have version " . $installed_version . ", latest is " . $module->version . "!" if ($installed_version);
		$self->emit_error(@msg);
		$ok = 0;
	}
	return $ok;
}

sub check_ddg {
	my ($self) = @_;
	my $ok                = 1;
	my $installed_version = $self->get_local_ddg_version;
	return $ok if $installed_version && $installed_version == '9.999';
	$self->emit_info("Checking for latest DDG Perl package...");
	my $packages = $self->duckpan_packages;
	my $module   = $packages->package('DDG');
	my $latest   = $self->duckpan . 'authors/id/' . $module->distribution->pathname;
	if ($installed_version && version->parse($installed_version) >= version->parse($module->version)) {
		my $msg = "DDG version: $installed_version";
		$msg .= " (duckpan has " . $module->version . ")" if $installed_version ne $module->version;
		$self->emit_debug($msg);
	} else {
		my @msg = ("Please install the latest DDG package with: duckpan DDG");
		if ($installed_version) {
			unshift @msg, "You have version " . $installed_version . ", latest is " . $module->version . "!";
		} else {
			unshift @msg, "You don't have DDG installed! Latest is " . $module->version . "!";
		}
		$self->emit_error(@msg);
		$ok = 0;
	}
	return $ok;
}

sub checking_dukgo_user {
	my ( $self, $user, $pass ) = @_;
	my $response = $self->http->request(POST($self->dukgo_login, Content => {
		username => $user,
		password => $pass,
	}));
	$response->code == 302 ? 1 : 0; # workaround, need something in dukgo
}

sub get_ia_type {
	my ($self) = @_;

	my $ia_type = first { $_->{dir}->is_dir } @{$self->ia_types};

	$self->emit_and_exit(-1, 'Must be run from the root of a checked-out Instant Answer repository.') unless ($ia_type);
	$self->emit_and_exit(-1, "Sorry, DuckPAN does not support " . $ia_type->{name} . " yet!") if $ia_type->{supported} == 0;

	return $ia_type;
}

sub empty_cache {
	my ($self) = @_;
	# Clear cache so share files are written into cache
	my $cache = $self->cfg->cache_path;
	$self->emit_info("Emptying DuckPAN cache...");
	$cache->remove_tree({keep_root => 1});
	$self->emit_info("DuckPAN cache emptied");
}

sub BUILD {
	my ($self) = @_;

	$self->emit_and_exit(1, 'We dont support Win32') if ($^O eq 'MSWin32');
	my $env_config = $self->cfg->config_path->child('env.ini');
	$self->empty_cache if $self->empty;
	if ($env_config->exists) {
		my $env = Config::INI::Reader->read_file($env_config);
		map { $ENV{$_} = $env->{'_'}{$_}; } keys %{$env->{'_'}} if $env->{'_'};
	}
}

1;

__END__

=pod

=head1 NAME

App::DuckPAN - The DuckDuckGo DuckPAN client

=head1 VERSION

version 0.165

=head1 DuckPAN

The DuckDuckHack Testing Tool

=head2 SYNPOSIS

DuckPAN is an application built to provide developers a testing environment for DuckDuckHack Instant Answers. It allows you to test instant answer triggers and preview their visual design and output.

C<duckpan help> provides more detailed information.

=head1 SEE ALSO

=over 4

=item L<https://github.com/duckduckgo/p5-app-duckpan/>

=item L<https://github.com/duckduckgo/>

=item L<https://duckduckgo.com/>

=item L<https://duck.co/>

=item L<http://duckpan.org/>

=back

=head1 CONTRIBUTION

To contribute to DuckPAN, please visit L<https://github.com/duckduckgo/p5-app-duckpan>. We also welcome and encourage contributions from our community. Please visit L<http://duckduckhack.com/> to contribute new instant answers, or visit L<https://duck.co/ideas> to share your ideas and instant answer source suggestions.

=head1 SUPPORT

B<IRC>:

    We invite you to join us at B<#duckduckgo> on B<irc.freenode.net> for any queries and lively discussion.

B<Repository>:

    L<https://github.com/duckduckgo/p5-app-duckpan>

B<Issue Tracker>:

    L<https://github.com/duckduckgo/p5-app-duckpan/issues>

=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