from pycaret import regression
from loguru import logger
import copy
import pandas as pd
import math
import numpy as np

from scripts.constants.app_configuration import PARAMETER_INPUT, COLUMN_RENAMER, LOGIN_TOKEN
from scripts.constants.app_constants import Constants
from scripts.core.model_factory.api_util import GetBetaInfo


def get_beta_info():
    login_token = LOGIN_TOKEN
    headers_data = {
        'User_id': 'user_100',
        'Login-Token': login_token,
        'Content-Type': 'application/json',
        'Cookie': f'login-token={login_token}; userId=user_100'
    }
    final_operating_limits = {}
    for col in PARAMETER_INPUT:
        tag_id = COLUMN_RENAMER[col]
        # get_beta_info_instance = GetBetaInfo()
        try:
            beta_details = get_beta_info_instance.beta_range(tag_id, headers_data)
            final_operating_limits[col] = beta_details

        except Exception as ex:
            logger.info(f"An unexpected error occurred: {ex}")
    parameter_operating_limits = {}

    for category, beta_values in final_operating_limits.items():
        for beta_key, values in beta_values.items():
            if beta_key not in parameter_operating_limits:
                parameter_operating_limits[beta_key] = {}
            parameter_operating_limits[beta_key][category] = values
    return parameter_operating_limits


# status_column = "R5 Reaction Section.R5 Reaction Status"


mapping_dict = {
    'Alcohol flow': 'Totalizer Alcohol'
    }

class ProfileGenerator(object):

    def __init__(self, df, df_feature_importance, target_col):
        df = df[df["Feed Drum.FIC-5102"] > 500]
        df = df[df["Feed Drum.FIC-5103"] > 500]
        df = df[df["Feed Drum.FIC-5111"] > 500]
        df = df[df["Feed Drum.FIC-5161"] >= 0]
        df = df[df["Reactor.CATCIRCULATION"] > 0]
        self.df = df
        self.target_col = target_col
        self.df_feature_importance = df_feature_importance

    @staticmethod
    def diff_calculator(_df, calc_column, diff_column):
        _df[calc_column] = _df[diff_column].diff()
        _df[calc_column] = np.where(_df[calc_column] > 0, _df[calc_column], 0)
        return _df

    def generate_profile(self):
        df_historic_data = self.df

        # Add the delta value column for both the totalizer columns
        # # Calculations for totalizer tags
        # diff_column = 'Totalizer Alcohol'
        # calc

        for each_key in mapping_dict:
            df_historic_data = self.diff_calculator(_df=df_historic_data, calc_column=each_key,
                                                    diff_column=mapping_dict[each_key])

        
        # # Filter out data from the period when the reactor was active:
        # logger.debug("Profile Data Count after filter-01 : {}".format(
        #     str(df_historic_data[status_column].count())))

        # df_historic_data = df_historic_data.loc[df_historic_data[status_column] == 1]
        # logger.debug("Profile Data Count after filter-02 : {}".format(
        #     str(df_historic_data[status_column].count())))
        # # Both Totalizer values need to have value greater than 0
        # df_historic_data = df_historic_data.loc[(df_historic_data['AT'] > 0) & (df_historic_data['AU'] > 0)]
        # logger.debug("Profile Data Count after filter-03 : {}".format(
        #     str(df_historic_data[status_column].count())))

        return df_historic_data

    def generate_bound(self):
        df_profile = self.generate_profile()
        # col_beta_avg = "Live Beta"
        full_profile = list()
        importances = dict(zip(self.df_feature_importance['columns'], self.df_feature_importance['importances']))
        parameter_operating_limits = get_beta_info()
        df_profile_temp = df_profile
        x_cols = PARAMETER_INPUT
        if not df_profile_temp.empty:
            for each_column in x_cols:
                profile_data = dict()
                profile_data["column"] = each_column
                profile_data["importances"] = importances[each_column]
                profile_data["min"] = math.floor(df_profile_temp[each_column].min())
                profile_data["max"] = math.ceil(df_profile_temp[each_column].max())
                profile_data["mean"] = df_profile_temp[each_column].mean()
                profile_data["std_dev"] = df_profile_temp[each_column].std()
                profile_data["range_min"] = df_profile_temp[each_column].mean() - 1.5 * df_profile_temp[
                    each_column].std()
                profile_data["range_max"] = df_profile_temp[each_column].mean() + 1.5 * df_profile_temp[
                    each_column].std()
                profile_data["golden_batch_minimum"] = math.floor(
                    max(profile_data["range_min"], profile_data["min"]))
                profile_data["golden_batch_maximum"] = math.ceil(
                    min(profile_data["range_max"], profile_data["max"]))
                full_profile.append(profile_data)
        df_report = pd.DataFrame(full_profile)
        df_report["module"] = Constants.MODULE_NAME
        df_report["model_version"] = Constants.VERSION
        return df_report


class GetBestModel:
    def __init__(self, df, raw_df, target_col, list_of_models, no_of_models=1):
        self.df = df
        self.raw_df = raw_df
        self.target_col = target_col
        self.no_of_models = no_of_models
        self.list_of_models = list_of_models

    def get_profile(self, df_feature_importance):
        total_cols = list(self.df.columns)
        x_cols = []
        for i in total_cols:
            if i != self.target_col:
                x_cols.append(i)
        target = self.target_col
        profile_df = self.df.loc[(self.df[target] >= self.df[target].quantile(0.85))]
        importances = dict(zip(df_feature_importance['columns'], df_feature_importance['importances']))
        selected_columns = copy.deepcopy(x_cols)
        selected_columns.append(target)
        pdf = profile_df[selected_columns]
        critical_parameter = selected_columns.copy()
        critical_parameter.remove(target)
        full_profile = list()
        for each_column in critical_parameter:
            profile_data = dict()
            profile_data["column"] = each_column
            profile_data["importances"] = importances[each_column]
            profile_data["min"] = math.floor(pdf[each_column].min())
            profile_data["max"] = math.ceil(pdf[each_column].max())
            profile_data["mean"] = pdf[each_column].mean()
            profile_data["std_dev"] = pdf[each_column].std()
            profile_data["range_min"] = pdf[each_column].mean() - 1.5 * pdf[each_column].std()
            profile_data["range_max"] = pdf[each_column].mean() + 1.5 * pdf[each_column].std()
            profile_data["golden_batch_minimum"] = math.floor(max(profile_data["range_min"], profile_data["min"]))
            profile_data["golden_batch_maximum"] = math.ceil(min(profile_data["range_max"], profile_data["max"]))
            full_profile.append(profile_data)
        df_report = pd.DataFrame(full_profile)
        df_report["operating_min"] = df_report["golden_batch_minimum"]
        df_report["operating_max"] = df_report["golden_batch_maximum"]
        df_report["module"] = Constants.MODULE_NAME
        df_report["model_version"] = Constants.VERSION
        df_report = df_report.round(1)
        df_report.sort_values(by='importances', ascending=False, inplace=True)
        return df_report

    def compare_get_best_model(self, fine_tune_tech, comparison_metric):
        """
        Train and compares the model based on the finetune tech and comparison metric
        :param fine_tune_tech: search library for fine-tuning of the selected model
        :param comparison_metric: metrics to select the best model
        :return: the best model, model name, metrics and parameters
        """
        try:
            logger.info("Using Pycaret to train mentioned models")
            regression.setup(data=self.df, target=self.target_col,
                             n_features_to_select=len(list(self.df.columns)), preprocess=False)
            logger.info(f"Selecting the best model using the metric {comparison_metric}")
            best_model = regression.compare_models(include=self.list_of_models, sort=comparison_metric,
                                                   n_select=self.no_of_models)
            logger.info("Tuning the Model")
            tuned_model = regression.tune_model(best_model, optimize=comparison_metric, search_library=fine_tune_tech)
            results = regression.pull()
            get_best_model_row = results.iloc[0]
            best_metrics = get_best_model_row.to_dict()
            best_metrics.pop('Model', None)
            model_name = str(tuned_model).split('(')[0]
            hyper_params = tuned_model.get_params()
            logger.info("Model Training Completed")
            logger.info("Loading Feature Importance")
            imp_list = list(abs(tuned_model.feature_importances_ * 100))
            importance_list = []
            for i in imp_list:
                importance_list.append(round(i, 2))
            feature_importance_df = pd.DataFrame({'columns': regression.get_config('X_train').columns,
                                                  'importances': importance_list}).sort_values(by='importances',
                                                                                               ascending=False)
            logger.info("Loaded Feature Importance")
            print(feature_importance_df)
            # profile_df = self.get_profile(feature_importance_df)
            profile_df = ProfileGenerator(self.raw_df, feature_importance_df, self.target_col).generate_bound()
            return tuned_model, model_name, best_metrics, hyper_params, profile_df
        except Exception as e:
            logger.exception(f"Unable to select the best model - {e}")
