#!/usr/bin/perl

use strict;
use Dpkg::IPC;
use Debian::PkgJs::Banned;
use Debian::PkgJs::Cache;
use Debian::PkgJs::Dependencies;
use Debian::PkgJs::Semver;
use Debian::PkgJs::Utils;
use Debian::PkgJs::Version;
use Getopt::Long;
use JSON;
use Progress::Any '$progress';
use Progress::Any::Output;

Progress::Any::Output->set( 'TermProgressBarColor',
    template =>
'<color ffff00>%p%</color> <color 808000>[</color>%B<color 808000>]</color>'
);

my %opt;

GetOptions(
    \%opt, qw(
      h|help
      v|version
      audit
      install
      install-command
      ignore
      nolink|no-link
      regenerate
      prod
      all
      strict
    )
);

if ( $opt{h} ) {
    print <<EOF;
Install all dependencies of a JS project using Debian dependencies when
available.

Options:
 -h, --help: print this
 --install: launch install-command if some Debian packages are missing
 --ignore: ignore missing Debian packages
 --install-command: command to install mising packages. Default:
                    "--install-command 'sudo apt install'"
 --no-link: don't link JS modules from Debian directories
 --regenerate: force package-lock.json regeneration
 --prod: don't install dev dependencies
 --all: don't remove sub-dependencies of Debian packages (link them and
        install wanted dependencies)
 --strict: download JS module if Debian version mismatch (using semver)
 --audit: don't install or download anything, just print result
EOF
    exit;
}
elsif ( $opt{v} ) {
    print "$VERSION\n";
    exit;
}

$opt{'install-command'} ||= 'sudo apt install';

# Step 0: generate package-lock.json if needed

if ( $opt{regenerate} or not -e 'package-lock.json' ) {
    spawn(
        exec =>
          [qw(npm i --package-lock-only --legacy-peer-deps --ignore-scripts)],
        wait_child => 1
    );
}

# Step 1: read package-lock.json and dispatch modules into lists:
#          - Debian packages to install
#          - Debian JS modules to link
#          - JS modules to download

my $content;
{
    open my $f, 'package-lock.json' or die $!;
    local $/ = undef;
    $content = JSON::from_json(<$f>);
    close $f;
}

unless ( $content->{packages} ) {
    print STDERR
"Unable to fine 'packages' key in package-lock.json, use --regenerate option\n";
    exit 1;
}

my ( %toLink, %toInstall, %toDownload, %maybeToDownload );

my $ownPackageLock = { packages => {} };

# Reduce package-lock.json to the strict needed
unless ( $opt{all} ) {

    # Reduce package-lock.json to the needed dependencies only
    scan(
        {
            dependencies => {
                (
                    $content->{packages}->{""}->{dependencies}
                    ? ( %{ $content->{packages}->{""}->{dependencies} } )
                    : ()
                ),
                (
                    !$opt{prod} && $content->{packages}->{""}->{devDependencies}
                    ? ( %{ $content->{packages}->{""}->{devDependencies} } )
                    : ()
                )
            }
        },
        ''
    );

    $content = $ownPackageLock;
}

# Read package-lock.json content and populate:
#  - Debian package to install
#  - Debian modules to link
#  - JS module to download and install
M: foreach my $package (
    sort {
        my @_a = ( $a =~ m#(node_modules/)#g );
        my @_b = ( $b =~ m#(node_modules/)#g );
        @_a <=> @_b || $a cmp $b;
    } keys %{ $content->{packages} }
  )
{
    next unless $package;                            # Skip "" key
    next unless $content->{packages}->{$package};    # Skip deleted
    my $module = $package;
    while ( $module =~ s#.*?node_modules/## ) {
        if ( $module =~ m#(.*?)/node_modules/# ) {
            next M if $toLink{$1};
        }
    }
    $module =~ s#.*node_modules/##;
    if ( my $debianPackage = availableModules->{$module} ) {
        $toLink{$module}++;
        my $wantedVersion = $content->{packages}->{$package}->{version};
        unless ( installedModules->{$module} ) {
            push @{ $toInstall{$debianPackage} }, $module;
            push @{ $maybeToDownload{$module} },
              [
                {
                    dest => $package,
                    url  => $content->{packages}->{$package}->{resolved},
                    wantedVersion => $wantedVersion,
                }
              ];
        }
        elsif ( $opt{strict} ) {
            my $debianVersion = pjson( installedModules->{$module} )->{version};
            if (
                $wantedVersion
                and not( $debianVersion
                    and semver( $debianVersion, '^' . $wantedVersion ) )
              )
            {
                $toLink{$module}--;
                delete $toLink{$module} unless $toLink{$module};
                $toDownload{$package} =
                  $content->{packages}->{$package}->{resolved};
            }
        }
        delete $content->{packages}->{$package};
        delete $content->{dependencies}->{$module};
    }
    else {
        $toDownload{$package} = $content->{packages}->{$package}->{resolved};
    }
}


# Summary
print scalar(%toLink)
  . ' modules '
  . ( $opt{nolink} ? 'already availables' : 'to link' ) . "\n";
print scalar(%toDownload) . " modules to download\n";
if ($opt{audit}) {
    print scalar(%toInstall) . " packages to install\n" if %toInstall;
    print "Missing dependencies:\n";
    print join(' ',map { s#.*/##; $_ } keys %toDownload)."\n";
    exit;
}

# Step 2: download missing Debian packages if needed

if (%toInstall) {
    unless ( $opt{install} or $opt{ignore} ) {
        print STDERR "\nThe following packages are needed, choose one of "
          . "--ignore or --install\n"
          . join( ' ', sort keys %toInstall ) . "\n";
        exit 1;
    }
    if ( $opt{install} ) {
        print scalar(%toInstall) . " packages to install\n";
        spawn(
            exec => [
                'sh',
                '-c',
                $opt{'install-command'} . ' '
                  . join( ' ', sort keys %toInstall )
            ],
            wait_child => 1,
        );
    }
}

mkdir 'node_modules';

# Step 3: link Debian JS modules into node_modules if needed

# Reset cache
installedModules(1);
foreach my $module ( keys %toLink ) {
    if ( installedModules->{$module} ) {
        if ( $opt{strict} and $maybeToDownload{$module} ) {
            my $continue = 0;
            foreach my $tmp ( @{ $maybeToDownload{$module} } ) {
                my $debianVersion =
                  pjson( installedModules->{$module} )->{version};
                if (
                    $tmp->{wantedVersion}
                    and not( $debianVersion
                        and
                        semver( $debianVersion, '^' . $tmp->{wantedVersion} ) )
                  )
                {
                    $toDownload{ $tmp->{path} } = $tmp->{url};
                }
                else {
                    $continue = 1;
                }
            }
            next unless $continue;
        }
        unless ( $opt{nolink} ) {

            if ( $module =~ m#(.*)/# ) {
                mkdir "node_modules/$1";
            }
            spawn(
                exec       => [ 'rm', '-rf', "node_modules/$module" ],
                wait_child => 1
            );
            spawn(
                exec => [
                    'ln',                        '-s',
                    installedModules->{$module}, "node_modules/$module"
                ],
                wait_child => 1
            );
        }
    }
}

# Step 4: download missing

$progress->target( scalar %toDownload );

if (%toDownload) {
    print "Downloading...\n";
    foreach my $modulePath ( keys %toDownload ) {
        $progress->update( message => $modulePath );
        downloadAndInstall( $modulePath, $toDownload{$modulePath} );
    }
    $progress->finish();
    print "Done\n";
}

# Internal subroutine to reduce package-lock.json
my %seen;

sub scan {
    my ( $package, $offset ) = @_;
    return if $seen{$package};
    $seen{$package}++;
    return
      unless $package->{dependencies} and %{ $package->{dependencies} };
    foreach my $dep ( keys %{ $package->{dependencies} } ) {
        next if $ownPackageLock->{packages}->{"$offset/node_modules/$dep"};
        my $tmp      = $offset;
        my $continue = 1;
      D: do {
            if (    $continue
                and $content->{packages}->{"${tmp}node_modules/$dep"} )
            {
                $ownPackageLock->{packages}->{"${tmp}node_modules/$dep"} =
                  $content->{packages}->{"${tmp}node_modules/$dep"};
                scan( $content->{packages}->{"${tmp}node_modules/$dep"},
                    "${tmp}node_modules/$dep/" )
                  unless availableModules->{$dep};
                $continue = 0;
            }
        } while ( $tmp =~ s#node_modules/.*?$## );
    }
}
