#  -----------------------------------------------------------------------------------------
#  (C) Copyright IBM Corp. 2021- 2023.
#  https://opensource.org/licenses/BSD-3-Clause
#  -----------------------------------------------------------------------------------------

from __future__ import print_function

import logging

from ibmfl.exceptions import HyperparamsException

VALID_LOSSES = ["binary_crossentropy", "categorical_crossentropy", "least_squares", "least_absolute_deviation", "auto"]
VALID_SCORES = []

logger = logging.getLogger(__name__)


def validate_parameters(hyperparams):
    """
    Valdiates specific hyperparameters parameters that are defined in
    `hyperparams` variables. Raises an exception for certain parameters that
    are either required or beyond the defined range if they are defined.

    :param hyperparams: Dictionary containing values for the hyperparameters.
    :type  hyperparams: `dict`
    :rtype: None
    """
    try:
        # Check Hyperparameter Type
        if not isinstance(hyperparams, dict):
            raise ValueError("Provided hyperparameter is not valid.")

        # Global Hyperparameter Check
        if "global" in hyperparams:
            params = hyperparams["global"]
        else:
            raise ValueError("Global parameters have not been defined.")

        if "learning_rate" in params:
            if params["learning_rate"] <= 0:
                raise ValueError("learning_rate={} must be strictly " "positive".format(params["learning_rate"]))
        else:
            raise ValueError("learning_rate has not been defined.")

        if "loss" in params:
            if params["loss"] not in VALID_LOSSES:
                raise ValueError(
                    "Loss {} is currently not supported."
                    "Accpted losses: {}".format(params["loss"], ", ".join(VALID_LOSSES))
                )
        else:
            raise ValueError("loss has not been defined.")

        if "num_classes" in params:
            # Validate Class Parameter Types
            if type(params["num_classes"]) is not int:
                raise ValueError(
                    "Provided classes value's type is not valid, " "should be an int value >= 2 for classification."
                )

            # Validate Classification Case
            if params["loss"] != "least_squares":
                if params["num_classes"] < 0:
                    raise ValueError("Provided class value must be >= 2 for " "classification.")

                if params["loss"] == "binary_crossentropy" and params["num_classes"] != 2:
                    raise ValueError("Binary class models must have class of 2.")
                elif params["loss"] == "categorical_crossentropy" and params["num_classes"] <= 2:
                    raise ValueError("Multiclass models must have class > 2.")
                elif params["loss"] == "auto":
                    if params["num_classes"] < 2:
                        raise ValueError("Class value must be >= 2.")
                    else:
                        logging.warning(
                            "Obtaining class labels based on local dataset. "
                            "This may cause failures during aggregation "
                            "when parties have distinctive class labels."
                        )
        else:
            # Handle Classes Not Defined Case
            if params["loss"] != "least_squares":
                raise ValueError(
                    "Classes has not been defined. Should provide " "a value >= 2 for classification models."
                )

        if "max_bins" in params:
            if not (2 <= params["max_bins"] and params["max_bins"] <= 255):
                raise ValueError(
                    "max_bins={} should be no smaller than 2 " "and no larger than 255.".format(params["max_bins"])
                )

        if "max_iter" in params:
            if params["max_iter"] < 1:
                raise ValueError("max_iter={} must not be smaller " "than 1.".format(params["max_iter"]))
        else:
            raise ValueError("max_iter has not been defined.")

        if "max_depth" in params:
            if params["max_depth"] is not None and params["max_depth"] < 0:
                raise ValueError("max_depth={} must be strictly greater" "than 0.".format(params["max_depth"]))

        if "max_leaf_nodes" in params:
            if params["max_leaf_nodes"] is not None and params["max_leaf_nodes"] <= 1:
                raise ValueError(
                    "max_leaf_nodes={} must be strictly greater" "than 1.".format(params["max_leaf_nodes"])
                )

        if "min_samples_leaf" in params:
            if params["min_samples_leaf"] is not None and params["min_samples_leaf"] < 0:
                raise ValueError("min_sample_leaf={} must not be smaller " "than 0".format(params["min_samples_leaf"]))

        if "data_sketch_accuracy" in params and params["data_sketch_accuracy"] is not None:
            if params["data_sketch_accuracy"] <= 0 or params["data_sketch_accuracy"] >= 1:
                raise ValueError("data_sketch_accuracy={} must be strictly greater than 0 and less than 1")

    except Exception as ex:
        logger.exception(str(ex))
        raise HyperparamsException("Defined global hyperparameters malformed. " + str(ex))


def init_parameters(obj, hyperparameters):
    """
    Initializes the hyperparameters to the corresponding object as an attribute.
    The parameters are propagated by reference, so changes should propagate to
    respective object passed into the function.

    :param obj: Any class object which to tie initialize the hyperparameters with.
    :type  obj: Object
    :param hyperparameters: Dictionary containing values for the hyperparameters.
    :type  hyperparameters: `dict`
    """
    # Initialize Global Configuration Parameter
    params = hyperparameters["global"]
    setattr(obj, "param", params)

    # Initialize Attributes (Pre-Checked Parameters)
    setattr(obj, "learning_rate", params["learning_rate"])
    setattr(obj, "loss", params["loss"])
    setattr(obj, "max_iter", params["max_iter"])

    if params["loss"] == "least_squares":
        setattr(obj, "num_classes", 1)
    elif params["loss"] in ["binary_crossentropy", "categorical_crossentropy", "auto"]:
        setattr(obj, "num_classes", params["num_classes"])

    # Initialize Attributes (Optional Values - Based on Default Parameters)
    if "l2_regularization" not in params or params["l2_regularization"] is None:
        setattr(obj, "l2_regularization", 0)
    else:
        setattr(obj, "l2_regularization", params["l2_regularization"])

    if "max_bins" not in params:
        setattr(obj, "max_bins", 255)
    else:
        setattr(obj, "max_bins", params["max_bins"])

    if "max_depth" not in params or params["max_depth"] is None:
        setattr(obj, "max_depth", None)
    else:
        setattr(obj, "max_depth", params["max_depth"])

    if "max_leaf_nodes" not in params or params["max_leaf_nodes"] is None:
        setattr(obj, "max_leaf_nodes", 31)
    else:
        setattr(obj, "max_leaf_nodes", params["max_leaf_nodes"])

    if "min_samples_leaf" not in params or params["min_samples_leaf"] is None:
        setattr(obj, "min_samples_leaf", 20)
    else:
        setattr(obj, "min_samples_leaf", params["min_samples_leaf"])

    if "random_state" in params:
        setattr(obj, "random_state", params["random_state"])
    else:
        setattr(obj, "random_state", None)

    if "scoring" in params:
        setattr(obj, "scoring", params["scoring"])
    else:
        setattr(obj, "scoring", None)

    if "verbose" not in params or params["verbose"] is None:
        setattr(obj, "verbose", False)
    else:
        setattr(obj, "verbose", True)

    if "data_sketch_accuracy" not in params or params["data_sketch_accuracy"] is None:
        setattr(obj, "data_sketch_accuracy", None)  # default
    else:
        setattr(obj, "data_sketch_accuracy", params["data_sketch_accuracy"])

    return obj
