package Debian::PkgJs::AutoTransform;

use strict;
use Debian::PkgJs::Utils;
use Dpkg::IPC;
use Exporter 'import';
use File::Copy;
use JSON;

use constant BUNDLE => './dhnodejsBundle.cjs';

our @EXPORT = ('run');

sub run {
    copy('package.json', '.mjs2cjs-package.json');
    my $pkg = pjson(main_package) or die 'Unable to open package.json';
    die "Package isn't 'type:module', aborting"
      unless $pkg->{type} eq 'module';
    my $entryPoint = $ENV{ENTRY_POINT} || $pkg->{module} || $pkg->{exports};
    $entryPoint = './index.js' if !$entryPoint && -e 'index.js';
    die 'Unable to find entry point. Set it in env vars (ENTRY_POINT)'
      if ref $entryPoint;
    my @links;
    foreach my $type ( grep { $pkg->{$_} } (qw(dependencies peerDependencies devDependencies)) )
    {
        foreach
          my $dep ( grep { !-e "node_modules/$_" } keys %{ $pkg->{$type} } )
        {
            eval { ln($dep); };
            push @links, $dep unless $@;
        }
    }
    spawn(
        exec       => [ qw(mjs2cjs -b -o ), BUNDLE, $entryPoint ],
        wait_child => 1,
    );
    spawn(
        exec => [qw(perl -i -pe ), q{s/(["'])node:/$1/g}, BUNDLE],
        wait_child => 1,
    );
    foreach (@links) {
        unlink "node_modules/$_";
        rmdir "node_modules/$_" if s#/.*?$##;
    }

    # Transform package.json
    if ( $pkg->{files} ) {
        push @{ $pkg->{files} }, BUNDLE;
    }
    $pkg->{module} ||= $entryPoint;
    my $mainCjs = BUNDLE;
    if ( -e 'debian/index.cjs' ) {
        copy('debian/index.cjs','index.cjs');
        push @{ $pkg->{files} }, 'index.cjs';
        $mainCjs = './index.cjs';
    }
    $pkg->{exports} = _inject_require( $pkg->{exports}, $mainCjs );

    open PKG, '>', 'package.json' or die $?;
    print PKG JSON->new->pretty->canonical->encode($pkg);
    close PKG;
}

# Add a CommonJS "require" condition (pointing to $mainCjs) to an ESM package's
# "exports" and return the new value.
#
# package.json is serialized with the "canonical" flag (keys sorted
# alphabetically) and Node resolves export conditions in key order, first match
# wins. The conditions Node uses for require() are node-addons, node, require
# and default; all of node-addons, node and default sort *before* "require", so
# a top-level entry among them (which, for an ESM package, points to the ES
# module) would be matched first and shadow the CommonJS bundle, making
# require('pkg') return the ESM ({ __esModule, default }) instead of the bundle.
#
# To keep require() reachable we move those conditions out of its way: a lone
# "default" is simply renamed to "import" (which only matches import()), any
# other combination is nested under "import" so import() resolution is fully
# preserved while require() falls through to the bundle.
sub _inject_require {
    my ( $exports, $mainCjs ) = @_;
    $exports = { import => $exports } unless ref $exports eq 'HASH';

    # Find the conditions object the bundle must be attached to. For a subpath
    # map (keys starting with ".") that is the package root ".".
    my $conds = $exports;
    if ( grep { m#^\.# } keys %$conds ) {
        die "mjs2cjs: \"exports\" has no \".\" entry to attach the bundle to\n"
          unless exists $conds->{'.'};
        $conds->{'.'} = { import => $conds->{'.'} }
          unless ref $conds->{'.'} eq 'HASH';
        $conds = $conds->{'.'};
    }

    my @shadow = grep { exists $conds->{$_} } qw(default node node-addons);
    if ( @shadow == 1 and $shadow[0] eq 'default' ) {
        my $default = delete $conds->{default};
        $conds->{import} //= $default;
    }
    elsif (@shadow) {
        %$conds = ( import => {%$conds} );
    }
    $conds->{require} = $mainCjs;
    return $exports;
}

1;
