package App::ModuleBuildTiny::Dist;

use 5.014;
use warnings;
our $VERSION = '0.050';

use CPAN::Meta;
use Config;
use Encode qw/encode_utf8 decode_utf8/;
use File::Basename qw/basename dirname/;
use File::Copy qw/copy/;
use File::Path qw/mkpath rmtree/;
use File::Spec::Functions qw/catfile catdir rel2abs/;
use File::Slurper qw/write_text read_text read_binary/;
use File::chdir;
use ExtUtils::Manifest qw/manifind maniskip maniread/;
use Module::Runtime 'require_module';
use Module::Metadata 1.000037;
use Pod::Escapes qw/e2char/;
use Pod::Simple::Text 3.23;
use POSIX 'strftime';
use Text::ParseWords 'shellwords';

use Env qw/@PERL5LIB @PATH/;

my $Build = $^O eq 'MSWin32' ? 'Build' : './Build';

sub find {
	my ($re, @dir) = @_;
	my $ret;
	File::Find::find(sub { $ret++ if /$re/ }, @dir);
	return $ret;
}

sub mbt_version {
	return -f 'dynamic-prereqs.yml' ? '0.048' : '0.039';
}

sub prereqs_for {
	my ($meta, $phase, $type, $module, $default) = @_;
	return $meta->effective_prereqs->requirements_for($phase, $type)->requirements_for_module($module) // $default // 0;
}

sub uptodate {
	my ($destination, @source) = @_;
	return if not -e $destination;
	for my $source (grep { defined && -e } @source) {
		return if -M $destination > -M $source;
	}
	return 1;
}

sub distfilename {
	my $distname = shift;
	return catfile('lib', split /-/, $distname) . '.pm';
}

sub generate_readme {
	my $distname = shift;
	my $filename = distfilename($distname);
	die "Main module file $filename doesn't exist\n" if not -f $filename;
	my $parser = Pod::Simple::Text->new;
	$parser->output_string( \my $content );
	$parser->parse_characters(1);
	$parser->parse_file($filename);
	return decode_utf8($content);
}

sub load_jsonyaml {
	my $file = shift;
	require Parse::CPAN::Meta;
	return Parse::CPAN::Meta->load_file($file);
}

sub load_mergedata {
	my $mergefile = shift;
	if (defined $mergefile and -r $mergefile) {
		return load_jsonyaml($mergefile);
	}
	return;
}

sub distname {
	my $extra = shift;
	return delete $extra->{name} if defined $extra->{name};
	return basename(rel2abs('.')) =~ s/ (?: ^ (?: perl|p5 ) - | [\-\.]pm $ )//xr;
}

sub detect_license {
	my ($data, $filename, $authors, $mergedata) = @_;
	if ($mergedata->{license} && @{$mergedata->{license}} == 1) {
		require Software::LicenseUtils;
		Software::LicenseUtils->VERSION(0.103014);
		my $spec_version = $mergedata->{'meta-spec'} && $mergedata->{'meta-spec'}{version} ? $mergedata->{'meta-spec'}{version} : 2;
		my @guess = Software::LicenseUtils->guess_license_from_meta_key($mergedata->{license}[0], $spec_version);
		die "Couldn't parse license from metamerge: @guess\n" if @guess > 1;
		if (@guess) {
			my $class = $guess[0];
			require_module($class);
			return $class->new({holder => join(', ', @{$authors})});
		}
	}
	my (@license_sections) = grep { /licen[cs]e|licensing|copyright|legal|authors?\b/i } $data->pod_inside;
	for my $license_section (@license_sections) {
		next unless defined ( my $license_pod = $data->pod($license_section) );
		require Software::LicenseUtils;
		Software::LicenseUtils->VERSION(0.103014);
		my $content = "=head1 LICENSE\n" . $license_pod;
		my @guess = Software::LicenseUtils->guess_license_from_pod($content);
		next if not @guess;
		die "Couldn't parse license from $license_section in $filename: @guess\n" if @guess != 1;
		my $class = $guess[0];
		my ($year) = $license_pod =~ /.*? copyright .*? ([\d\-]+)/;
		require_module($class);
		return $class->new({holder => join(', ', @{$authors}), year => $year});
	}
	die "No license found in $filename\n";
}

sub get_changes {
	my $self = shift;
	my $version = quotemeta $self->meta->version;
	open my $changes, '<:raw', 'Changes' or die "Couldn't open Changes file";
	my (undef, @content) = grep { / ^ $version (?:-TRIAL)? (?:\s+|$) /x ... /^\S/ } <$changes>;
	pop @content while @content && $content[-1] =~ / ^ (?: \S | \s* $ ) /x;
	return @content;
}

sub preflight_check {
	my ($self, %opts) = @_;

	die "Changes appears to be empty\n" if not $self->get_changes;

	my $meta_version = $self->{meta}->version;
	die "Version is still zero\n" if $meta_version eq '0.000';

	die "Abstract is not set\n" if $self->{meta}->abstract eq 'INSERT YOUR ABSTRACT HERE';

	if ($opts{tag}) {
		require Git::Wrapper;
		my $git = Git::Wrapper->new('.');

		die "Dirty state in repository\n" if $git->status->is_dirty;
		die "Tag v$meta_version already exists\n" if eval { $git->rev_parse({ quiet => 1, verify => 1}, "v$meta_version") };
	}

	my $module_name = $self->{meta}->name =~ s/-/::/gr;
	my $detected_version = $self->{data}->version($module_name);
	die sprintf "Version mismatch between module and meta, did you forgot to run regenerate? (%s versus %s)\n", $detected_version, $meta_version if $detected_version != $meta_version;
}

sub scan_files {
	my ($files, $omit, %extra_args) = @_;
	my $combined = CPAN::Meta::Requirements->new;
	require Perl::PrereqScanner;
	my $scanner = Perl::PrereqScanner->new(\%extra_args);
	for my $file (@{$files}) {
		my $prereqs = $scanner->scan_file($file);
		$combined->add_requirements($prereqs);
	}
	$combined->clear_requirement($_) for @{$omit};
	return $combined
}

sub _scan_prereqs {
	my ($omit, %opts) = @_;
	my (@runtime_files, @test_files, @planner_files);
	File::Find::find(sub { push @runtime_files, $File::Find::name if -f && /\.pm$/ }, 'lib') if -d 'lib';
	File::Find::find(sub { push @runtime_files, $File::Find::name if -f }, 'script') if -d 'script';
	File::Find::find(sub { push @test_files, $File::Find::name if -f && /\.(t|pm)$/ }, 't') if -d 't';

	my $runtime = scan_files(\@runtime_files, $omit);
	my $test = scan_files(\@test_files, $omit);

	my %prereqs = (
		runtime => { requires => $runtime->as_string_hash },
		test    => { requires => $test->as_string_hash },
	);

	if (-d 'planner') {
		File::Find::find(sub { push @planner_files, $File::Find::name if -f && /\.pl$/ }, 'planner');
		require Perl::PrereqScanner::Scanner::DistBuild;
		my $configure = scan_files(\@planner_files, ['strict', 'warnings', @{$omit}], extra_scanners => [ 'DistBuild' ]);
		$configure->add_minimum('Dist::Build' => '0.003');
		$prereqs{configure} = { requires => $configure->as_string_hash };
	} else {
		$prereqs{configure} = { requires => { 'Module::Build::Tiny' => mbt_version() } };
	}

	my $prereqs = CPAN::Meta::Prereqs->new(\%prereqs);
	require CPAN::Meta::Prereqs::Filter;
	return CPAN::Meta::Prereqs::Filter::filter_prereqs($prereqs, %opts);
}


sub scan_prereqs {
	my ($self, %opts) = @_;
	my @omit = (@{ $opts{omit} // [] }, keys %{ $self->{meta}->provides });
	return _scan_prereqs(\@omit, %opts);
}

sub load_prereqs {
	my ($provides, %opts) = @_;
	my @prereqs;
	if (-f 'prereqs.json') {
		push @prereqs, load_jsonyaml('prereqs.json');
	}
	if (-f 'prereqs.yml') {
		push @prereqs, load_jsonyaml('prereqs.yml');
	}
	if (-f 'cpanfile') {
		require Module::CPANfile;
		push @prereqs, Module::CPANfile->load('cpanfile')->prereq_specs;
	}
	if ($opts{scan}) {
		push @prereqs, _scan_prereqs([ keys %{$provides} ])->as_string_hash;
	}

	if (@prereqs == 1) {
		return $prereqs[0];
	}
	elsif (@prereqs == 0) {
		return {};
	}
	else {
		@prereqs = map { CPAN::Meta::Prereqs->new($_) } @prereqs;
		my $prereqs = $prereqs[0]->with_merged_prereqs([ @prereqs[1..$#prereqs] ]);
		$prereqs->as_string_hash;
	}
}

sub new {
	my ($class, %opts) = @_;
	my $mergefile = $opts{mergefile} // (grep { -f } qw/metamerge.json metamerge.yml/)[0];
	my $mergedata = load_mergedata($mergefile) // {};
	my $distname = distname($mergedata);
	my $filename = distfilename($distname);
	my $podname = $filename =~ s/\.pm$/.pod/r;

	my $data = Module::Metadata->new_from_file($filename, collect_pod => 1, decode_pod => 1) or die "Couldn't analyse $filename: $!";
	my $pod_data = -e $podname && Module::Metadata->new_from_file($podname, collect_pod => 1, decode_pod => 1) // $data;
	my @authors = map { s/E<([^>]+)>/e2char($1)/ge; m/ \A \s* (.+?) \s* \z /x } grep { /\S/ } split /\n/, $pod_data->pod('AUTHOR') // $pod_data->pod('AUTHORS') // '' or warn "Could not parse any authors from `=head1 AUTHOR` in $filename";
	my $license = detect_license($pod_data, $filename, \@authors, $mergedata);

	my $load_meta = !%{ $opts{regenerate} // {} } && uptodate('META.json', 'cpanfile', 'prereqs.json', 'prereqs.yml', $mergefile);

	my $mode = -d 'planner' ? 'DB' : 'MBT';
	my $meta = $load_meta ? CPAN::Meta->load_file('META.json', { lazy_validation => 0 }) : do {
		my ($abstract) = ($pod_data->pod('NAME') // '')  =~ / \A \s+ \S+ \s? - \s? (.+?) \s* \z /x or warn "Could not parse abstract from `=head1 NAME` in $filename";
		my $version = $data->version($data->name) // die "Cannot parse \$VERSION from $filename";

		my $provides = Module::Metadata->provides(version => 2, dir => 'lib');
		my $prereqs = load_prereqs($provides, %opts);
		if ($mode eq 'MBT') {
			$prereqs->{configure}{requires}{'Module::Build::Tiny'} //= mbt_version();
		} else {
			$prereqs->{configure}{requires}{'Dist::Build'} //= '0.003';
		}
		$prereqs->{develop}{requires}{'App::ModuleBuildTiny'} //= $VERSION;
		my %resources = $class->generate_resources(%opts);

		my $metahash = {
			name                => $distname,
			version             => $version->stringify,
			author              => \@authors,
			abstract            => $abstract,
			dynamic_config      => 0,
			license             => [ $license->meta2_name ],
			prereqs             => $prereqs,
			release_status      => $opts{trial} // $version =~ /_/ ? 'testing' : 'stable',
			generated_by        => "App::ModuleBuildTiny version $VERSION",
			'meta-spec'         => {
				version         => 2,
				url             => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec'
			},
			(resources          => \%resources) x!! %resources,
			x_spdx_expression   => $license->spdx_expression,
			x_generated_by_perl => "$^V",
		};

		if (-e 'dynamic-prereqs.yml') {
			my $dynamic_prereqs = load_jsonyaml('dynamic-prereqs.yml');
			for my $expression (@{ $dynamic_prereqs->{expressions}}) {
				$expression->{condition} = [ shellwords($expression->{condition}) ] unless ref $expression->{condition};
				if (not ref $expression->{prereqs}) {
					my ($module, $version) = shellwords($expression->{prereqs});
					$version //= '0';
					$expression->{prereqs} = { $module => $version };
				}
			}
			$metahash->{x_dynamic_prereqs} = $dynamic_prereqs;
		}
		if (%{$mergedata}) {
			require CPAN::Meta::Merge;
			$metahash = CPAN::Meta::Merge->new(default_version => '2')->merge($metahash, $mergedata);
		}
		if ($metahash->{x_dynamic_prereqs}) {
			$metahash->{dynamic_config} = 1;
			$metahash->{prereqs}{configure}{requires}{'CPAN::Requirements::Dynamic'} //= '0.002' if $mode eq 'MBT';
		}

		# this avoids a long-standing CPAN.pm bug that incorrectly merges runtime and
		# "build" (build+test) requirements by ensuring requirements stay unified
		# across all three phases
		require CPAN::Meta::Prereqs::Filter;
		my $filtered = CPAN::Meta::Prereqs::Filter::filter_prereqs(CPAN::Meta::Prereqs->new($metahash->{prereqs}), sanitize => 1);
		my $merged_prereqs = $filtered->merged_requirements([qw/runtime build test/], ['requires']);
		my %seen;
		for my $phase (qw/runtime build test/) {
			my $requirements = $filtered->requirements_for($phase, 'requires');
			for my $module ($requirements->required_modules) {
				$requirements->clear_requirement($module);
				next if $seen{$module}++;
				my $module_requirement = $merged_prereqs->requirements_for_module($module);
				$requirements->add_string_requirement($module => $module_requirement);
			}
		}
		$metahash->{prereqs} = $filtered->as_string_hash;

		$metahash->{provides} //= $provides if not $metahash->{no_index};
		CPAN::Meta->create($metahash, { lazy_validation => 0 });
	};

	my %files;
	if (not $opts{regenerate}{MANIFEST} and -r 'MANIFEST') {
		%files = %{ maniread() };
	}
	else {
		my $maniskip = maniskip;
		%files = %{ manifind() };
		delete $files{$_} for grep { $maniskip->($_) } keys %files;
	}
	delete $files{$_} for keys %{ $opts{regenerate} };
	
	my $dist_name = $meta->name;
	$files{'Build.PL'} //= do {
		my $minimum_perl = prereqs_for($meta, qw/runtime requires perl 5.008/);
		my $header = "# This Build.PL for $dist_name was generated by mbtiny $VERSION.";
		if ($mode eq 'MBT') {
			my $minimum_mbt = prereqs_for($meta, qw/configure requires Module::Build::Tiny/);
			"$header\nuse $minimum_perl;\nuse Module::Build::Tiny $minimum_mbt;\nBuild_PL();\n";
		} else {
			my $minimum_db = prereqs_for($meta, qw/configure requires Dist::Build/);
			"$header\nuse $minimum_perl;\nuse Dist::Build $minimum_db;\nBuild_PL(\\\@ARGV, \\\%ENV);\n";
		}
	};
	{
		local $ENV{CPAN_META_JSON_BACKEND} = JSON::MaybeXS::JSON();
		$files{'META.json'} //= $meta->as_string;
		$files{'META.yml'} //= $meta->as_string({ version => 1.4 });
	}
	$files{LICENSE} //= $license->fulltext;
	$files{README} //= generate_readme($dist_name);
	if ($opts{regenerate}{Changes}) {
		my $time = strftime("%Y-%m-%d %H:%M:%S%z", localtime);
		my $header = sprintf "%-9s %s\n", $meta->version, $time;
		$files{Changes} = read_text('Changes') =~ s/(?<=\n\n)/$header/er;
	}
	# This must come last
	$files{MANIFEST} //= join '', map { "$_\n" } sort keys %files;

	return bless {
		files => \%files,
		meta  => $meta,
		license => $license,
		data => $data,
	}, $class
}


sub generate_resources {
	my ($class, %opts) = @_;
	my %result;

	if ($opts{add_repository}) {
		require Git::Wrapper;
		my $git = Git::Wrapper->new('.');
		my ($origin) = $git->remote('get-url' => 'origin');
		if ($origin =~ m{https://github.com/([\w.-]+)/([\w.-]+).git}) {
			$result{repository} = {
				type => 'git',
				web  => "https://github.com/$1/$2",
				url  => $origin,
			};
		} elsif ($origin =~ m{git\@github.com:([\w.-]+)/([\w.-]+).git}) {
			$result{repository} = {
				type => 'git',
				web  => "https://github.com/$1/$2",
				url  => "https://github.com/$1/$2.git",
			};
		} elsif ($origin =~ m{^https?://}) {
			$result{repository} = {
				type => 'git',
				url  => $origin,
			};
		}
	}

	if ($opts{add_bugtracker}) {
		require Git::Wrapper;
		my $git = Git::Wrapper->new('.');
		my ($origin) = $git->remote('get-url' => 'origin');
		if ($origin =~ m{https://github.com/([\w.-]+)/([\w.-]+).git}) {
			$result{bugtracker} = {
				web  => "https://github.com/$1/$2/issues",
			};
		} elsif ($origin =~ m{git\@github.com:([\w.-]+)/([\w.-]+).git}) {
			$result{bugtracker} = {
				web  => "https://github.com/$1/$2/issues",
			};
		}
	}
	return %result;
}

sub write_dir {
	my ($self, $dir, $verbose) = @_;
	mkpath($dir, $verbose, oct '755');
	my $files = $self->{files};
	for my $filename (keys %{$files}) {
		my $target = "$dir/$filename";
		mkpath(dirname($target)) if not -d dirname($target);
		if ($files->{$filename}) {
			write_text($target, $files->{$filename});
		}
		else {
			copy($filename, $target);
		}
	}
}

sub write_tarball {
	my ($self, $name) = @_;
	require Archive::Tar;
	my $arch = Archive::Tar->new;
	for my $filename ($self->files) {
		$arch->add_data($filename, $self->get_file($filename), { mode => oct '0644'} );
	}
	$arch->write($name, &Archive::Tar::COMPRESS_GZIP, $name =~ s/.tar.gz$//r);
	return $name;
}

sub files {
	my $self = shift;
	return keys %{ $self->{files} };
}

sub get_file {
	my ($self, $filename) = @_;
	return if not exists $self->{files}{$filename};
	my $raw = $self->{files}{$filename};
	return $raw ? encode_utf8($raw) : read_binary($filename);
}

sub run {
	my ($self, %opts) = @_;
	require File::Temp;
	my $dir  = File::Temp::tempdir(CLEANUP => 1);
	$self->write_dir($dir, $opts{verbose});
	local $CWD = $dir;
	my $ret = !!1;
	if ($opts{build}) {
		system $Config{perlpath}, 'Build.PL';
		system $Config{perlpath}, 'Build';
		my @extralib = map { rel2abs("blib/$_") } 'arch', 'lib';
		local @PERL5LIB = (@extralib, @PERL5LIB);
		local @PATH = (rel2abs(catdir('blib', 'script')), @PATH);
		for my $command (@{ $opts{commands} }) {
			say join ' ', @{$command} if $opts{verbose};
			$ret &&= not system @{$command};
		}
	}
	else {
		for my $command (@{ $opts{commands} }) {
			say join ' ', @{$command} if $opts{verbose};
			$ret &&= not system @{$command};
		}
	}
	return $ret;
}

for my $method (qw/meta license/) {
	no strict 'refs';
	*$method = sub { my $self = shift; return $self->{$method}; };
}

for my $method (qw/name version release_status/) {
	no strict 'refs';
	*$method = sub { my $self = shift; return $self->{meta}->$method; }
}

sub fullname {
	my $self = shift;
	my $trial = $self->release_status eq 'testing' && $self->version !~ /_/;
	return $self->meta->name . '-' . $self->meta->version . ($trial ? '-TRIAL' : '' );
}

sub archivename {
	my $self = shift;
	return $self->fullname . '.tar.gz';
}

1;
