### ----------------------------------------------------
### -- zef
### -- Licenses: Artistic-2.0
### -- Authors: Nick Logan, Tony O'Dell
### -- File: lib/Zef/Client.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 2
 {
            self.logger.emit({
                level   => INFO,
                stage   => INSTALL,
                phase   => BEFORE,
                candi   => $candi,
                message => "Installing: {$candi.dist.?identity // $candi.as}",
            });

            for @curs -> $cur {
                KEEP self.logger.emit({
                    level   => VERBOSE,
                    stage   => INSTALL,
                    phase   => AFTER,
                    candi   => $candi,
                    message => "Install [OK] for {$candi.dist.?identity // $candi.as}",
                });

                CATCH {
                    when /'already installed'/ {
                        self.logger.emit({
                            level   => INFO,
                            stage   => INSTALL,
                            phase   => AFTER,
                            candi   => $candi,
                            message => "Install [SKIP] for {$candi.dist.?identity // $candi.as}: {$_}",
                        });
                    }
                    default {
                        self.logger.emit({
                            level   => ERROR,
                            stage   => INSTALL,
                            phase   => AFTER,
                            candi   => $candi,
                            message => "Install [FAIL] for {$candi.dist.?identity // $candi.as}: {$_}",
                        });
                        $_.rethrow;
                    }
                }

                take $candi if $!installer.install($candi, :$cur, :force($!force-install), :precompile($!precompile-install), :$!logger, :timeout($!install-timeout));
            }
        }

        return @installed;
    }

    # Note that since adding CURS support some of the functions and apis feel
    # a little odd now. For now we try to maintain the original output and
    # design expectations as best we can, but in the next version of zef
    # we'll design more around CURFS and less around CURI. For instance
    # this 'deploy' method takes @candidates although we only use it for
    # displaying what will be installed - since we copy $curs to the install
    # target we don't need to know the individual candidates (and indeed we
    # can get them from the repository itself).
    #| Deploy all distributions installed to staging to a given repository
    method deploy(:$curs, *@candidates ($, *@) --> Array[Candidate]) is implementation-detail {
        for @candidates -> $candi {
            self.logger.emit({
                level   => INFO,
                stage   => INSTALL,
                phase   => BEFORE,
                candi   => $candi,
                message => "Installing: {$candi.dist.?identity // $candi.as}",
            });
        }

        {
            CATCH {
                default {
                    for @candidates -> $candi {
                        self.logger.emit({
                            level   => ERROR,
                            stage   => INSTALL,
                            phase   => AFTER,
                            candi   => $candi,
                            message => "Install [FAIL] for {$candi.dist.?identity // $candi.as}: {$_}",
                        });
                    }
                    $_.rethrow;
                }
            }
            $curs.deploy();
        }

        for @candidates -> $candi {
            self.logger.emit({
                level   => VERBOSE,
                stage   => INSTALL,
                phase   => AFTER,
                candi   => $candi,
                message => "Install [OK] for {$candi.dist.?identity // $candi.as}",
            });
        }

        return Array[Candidate].new(@candidates);
    }

    #| This organizes and executes multiples phases for multiples candidates (test/build/install/etc)
    method make-install(
        CompUnit::Repository :@to!, # target CompUnit::Repository
        Bool :$fetch = True,        # try fetching whats missing
        Bool :$build = True,        # run Build.rakumod (DEPRECATED..?)
        Bool :$test  = True,        # run tests
        Bool :$dry,                 # do everything *but* actually install
        Bool :$serial,
        *@candidates ($, *@),
        *%_
    ) {
        # Allowing multiple install targets complicates things too much.
        # TODO: remove this ability from the code everywhere.
        if @to.elems > 1 {
            die "Installing to multiple repos is no longer supported."
        }

        my @curs = @to.grep: -> $cur {
            UNDO {
                self.logger.emit({
                    level   => WARN,
                    stage   => INSTALL,
                    phase   => BEFORE,
                    message => "CompUnit::Repository install target is not writeable/installable: {$cur}"
                });
            }
            KEEP {
                self.logger.emit({
                    level   => TRACE,
                    stage   => INSTALL,
                    phase   => BEFORE,
                    message => "CompUnit::Repository install target is valid: {$cur}"
                });
            }
            $cur.?can-install || next();
        }
        die "Need a valid installation target to continue" unless ?$dry || +@curs;

        my $stage-for-repo = CompUnit::RepositoryRegistry.repository-for-name(@curs.head.?name // '');

        # XXX: Each loop block below essentially represents a phase, so they will probably
        # be moved into their own method/module related directly to their phase. For now
        # lumping them here allows us to easily move functionality between phases until we
        # find the perfect balance/structure.
        die "Must specify something to install" unless +@candidates;

        # Fetch Stage:
        # Use the results from searching each available Repository and download/fetch the distributions they point at
        my @fetched-candidates = eager gather for @candidates -> $store {
            # Note that this method of not fetching Zef::Distribution::Local means we cannot
            # show fetching messages that would be fired in self.fetch(|) ( such as the download uri ).
            # The reason it doesn't just fetch regardless is because it avoids caching local dev dists
            # ala `zef install .` from polluting the name/auth/api/ver namespace of the local cache.
            # TODO: Find a solution for the issues noted above which will resolve GH#261 "zef install should tell user where the install was from"
            take $_ for ($store.dist.^name.contains('Zef::Distribution::Local') || !$fetch) ?? $store !! self.fetch($store, |%_);
        }
        die "Failed to fetch any candidates. No reason to proceed" unless +@fetched-candidates;

        # Filter Stage:
        # Handle stuff like removing distributions that are already installed, that don't have
        # an allowable license, etc. It faces the same "fetch an alternative if available on failure"
        # problem outlined below under `Sort Phase` (a depends on [A, B] where A gets filtered out
        # below because it has the wrong license means we don't need anything that depends on A but
        # *do* need to replace those items with things depended on by B [which replaces A])
        my @filtered-candidates = @fetched-candidates.grep: -> $candi {
            my $*error;
            self.logger.emit({
                level   => DEBUG,
                stage   => FILTER,
                phase   => BEFORE,
                candi   => $candi,
                message => "Filtering: {$candi.dist.identity}",
            });
            KEEP $!logger.emit({
                level   => DEBUG,
                stage   => FILTER,
                phase   => AFTER,
                candi   => $candi,
                message => "Filtering [OK] for {$candi.dist.?identity // $candi.as}",
            });
            UNDO $!logger.emit({
                level   => ERROR,
                stage   => FILTER,
                phase   => AFTER,
                candi   => $candi,
                message => "Filtering [FAIL] for {$candi.dist.?identity // $candi.as}: {$*error}",
            });

            $*error = do given %!config<License> {
                when .<blacklist>.?chars && any(|.<blacklist>) ~~ any('*', $candi.dist.meta<license> // '') {
                    "License blacklist configuration exists and matches {$candi.dist.meta<license> // 'n/a'} for {$candi.dist.name}";
                }
                when .<whitelist>.?chars && any(|.<whitelist>) ~~ none('*', $candi.dist.meta<license> // '') {
                    "License whitelist configuration exists and does not match {$candi.dist.meta<license> // 'n/a'} for {$candi.dist.name}";
                }
            }

            $*error.?chars;
        }
        die "All candidates have been filtered out. No reason to proceed" unless +@filtered-candidates;


        # Sort Phase:
        # This ideally also handles creating alternate build orders when a `depends` includes
        # alternative dependencies. Then if the first build order fails it can try to fall back
        # to the next possible build order. However such functionality may not be useful this late
        # as at this point we expect to have already fetched/filtered the distributions... so either
        # we fetch all alternatives (most of which would probably would not use) or do this in a way
        # that allows us to return to a previous state in our plan (xxx: Zef::Plan is planned)
        my @sorted-candidates = self.sort-candidates(@filtered-candidates, |%_);
        die "Something went terribly wrong determining the build order" unless +@sorted-candidates;


        # Setup(?) Phase:
        # Attach appropriate metadata so we can do --dry runs using -I/some/dep/path
        # and can install after we know they pass any required tests
        my @linked-candidates = self.link-candidates(@sorted-candidates);
        die "Something went terribly wrong linking the distributions" unless +@linked-candidates;


        # Non staging workflow
        my $curi-installer = sub (*@_) {
            # Build Phase:
            my @built-candidates = ?$build ?? self.build(@_) !! @_;
            die "No installable candidates remain after `build` failures" unless +@built-candidates;

            # We aren't using the CURS workflow, so we need to add the distribution path / CURFS repo
            # to each distribution includes listing.
            for @sorted-candidates {
                $_.dist.metainfo<includes>.prepend($_.dist.path);
            }

            # Test Phase:
            my @tested-candidates = !$test
                ?? @built-candidates
                !! self.test(@built-candidates).grep({ $!force-test || .test-results.grep(!*.so).elems == 0 });

            # actually we *do* want to proceed here later so that the Report phase can know about the failed tests/build
            die "All candidates failed building and/or testing. No reason to proceed" unless +@tested-candidates;

            # Install Phase:
            # Ideally `--dry` uses a special unique CompUnit::Repository that is meant to be deleted entirely
            # and contain only the modules needed for this specific run/plan
            my @installed-candidates = ?$dry ?? @tested-candidates !! self.install(:@curs, @tested-candidates);

            # Report phase:
            # Handle exit codes for various option permutations like --force
            # Inform user of what was tested/built/installed and what failed
            # Optionally report to any cpan testers type service (testers.perl6.org)
            unless $dry {
                # Get the name of the bin scripts
                my sub bin-names($dist) { $dist.meta<files>.hash.keys.grep(*.starts-with("bin/")).map(*.substr(4)) };

                if @installed-candidates.map(*.dist).map(*.&bin-names).flat.unique -> @bins {
                    my $msg = "\n{+@bins} bin/ script{+@bins>1??'s'!!''}{+@bins??' ['~@bins~']'!!''} installed to:"
                    ~ "\n" ~ @curs.map(*.prefix.child('bin')).join("\n");
                    self.logger.emit({
                        level   => INFO,
                        stage   => REPORT,
                        phase   => LIVE,
                        message => $msg,
                    });
                }
            }

            @installed-candidates;
        }

        # This convoluted code should just be able to `use CompUnit::Repository::Staging; CompUnit::Repository::Staging.new(...)`
        # but that creates precompilation issues, presumably because the repository chain includes e.g. -I. when zef is installing
        # itself.
        # See: https://github.com/rakudo/rakudo/issues/5199
        my $staging-repo = !$stage-for-repo ?? Nil !! do {
            # We do this here instead of inside the $curs-installer sub because we
            # only want to create this repository once, even when using --serial
            my $staging-at = %!config<TempDir>.IO.child("{time}.{$*PID}.{(^10000).rand}");
            die "failed to create directory: {$staging-at.absolute}"
                unless ($staging-at.IO.e || mkdir($staging-at));

            # The first condition is preemptively trying to avoid any issue if/when CURS is moved to
            # the core (where it wouldn't be found via $*REPO.resolve(...)). The second condition finds
            # the CURFS module and then loads it's code CURAP because that won't precompile it.
            # I've tried a lot of various work arounds using EVAL, no precompilation, etc and none of
            # them have worked 100% (something might work for bin/zef and fail for `raku -e 'use Zef::CLI install .`
            # for instance).
            my %curfs-new-args = :prefix($staging-at), :name($stage-for-repo.name), :next-repo($stage-for-repo);
            my $curfs-short-name = 'CompUnit::Repository::Staging';
            (try ::($curfs-short-name))
                ?? ::($curfs-short-name).new(|%curfs-new-args)
                !! do {
                    # Find CURS from the core repository so we can load it by path later
                    my $core-repo = CompUnit::RepositoryRegistry.repository-for-name('core');
                    my $curs-dist = $core-repo.resolve(CompUnit::DependencySpecification.new(:short-name($curfs-short-name))).distribution;

                    # Load CURS by using CURAP (which doesn't precompile)
                    my $curs-provides-entry = $curs-dist.meta<provides>{$curfs-short-name};
                    my $curs-name-path = $curs-provides-entry.?keys.?head // $curs-provides-entry;
                    my $curs-handle = $curs-dist.content($curs-name-path);
                    $curs-handle.close(); # I think these handles shouldn't be opened already from ::InstalledDistribution :(
                    $*REPO.load($curs-handle.path);
                    ::($curfs-short-name).new(|%curfs-new-args);
                }
        }

        # Staging workflow
        my $curs-installer = sub (*@_) {
            # Build Phase:
            my @built-candidates = ?$build ?? self.build(@_) !! @_;
            die "No installable candidates remain after `build` failures" unless +@built-candidates;

            my @staged-candidates = @built-candidates.map({
                self.logger.emit({
                    level   => INFO,
                    stage   => STAGING,
                    phase   => BEFORE,
                    message => "Staging {$_.dist.identity}",
                });
                my Str @includes = $staging-repo.path-spec;
                $_.dist.metainfo<includes> = @includes;
                $staging-repo.install($_.dist, :precompile($!precompile-install));
                self.logger.emit({
                    level   => INFO,
                    stage   => STAGING,
                    phase   => AFTER,
                    message => "Staging [OK] for {$_.dist.identity}",
                });

                $_;
            });

            $staging-repo.remove-artifacts;

            # Test Phase:
            my @tested-candidates = !$test
                ?? @built-candidates
                !! self.test(@built-candidates).grep({ $!force-test || .test-results.grep(!*.so).elems == 0 });

            # actually we *do* want to proceed here later so that the Report phase can know about the failed tests/build
            die "All candidates failed building and/or testing. No reason to proceed" unless +@tested-candidates;

            # Install Phase:
            # Ideally `--dry` uses a special unique CompUnit::Repository that is meant to be deleted entirely
            # and contain only the modules needed for this specific run/plan
            my @installed-candidates = ?$dry ?? @tested-candidates !! do {
                self.logger.emit({
                    level   => VERBOSE,
                    stage   => INSTALL,
                    phase   => BEFORE,
                    message => "Installing staged code",
                });
                self.deploy(|@tested-candidates, :curs($staging-repo));
                self.logger.emit({
                    level   => VERBOSE,
                    stage   => INSTALL,
                    phase   => AFTER,
                    message => "Installation [OK]",
                });

                @tested-candidates;
            }

            # Report phase:
            # Handle exit codes for various option permutations like --force
            # Inform user of what was tested/built/installed and what failed
            # Optionally report to any cpan testers type service (testers.perl6.org)
            unless $dry {
                # Get the name of the bin scripts
                my sub bin-names($dist) { $dist.meta<files>.hash.keys.grep(*.starts-with("bin/")).map(*.substr(4)) };

                if @installed-candidates.map(*.dist).map(*.&bin-names).flat.unique -> @bins {
                    my $msg = "\n{+@bins} bin/ script{+@bins>1??'s'!!''}{+@bins??' ['~@bins~']'!!''} installed to:"
                    ~ "\n" ~ @curs.map(*.prefix.child('bin')).join("\n");
                    self.logger.emit({
                        level   => INFO,
                        stage   => REPORT,
                        phase   => LIVE,
                        message => $msg,
                    });
                }
            }

            @installed-candidates;
        }

        my $installer = $stage-for-repo.so ?? $curs-installer !! $curi-installer;
        my Candidate @installed = ?$serial ?? @linked-candidates.map({ |$installer($_) }) !! $installer(@linked-candidates);
        return @installed;
    }

    #| Return distributions that depend on the given identity
    method list-rev-depends($identity, Bool :indirect($) --> Array[Candidate]) {
        my $spec = Zef::Distribution::DependencySpecification.new($identity);
        my $dist = self.list-available.first(*.dist.contains-spec($spec)).?dist
                || self.list-installed.first(*.dist.contains-spec($spec)).?dist;
        return Array[Candidate].new unless $dist;

        my $rev-deps := gather for self.list-available -> $candi {
            my $specs := self.list-dependencies($candi);

            take $candi if $specs.first({ $dist.contains-spec($_, :strict) });
        }
        my Candidate @results = $rev-deps.unique(:as(*.dist.identity));
        return @results;
    }

    #| Return all distributions from all configured repositories
    method list-available(*@recommendation-manager-names --> Array[Candidate]) {
        my Candidate @available = $!recommendation-manager.available(@recommendation-manager-names);
        return @available;
    }

    #| Return all distributions in known CompUnit::Repository::Installation repositories
    method list-installed(*@repos --> Array[Candidate]) {
        my @curs  = +@repos ?? @repos !! $*REPO.repo-chain;
        my @curis = @curs.grep(CompUnit::Repository::Installation);
        my @curi-dists = @curis.map(-> $curi { Hash.new({ :$curi, :dists($curi.installed) }) }).grep({ $_<dists>.defined });
        my Candidate @dists = gather for @curi-dists -> % [:$curi, :@dists] {
            for @dists -> $curi-dist {
                if try { Zef::Distribution.new( |%($curi-dist.meta) ) } -> $dist {
                    take Candidate.new( :$dist, :from($curi.path-spec), :uri($curi.path-spec) );
                }
            }
        }
        return @dists;
    }

    method list-leaves(--> Array[Candidate]) {
        my @installed = self.list-installed;
        my @dep-specs = self.list-dependencies(@installed);

        my Candidate @leaves = gather for @installed -> $candi {
            my $dist := $candi.dist;
            take $candi unless @dep-specs.first: { $dist.contains-spec($_) }
        }
        return @leaves;
    }

    #| Return distributions that are direct dependencies of the supplied distributions
    method list-dependencies(*@candis --> Array[DependencySpecification]) {
        my $deps := gather for @candis -> $candi {
            take $_ for grep *.defined, flat
                ($candi.dist.depends-specs       if ?$!depends),
                ($candi.dist.test-depends-specs  if ?$!test-depends),
                ($candi.dist.build-depends-specs if ?$!build-depends);
        }

        # This returns both Zef::Distribution::DependencySpecification and Zef::Distribution::DependencySpecification::Any
        #my Zef::Distribution::DependencySpecification @results = $deps.unique(:as(*.identity));
        my DependencySpecification @results = $deps.unique(:as(*.identity));
        return @results;
    }

    #| Returns the best matching distributions from installed sources, in preferred order, similar to $*REPO.resolve
    method resolve($spec, CompUnit::Repository :@at --> Array[Candidate]) {
        my $candis := self.list-installed(@at).grep(*.dist.contains-spec($spec));
        my Candidate @results = $candis.sort(*.dist.ver).sort(*.dist.api).reverse;
        return @results;
    }

    #| Return true if the requested dependency is already installed
    multi method is-installed(Str $spec, |c --> Bool:D) {
        return self.is-installed(Zef::Distribution::DependencySpecification.new($spec));
    }

    #| Return true of one-or-more of the requested dependencies are already installed
    multi method is-installed(Zef::Distribution::DependencySpecification::Any $spec, |c --> Bool:D) {
        return so $spec.specs.first({ self.is-installed($_, |c) });
    }

    #| Return true if the requested dependency is already installed
    multi method is-installed(Zef::Distribution::DependencySpecification $spec, |c --> Bool:D) {
        return do given $spec.?from-matcher {
            when 'bin'    { so Zef::Utils::FileSystem::which($spec.name) }
            when 'native' { so self!native-library-is-installed($spec) }
            default       { so self.resolve($spec, |c).so }
        }
    }

    #| Return true of a native library can be seen by NativeCall
    method !native-library-is-installed($spec --> Bool) {
        use MONKEY-SEE-NO-EVAL;
        my $lib = "'$spec.name()'";
        $lib = "$lib, v$spec.ver()" if $spec.ver;
        try {
            EVAL qq[use NativeCall; sub native_library_is_installed is native($lib) \{ !!! \}; native_library_is_installed(); ];
            CATCH { default { return False if .payload.starts-with("Cannot locate native library") } }
        }
        return True;
    }

    #| Topological sort used to determine which dependency can be processed next in a given phase
    method sort-candidates(@candis --> Array[Candidate]) {
        my Candidate @tree;
        my $visit = sub ($candi) {
            return if ($candi.dist.metainfo<marked> // 0) == 1;
            if ($candi.dist.metainfo<marked> // 0) == 0 {
                $candi.dist.metainfo<marked> = 1;

                my @deps = |self.list-dependencies($candi);

                for @deps -> $m {
                    for @candis.grep(*.dist.contains-spec($m)) -> $m2 {
                        $visit($m2);
                    }
                }
                @tree.append($candi);
            }
        };

        for @candis -> $candi {
            $visit($candi) if ($candi.dist.metainfo<marked> // 0) == 0;
        }

        .dist.metainfo<marked> = Nil for @tree;
        return @tree;
    }

    #| Adds appropriate include (-I / PERL6LIB) paths for dependencies
    proto method link-candidates(|) {*}
    multi method link-candidates(Bool :recursive($)! where *.so, *@candidates) {
        # :recursive
        # Given Foo::XXX that depends on Bar::YYY that depends on Baz::ZZZ
        #   - Foo::XXX -> -I/Foo/XXX -I/Bar/YYY -I/Baz/ZZZ
        #   - Bar::YYY -> -I/Bar/YYY -I/Baz/ZZZ
        #   - Baz::ZZZ -> -I/Baz/ZZZ

        # XXX: Need to change this so it only add indirect dependencies
        # instead of just using recursion on the array in order. Otherwise there
        # can be distributions that are part of a different dependency
        # chain will end up with some extra includes

        my @linked = self.link-candidates(@candidates);
        @ = @linked.map: -> $candi { # can probably use rotor instead of doing the `@a[$index + 1..*]` dance
            my @direct-includes    = $candi.dist.metainfo<includes>.grep(*.so);
            my @recursive-includes = try @linked[(++$)..*]\
                .map(*.dist.metainfo<includes>).map(*.flat);
            my @unique-includes    = unique(@direct-includes, @recursive-includes);
            my Str @results        = @unique-includes.grep(*.so);
            $candi.dist.metainfo<includes> = @results;
            $candi;
        }
    }
    multi method link-candidates(Bool :inclusive($)! where *.so, *@candidates) {
        # :inclusive
        # Given Foo::XXX that depends on Bar::YYY that depends on Baz::ZZZ
        #   - Foo::XXX -> -I/Foo/XXX -I/Bar/YYY -I/Baz/ZZZ
        #   - Bar::YYY -> -I/Foo/XXX -I/Bar/YYY -I/Baz/ZZZ
        #   - Baz::ZZZ -> -I/Foo/XXX -I/Bar/YYY -I/Baz/ZZZ
        my @linked = self.link-candidates(@candidates);
        @ = @linked.map(*.dist.metainfo<includes>).map(*.flat).unique;
    }
    multi method link-candidates(*@candidates) {
        # Default
        # Given Foo::XXX that depends on Bar::YYY that depends on Baz::ZZZ
        #   - Foo::XXX -> -I/Foo/XXX -I/Bar/YYY
        #   - Bar::YYY -> -I/Bar/YYY -I/Baz/ZZZ
        #   - Baz::ZZZ -> -I/Baz/ZZZ
        @ = @candidates.map: -> $candi {
            my @dep-specs = |self.list-dependencies($candi);

            # this could probably be done in the topological-sort itself
            my $includes := eager gather for @dep-specs -> $spec {
                CANDIDATE: for @candidates -> $fcandi {
                    my $fdist := $fcandi.dist;
                    if $fdist.contains-spec($spec) {
                        take $fdist.IO.absolute;
                        take $_ for |$fdist.metainfo<includes>.grep(*.so);
                        last CANDIDATE;
                    }
                }
            }

            my Str @results = $includes.unique;
            $candi.dist.metainfo<includes> = @results;

            $candi;
        }
    }
}



