""" The `Best Model Order Equality` chooses the best model (i.e. the one with the highest
`coefficient of determination`) as the indicator of the performance of each group of uniquely
identified resources (e.g. corresponding to the same function). Then each pair of baseline and
target models is compared lexicographically (e.g. the `linear` model is lexicographically smaller
than `quadratic` model), and any change in this ordering is detected as either ``Optimization`` or
``Degradation`` if the minimal confidence of the models is above certain threshold.

  - **Detects**: `Order` changes; ``Optimization`` and ``Degradation``
  - **Confidence**: Minimal `coefficient of determination` of best models of baseline and target
    minor versions
  - **Limitations**: Profiles postprocessed by :ref:`postprocessors-regression-analysis`

The example of the output generated by the `BMOE` method is as follows ::

    * 1eb3d6: Fix the degradation of search
    |\
    | * 7813e3: Implement new version of search
    |   > collected by complexity+regression_analysis for cmd: '$ mybin'
    |     > applying 'best_model_order_equality' method
    |       - Optimization         at SLList_search(SLList*, int)
    |           from: power -> to: linear (with confidence r_square = 0.99)
    |
    * 7813e3: Implement new version of search
    |\
    | * 503885: Fix minor issues
    |   > collected by complexity+regression_analysis for cmd: '$ mybin'
    |     > applying 'best_model_order_equality' method
    |       - Degradation          at SLList_search(SLList*, int)
    |           from: linear -> to: power (with confidence r_square = 0.99)
    |
    * 503885: Fix minor issues

In the output above, we detected the ``Optimization`` between commits ``1eb3d6`` (target) and
``7813e3`` (baseline), where the best performance model of running time of ``SLList_search``
function changed from **power** model to **linear**. For the methods based on
:ref:`postprocessors-regression-analysis` we use the `coefficient of determination` (:math:`r^2`)
to represent a confidence, and take the minimal `coefficient of determination` of target and
baseline model as a confidence for this detected change. Since :math:`r^2` is almost close to the
value `1.0` (which would mean, that the model precisely fits the measured values), this signifies
that the best model fit the data tightly and hence the detected optimization is **not spurious**.
"""

from __future__ import annotations

# Standard Imports
from typing import Any, Iterable, TYPE_CHECKING

# Third-Party Imports

# Perun Imports
from perun.check.methods.abstract_base_checker import AbstractBaseChecker
from perun.utils.structs import DegradationInfo, PerformanceChange
import perun.check.detection_kit as detection

if TYPE_CHECKING:
    from perun.profile.factory import Profile


CONFIDENCE_THRESHOLD = 0.9
MODEL_ORDERING = [
    "constant",
    "logarithmic",
    "linear",
    "quadratic",
    "power",
    "exponential",
]


class BestModelOrderEquality(AbstractBaseChecker):
    def check(
        self, baseline_profile: Profile, target_profile: Profile, **_: Any
    ) -> Iterable[DegradationInfo]:
        """Checks between a pair of (baseline, target) profiles, whether there can be degradation detected

        This is based on simple heuristic, where for the same function models, we only check the order
        of the best fit models. If these differ, we detect the possible degradation.

        :param baseline_profile: baseline against which we are checking the degradation
        :param target_profile: profile corresponding to the checked minor version
        :param _: unification with other detection methods (unused in this method)
        :returns: tuple (degradation result, degradation location, degradation rate)
        """
        best_baseline_models = detection.get_filtered_best_models_of(
            baseline_profile, group="param"
        )
        best_target_models = detection.get_filtered_best_models_of(target_profile, group="param")

        for uid, best_model in best_target_models.items():
            best_baseline_model = best_baseline_models.get(uid)
            if best_baseline_model:
                confidence = min(best_baseline_model.r_square, best_model.r_square)
                if (
                    confidence >= CONFIDENCE_THRESHOLD
                    and best_baseline_model.type != best_model.type
                ):
                    baseline_ordering = MODEL_ORDERING.index(best_baseline_model.type)
                    target_ordering = MODEL_ORDERING.index(best_model.type)
                    if baseline_ordering > target_ordering:
                        change = PerformanceChange.Optimization
                    else:
                        change = PerformanceChange.Degradation
                    degradation_rate = target_ordering - baseline_ordering
                else:
                    change = PerformanceChange.NoChange
                    degradation_rate = 0

                yield DegradationInfo(
                    res=change,
                    t="model",
                    loc=uid,
                    fb=best_baseline_model.type,
                    tt=best_model.type,
                    rd=degradation_rate,
                    ct="r_square",
                    cr=confidence,
                )
