# SPDX-FileCopyrightText: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later

package ksb::DebugOrderHints;

use ksb;

# ksb::DebugOrderHints
#
# This module is motivated by the desire to help the user debug a kdesrc-build
# failure more easily. It provides support code to rank build failures on a per
# module from 'most' to 'least' interesting, as well as to sort the list of
# (all) failures by their respective rankings. This ranking is determined by
# trying to evaluate whether or not a given build failure fits a number of
# assumptions/heuristics. E.g.: a module which fails to build is likely to
# trigger build failures in other modules that depend on it (because of a
# missing dependency).
#

sub _getPhaseScore
{
    my $phase = shift;

    #
    # Assumption: build & install phases are interesting.
    # Install is particularly interesting because that should 'rarely' fail,
    # and so if it does there are probably underlying system issues at work.
    #
    # Assumption: 'test' is opt in and therefore the user has indicated a
    # special interest in that particular module?
    #
    # Assumption: source updates are likely not that interesting due to
    # e.g. transient network failure. But it might also indicate something
    # more serious such as an unclean git repository, causing scm commands
    # to bail.
    #

    return 4 if ($phase eq 'install');
    return 3 if ($phase eq 'test');
    return 2 if ($phase eq 'build');
    return 1 if ($phase eq 'update');
    return 0;
}

sub _compareDebugOrder
{
    my ($moduleGraph, $extraDebugInfo, $a, $b) = @_;
    my $nameA = $a->name();
    my $nameB = $b->name();

    #
    # Enforce a strict dependency ordering.
    # The case where both are true should never happen, since that would
    # amount to a cycle, and cycle detection is supposed to have been
    # performed beforehand.
    #
    # Assumption: if A depends on B, and B is broken then a failure to build
    # A is probably due to lacking a working B.
    #
    my $bDependsOnA = $moduleGraph->{$nameA}->{votes}->{$nameB} // 0;
    my $aDependsOnB = $moduleGraph->{$nameB}->{votes}->{$nameA} // 0;
    my $order = $bDependsOnA ? -1 : ($aDependsOnB ? 1 : 0);

    return $order if $order;

    #
    # TODO we could tag explicitly selected modules from command line?
    # If we do so, then the user is probably more interested in debugging
    # those first, rather than 'unrelated' noise from modules pulled in due
    # to possibly overly broad dependency declarations. In that case we
    # should sort explicitly tagged modules next highest, after dependency
    # ordering.
    #

    #
    # Assuming no dependency resolution, next favour possible root causes as
    # may be inferred from the dependency tree.
    #
    # Assumption: there may be certain 'popular' modules which rely on a
    # failed module. Those should probably not be considered as 'interesting'
    # as root cause failures in less popuplar dependency trees. This is
    # essentially a mitigation against noise introduced from raw 'popularity'
    # contests (see below).
    #
    my $isRootA = (scalar keys %{$moduleGraph->{$nameA}->{deps}}) == 0;
    my $isRootB = (scalar keys %{$moduleGraph->{$nameB}->{deps}}) == 0;

    return -1 if $isRootA && !$isRootB;
    return 1 if $isRootB && !$isRootA;

    #
    # Next sort by 'popularity': the item with the most votes (back edges) is
    # depended on the most.
    #
    # Assumption: it is probably a good idea to debug that one earlier.
    # This would point the user to fixing the most heavily used dependencies
    # first before investing time in more 'exotic' modules
    #
    my $voteA = scalar keys %{$moduleGraph->{$nameA}->{votes}};
    my $voteB = scalar keys %{$moduleGraph->{$nameB}->{votes}};
    my $votes = $voteB <=> $voteA;

    return $votes if $votes;

    #
    # Try and see if there is something 'interesting' that might e.g. indicate
    # issues with the system itself, preventing a successful build.
    #
    my $phaseA = _getPhaseScore($extraDebugInfo->{phases}->{$nameA} // '');
    my $phaseB = _getPhaseScore($extraDebugInfo->{phases}->{$nameB} // '');
    my $phase = $phaseB <=> $phaseA;

    return $phase if $phase;

    #
    # Assumption: persistently failing modules do not prompt the user
    # to act and therefore these are likely not that interesting.
    # Conversely *new* failures are.
    #
    # If we get this wrong the user will likely be on the case anyway:
    # someone does not need prodding if they have been working on it
    # for the past X builds or so already.
    #
    my $failCountA = $a->getPersistentOption('failure-count');
    my $failCountB = $b->getPersistentOption('failure-count');
    my $failCount = ($failCountA // 0) <=> ($failCountB // 0);

    return $failCount if $failCount;

    #
    # If there is no good reason to perfer one module over another,
    # simply sort by name to get a reproducible order.
    # That simplifies autotesting and/or reproducible builds.
    # (The items to sort are supplied as a hash so the order of keys is by
    # definition not guaranteed.)
    #
    my $name = ($nameA cmp $nameB);

    return $name;
}

sub sortFailuresInDebugOrder
{
    my ($moduleGraph, $extraDebugInfo, $failuresRef) = @_;
    my @failures = @{$failuresRef};

    my @prioritised = sort {
        _compareDebugOrder($moduleGraph, $extraDebugInfo, $a, $b);
    } (@failures);

    return @prioritised;
}

1;
