# SPDX-License-Identifier: BSD-2-Clause
""" Provides a command line interface to update a performance limits item. """

# Copyright (C) 2023 embedded brains GmbH & Co. KG
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import contextlib
import json
import sys
from typing import Any, Dict, List

from specitems import (Item, ItemCache, ItemCacheConfig,
                       create_argument_parser, create_config, init_logging,
                       item_is_enabled)
from specware import load_specware_config, SpecWareTypeProvider

from .testoutputparser import augment_report

_LimitsByUID = Dict[str, Dict[str, Dict[str, float]]]


def _calculate_limits(runtime_measurement: Dict[str, Any], uid: str,
                      variant: str, limits_by_uid: _LimitsByUID,
                      args: Any) -> Dict[str, float]:
    minimum = runtime_measurement["min"]
    median = runtime_measurement["q2"]
    maximum = runtime_measurement["max"]
    min_lower_bound = args.lower_bound_scaler * minimum
    max_upper_bound = args.upper_bound_scaler * maximum
    median_lower_bound = max(args.lower_bound_scaler * median, min_lower_bound)
    median_upper_bound = min(args.upper_bound_scaler * median, max_upper_bound)
    if args.lazy:
        try:
            current_limits = limits_by_uid[uid][variant]
        except KeyError:
            pass
        else:
            limit = current_limits["min-lower-bound"]
            if limit <= minimum:
                min_lower_bound = limit
            limit = current_limits["median-lower-bound"]
            if limit <= median:
                median_lower_bound = limit
            limit = current_limits["median-upper-bound"]
            if limit >= median:
                median_upper_bound = limit
            limit = current_limits["max-upper-bound"]
            if limit >= maximum:
                max_upper_bound = limit
    return {
        "max-upper-bound": max_upper_bound,
        "median-lower-bound": median_lower_bound,
        "median-upper-bound": median_upper_bound,
        "min-lower-bound": min_lower_bound
    }


def _update_perf_limits(reports: List[Any], name_to_uid: Dict[str, str],
                        limits_by_uid: _LimitsByUID, args: Any) -> None:
    for report in reports:
        if "performance" in report["executable"]:
            if "info" not in report:
                augment_report(report, report["output"])
            try:
                test_cases = report["test-suite"]["test-cases"]
            except KeyError:
                print("no performance data available for:",
                      report["executable"])
                continue
            for test_case in test_cases:
                for runtime_measurement in test_case["runtime-measurements"]:
                    uid = name_to_uid[runtime_measurement["name"]]
                    variant = runtime_measurement["variant"]
                    limits_by_uid.setdefault(uid,
                                             {})[variant] = _calculate_limits(
                                                 runtime_measurement, uid,
                                                 variant, limits_by_uid, args)


def _write_perf_limits(perf_limits: Item, limits_by_uid: _LimitsByUID) -> None:
    links = [
        link for link in perf_limits["links"]
        if link["role"] != "performance-runtime-limits"
    ]
    for uid, limits in sorted(limits_by_uid.items()):
        links.append({
            "limits": limits,
            "role": "performance-runtime-limits",
            "uid": uid
        })
    perf_limits.data["links"] = links
    perf_limits.save()


def cliupdateperf(argv: list[str] = sys.argv) -> None:
    """ Updates a performance limits item. """
    parser = create_argument_parser()
    parser.add_argument("--config-file",
                        type=str,
                        default=None,
                        help="use this configuration file")
    parser.add_argument('--lazy',
                        action="store_true",
                        help="only update necessary limits")
    parser.add_argument('--lower-bound-scaler',
                        type=float,
                        default=0.9,
                        help="scaler to define lower bounds (default: 0.9)")
    parser.add_argument('--upper-bound-scaler',
                        type=float,
                        default=1.1,
                        help="scaler to define upper bounds (default: 1.1)")
    parser.add_argument(
        "perf_limits_uid",
        metavar="PERF_LIMITS_UID",
        nargs=1,
        help="the UID of the performance limits item to update")
    parser.add_argument("reports",
                        metavar="REPORTS",
                        nargs="+",
                        help="the test report files")
    args = parser.parse_args(argv[1:])
    init_logging(args)
    config, working_directory = load_specware_config(args.config_file)
    with contextlib.chdir(working_directory):
        item_cache = ItemCache(create_config(config["spec"], ItemCacheConfig),
                               type_provider=SpecWareTypeProvider({}),
                               is_item_enabled=item_is_enabled)
        name_to_uid: Dict[str, str] = {}
        for item in item_cache.items_by_type[
                "requirement/non-functional/performance-runtime"]:
            name_to_uid[item.ident] = item.uid
        perf_limits = item_cache[args.perf_limits_uid[0]]
        limits_by_uid: _LimitsByUID = {}
        for link in perf_limits["links"]:
            if link["role"] == "performance-runtime-limits":
                limits_by_uid[link["uid"]] = link["limits"]
        for report in args.reports:
            with open(report, "r", encoding="utf-8") as src:
                data = json.load(src)
            _update_perf_limits(data["reports"], name_to_uid, limits_by_uid,
                                args)
        _write_perf_limits(perf_limits, limits_by_uid)
